Last weekend, I participated in the Intigriti-CTF-2024, and it was really fun because there were so many problems of various types. I’d like to take some time to review the challenges I solved and the ones I attempted during the competition.

The first category is the warmup. Since the scores were low, I thought I would solve them easily, but they turned out to be not that straightforward.

Warm Up



Sanity Check


Sanity Check



This challenge was a simple one. Clicking the link redirected to the competition’s Discord server. By checking the Discord channel, the flag could be found, making it an easy and straightforward challenge.



Sanity Check



Social


social


This challenge required checking the CTF’s Twitter, YouTube, and Reddit to find parts of the flag, which needed to be combined to solve the challenge.

At first, I thought it would be easy, but in the case of Twitter, I couldn’t access certain pages without logging in. I wasted some time assuming the challenge was designed to be solvable without logging in. In the end, logging in was necessary. For Twitter, the required value was found in the mentions after logging in.


 0110100000110000011100000011001101011111011110010011000001110101


Next is YouTube.


social


There was an issue where the required part could be found by sorting the comments by recent, but I managed to solve it this way.

Next is Reddit.


social


It’s not just about combining the parts; you also need to apply the correct decoding method to solve it.


1.0110100000110000011100000011001101011111011110010011000001110101
2.5f336e6a30795f
3.ZDRfYzdm


If you convert the first part to ASCII,


social


Second


social


Last


social


So the flag becomes INTIGRITI{h0p3_y0u_3nj0y_d4_c7f}.



Lost Program


social


TODO: find lots of 😎🐛 on 🥷🥝🎮

Using these hints, I figured out what they meant after some thought. The first emoji represented bug bounty, and the second referred to Ninja Kiwi. With that understanding, I constructed the flag accordingly.


social


The correct flag is INTIGRITI{Ninja_Kiwi_Games}.



IN Plain Sight


social


This challenge was one I checked after the competition ended. When downloading the image, you could see a picture of a cat.


social


social


Looking at the hex values, I noticed that the file contained a flag.png and identified the “PK” file signature, indicating that another file might be hidden inside. To investigate further, I used binwalk to analyze the file.


social


By running binwalk on the file, you can see the PK file signature, indicating the presence of an embedded file.


social


A zip file is visible in the results, but the flag.png file itself is not directly shown.


social


The zip file was password-protected, making it inaccessible. I couldn’t solve it before the competition ended, but according to the write-up, here’s what happened:


social


In HxD, upon closer inspection, you can find a specific phrase embedded in the file. Entering that value as the password allows you to extract the contents of the zip file.


social


However, when you open the flag.png file, you’ll see that it’s just a blank white image.


social


If you paint the background black in a program like Paint, the hidden flag becomes visible on the image.



IrrORversible


social


When I first approached this challenge, I thought it might require a brute-force method. However, since there were no hints provided, I doubted that brute-forcing was the intended solution and suspected it might involve XOR operations instead. I attempted this problem but couldn’t solve it within the time limit.


social


When you run the program, it outputs encrypted values based on the plaintext input you provide. This challenge involves inputting specific plaintext values and observing their corresponding encrypted outputs.


social


This challenge requires understanding XOR operations. With XOR, the following properties hold:


plaintext ^ key = encrypt
plaintext ^ encrypt = key


Therefore, by inputting a known plaintext and obtaining its encrypted output, you can XOR the plaintext with the encrypted result to determine the key.


social


The flag is INTIGRITI{b451c_x0r_wh47?}.


Layers


social


When you extract the compressed file, you can see that it contains numerous files.


social


social


social


The combined ASCII values were entered into CyberChef for processing.


social


I couldn’t find any special values when combining the ASCII values. When I opened each file individually, I noticed that ASCII characters were present in them. Initially, I tried combining the ASCII values in numerical order of the files, but the output didn’t make sense. I continued working on the challenge but couldn’t solve it within the time limit. According to the write-up, the solution is as follows:


The ls -lart command plays a key role. I searched for the explanation of ls -lart using GPT, and here’s what it means:


Explanation of ls -lart The ls -lart command lists files in a directory with detailed information and sorts them based on specific criteria. The options serve the following purposes:

l (long listing format):

Displays detailed file information, such as: File permissions (e.g., rw-r–r–) Owner and group (e.g., crystal crystal) File size (e.g., 8 bytes) Last modification time (e.g., Aug 19 17:15) File name (e.g., 52) a (all):

Includes hidden files (those starting with .) in the listing. For example, it will show . (current directory) and .. (parent directory). r (reverse order):

Reverses the default sorting order. By default, ls -l sorts in ascending order by name or time. Adding r changes this to descending order. t (time):

Sorts files by their modification time. The most recently modified files appear first. Combined Effect of ls -lart Time-based Sorting: The t option sorts files by modification time. Reversed Order: The r option changes the order from descending (default) to ascending. Includes Hidden Files: The a option ensures that hidden files are listed as well. Detailed Output: The l option provides additional details about each file, such as permissions, ownership, and size. When applied to the challenge files, the result would look something like this:


social


The files are sorted by their creation times, and by converting the binary values from these files in this order, you can obtain the flag. According to the official write-up, the solution was achieved using the following source code.


import zipfile
import os
from datetime import datetime

ARCHIVE_NAME = "layers.zip"
EXTRACT_DIR = "files"

def binary_to_char(binary_str):
    return chr(int(binary_str, 2))

with zipfile.ZipFile(ARCHIVE_NAME, 'r') as zipf:
    for info in zipf.infolist():
        extracted_path = zipf.extract(info, EXTRACT_DIR)
        date_time = datetime(*info.date_time)
        mod_time = date_time.timestamp()
        os.utime(extracted_path, (mod_time, mod_time))

file_data = []
for file_name in os.listdir(EXTRACT_DIR):
    file_path = os.path.join(EXTRACT_DIR, file_name)
    mod_time = os.path.getmtime(file_path)
    with open(file_path, "r") as f:
        binary_data = f.read().strip()
        char = binary_to_char(binary_data)
    file_data.append((mod_time, char))

file_data.sort()
reconstructed_string = ''.join([char for _, char in file_data])

print("Reconstructed String:")
print(reconstructed_string)

for file_name in os.listdir(EXTRACT_DIR):
    os.remove(os.path.join(EXTRACT_DIR, file_name))
os.rmdir(EXTRACT_DIR)


By using it, you can obtain the corresponding string.


social


social


Then the flag becomes INTIGRITI{7h3r35_l4y3r5_70_7h15_ch4ll3n63}.



BabyFlow


BabyFlow


I ran the file.


BabyFlow


BabyFlow


The program said the password was incorrect, so I opened it in Ghidra to analyze it.


BabyFlow


The structure of the function is as follows: When the input value is SuPeRsEcUrEPaSsWoRd123, it prints correct. However, if local_C == 0, it prints: Are you sure you are admin? o.O. To get the flag, local_C needs to hold a value other than 0. To achieve this, local_C must be influenced to take a different value. Analyzing the value-checking part, it uses strncmp, which only compares the specified number of characters. It does not check the remaining characters beyond the specified limit. This allows reading additional values that follow the compared string.


BabyFlow


BabyFlow


This is the part where local_C is declared. Its location is as follows:


BabyFlow


is 0


BabyFlow


After entering the input, checking the value reveals the following:


BabyFlow


BabyFlow


Since the value becomes aaa, which is not 0, the program prints the flag. I solved it using this payload.


BabyFlow



Rigged Slot Machine 1


Rigged Slot Machine 1


This challenge seemed strange because, according to the write-up, the provided code was very similar to the one I wrote while solving it. However, I couldn’t solve it, and even using the code provided in the write-up, the flag wasn’t revealed. Starting with the source code, here’s what I found:


void main(void)

{
  time_t tVar1;
  long in_FS_OFFSET;
  uint local_20;
  int local_1c;
  __gid_t local_18;
  int local_14;
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setvbuf(stdout,(char *)0x0,2,0);
  local_18 = getegid();
  setresgid(local_18,local_18,local_18);
  tVar1 = time((time_t *)0x0);
  srand((uint)tVar1);
  setup_alarm(0xb4);
  local_20 = 100;
  puts("Welcome to the Rigged Slot Machine!");
  puts("You start with $100. Can you beat the odds?");
  do {
    while( true ) {
      while( true ) {
        local_1c = 0;
        printf("\nEnter your bet amount (up to $%d per spin): ",100);
        local_14 = __isoc99_scanf(&DAT_0010222e,&local_1c);
        if (local_14 == 1) break;
        puts("Invalid input! Please enter a numeric value.");
        clear_input();
      }
      if ((local_1c < 1) || (100 < local_1c)) break;
      if ((int)local_20 < local_1c) {
        printf("You cannot bet more than your current balance of $%d!\n",(ulong)local_20);
      }
      else {
        play(local_1c,&local_20);
        if (0x20a6e < (int)local_20) {
          payout(&local_20);
        }
      }
    }
    printf("Invalid bet amount! Please bet an amount between $1 and $%d.\n",100);
  } while( true );
}


You start with $100 and can bet up to $100 based on the user’s input. If your total money exceeds 133742, the payout function is executed.


void play(int param_1,uint *param_2)
{
  uint uVar1;
  long lVar2;
  int iVar3;
  long in_FS_OFFSET;
  int local_1c;
  
  lVar2 = *(long *)(in_FS_OFFSET + 0x28);
  iVar3 = rand();
  iVar3 = iVar3 % 100;
  if (iVar3 == 0) {
    local_1c = 100;
  }
  else if (iVar3 < 10) {
    local_1c = 5;
  }
  else if (iVar3 < 0xf) {
    local_1c = 3;
  }
  else if (iVar3 < 0x14) {
    local_1c = 2;
  }
  else if (iVar3 < 0x1e) {
    local_1c = 1;
  }
  else {
    local_1c = 0;
  }
  uVar1 = param_1 * local_1c - param_1;
  if ((int)uVar1 < 1) {
    if ((int)uVar1 < 0) {
      printf("You lost $%d.\n",(ulong)-uVar1);
    }
    else {
      puts("No win, no loss this time.");
    }
  }
  else {
    printf("You won $%d!\n",(ulong)uVar1);
  }
  *param_2 = *param_2 + uVar1;
  printf("Current Balance: $%d\n",(ulong)*param_2);
  if ((int)*param_2 < 1) {
    puts("You\'re out of money! Game over!");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  if (lVar2 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


With a certain probability, you can either win more money, lose money, or break even.


void payout(int *param_1)

{
  FILE *__stream;
  long in_FS_OFFSET;
  char local_58 [72];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  if (*param_1 < 0x20a6f) {
    puts("You can\'t withdraw money until you win the jackpot!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts(
        "Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running  this on the shell server."
        );
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  fgets(local_58,0x40,__stream);
  printf("Congratulations! You\'ve won the jackpot! Here is your flag: %s\n",local_58);
  fclose(__stream);
                    /* WARNING: Subroutine does not return */
  exit(-1);
}


When the payout function is executed, it seems to read and print the contents of flag.txt. Locally, the process runs very quickly, but when running it remotely, it seems to take over 3 minutes, preventing successful completion.


Rigged Slot Machine 1


So, looking at the flags of those who solved it…


Rigged Slot Machine 1


It seems to be displayed like this.



REV



Secure Bank


Secure Bank


Secure Bank


When running the challenge file, it prompts for a PIN. I opened it using Ghidra for analysis.


bool main(void)

{
  undefined4 local_14;
  int local_10;
  undefined4 local_c;
  
  banner();
  login_message();
  printf("Enter superadmin PIN: ");
  __isoc99_scanf(&DAT_001021ea,&local_10);
  if (local_10 == 1337) {
    local_c = generate_2fa_code(1337);
    printf("Enter your 2FA code: ");
    __isoc99_scanf(&DAT_001021ea,&local_14);
    validate_2fa_code(local_14,local_c);
  }
  else {
    puts("Access Denied! Incorrect PIN.");
  }
  return local_10 != 1337;
}


I found that the first PIN can be successfully entered as 1337, and then a second prompt for a 2fa_code appears.


uint generate_2fa_code(int param_1)

{
  int local_14;
  uint local_10;
  uint local_c;
  
  local_10 = param_1 * 0xbeef;
  local_c = local_10;
  for (local_14 = 0; local_14 < 10; local_14 = local_14 + 1) {
    local_c = obscure_key(local_c);
    local_10 = ((local_10 ^ local_c) << 5 | (local_10 ^ local_c) >> 0x1b) +
               (local_c << ((char)local_14 + (char)(local_14 / 7) * -7 & 0x1fU) ^
               local_c >> ((char)local_14 + (char)(local_14 / 5) * -5 & 0x1fU));
  }
  return local_10 & 0xffffff;
}


uint obscure_key(uint param_1)

{
  return ((param_1 ^ 0xa5a5a5a5) << 3 | (param_1 ^ 0xa5a5a5a5) >> 0x1d) * 0x1337 ^ 0x5a5a5a5a;
}


When the 2fa_code is entered, the program processes the input and determines whether it is


void validate_2fa_code(int param_1,int param_2)

{
  if (param_1 == param_2) {
    puts("Access Granted! Welcome, Superadmin!");
    printf("Here is your flag: %s\n","INTIGRITI{fake_flag}");
  }
  else {
    puts("Access Denied! Incorrect 2FA code.");
  }
  return;
}


The program outputs the flag when the correct 2fa_code is entered. There are two ways to solve this challenge, but since I originally solved it using static analysis, I decided to try the dynamic approach mentioned in the write-up. Here’s what I observed in this straightforward challenge:


Secure Bank


After setting a breakpoint at the section following the FA function call, examining the value shows that this specific value is stored:


0x568720 → 5670688


The reason for converting it to decimal is…


Secure Bank


…because the program uses %u to receive the value, and later, the generated FA value will be compared as a decimal.


Secure Bank



TriForce Recon


TriForce Recon


When you extract the compressed file,


TriForce Recon


You can see that it consists of three files. Each file has a different format:

One is an .exe file. One is an .elf file. One is a Mach-O file.


TriForce Recon


When I executed the ELF file, it seemed to require an input value. According to the write-up, the suggested approach appeared simpler, so I’ll introduce that method. (Initially, I had used static analysis to write code and figure out the value myself.) Looking at the code in IDA, it’s clear that a simple XOR operation can be performed to solve it.


TriForce Recon


You can identify the string to XOR and the key value.


TriForce Recon


TriForce Recon


Therefore, it’s a straightforward challenge where you simply input the XOR key and the string to solve it.


TriForce Recon


This challenge involved combining the flags from three programs with different formats.


The final flag is: Intigriti{s7reaMCircUm574NcEgrAV3UnpL3A54ntRE5i6N4T10nSatI5fAct1on}.




OSINT




TrackDown1,2


TrackDown1,2


TrackDown1,2


This challenge involves identifying the location where the given photo was taken.


TrackDown1,2


The above photo is from the first challenge, and if you search for it on Google, you will find…


TrackDown1,2


By searching for the building from the photo on Google, I was able to identify it. Using Google Street View and examining the photo, I noticed the table in the image had a restaurant-like feel, so I tried the name of a nearby bar or building. This resulted in the flag being validated:

INTIGRITI{Si_Lounge_Hanoi}.


TrackDown1,2


This is the second photo, and if you search for it using Google Lens, you will find…


TrackDown1,2


The search results showed a photo that appears to be taken at night, likely from the same location as the previous one. Based on this, I identified the hotel and the flag was successfully validated:

INTIGRITI{Express_by_M_Village}.




NO Comment


NO Comment


This was another challenge I couldn’t solve within the time limit. However, when you download the photo, you can see…


NO Comment


The image looks like this, and since it’s an OSINT challenge, I initially thought it was similar to a trackdown problem, so I wasted a lot of time searching on Google. I’ll follow the write-up to proceed. By using an image metadata tool like exiftool, I was able to confirm the following details:


NO Comment


By checking the comment, I found a link. Upon reviewing the write-up, it seems that the format is related to imgur.com.


NO Comment


When checking the location, I found the following encoded string:


V2hhdCBhICJsb25nX3N0cmFuZ2VfdHJpcCIgaXQncyBiZWVuIQoKaHR0cHM6Ly9wYXN0ZWJpbi5jb20vRmRjTFRxWWc=


I input this value into CyberChef, and…


NO Comment


in link


NO Comment


The site appears, and it prompts for a password. The password, which we decoded earlier as long_strange_trip, should be entered. Once entered, the following string can be obtained:


NO Comment


25213a2e18213d2628150e0b2c00130e020d024004301e5b00040b0b4a1c430a302304052304094309


After entering the password, it seems that you need to access the profile to gather further information.


NO Comment


NO Comment


You can then infer from the profile that the encrypted data was XORed.


NO Comment


INTIGRITI{instagram.com/reel/C7xYShjMcV0}




Bob L’éponge


Bob L'éponge


I couldn’t solve this challenge within the time limit either. When you enter the link, the following video appears.


Bob L'éponge


There is this strange video, and I’m not sure where or how to use it…


Bob L'éponge


When checking the profile’s playlist, I saw this list, but when I played it, there was nothing useful, so I couldn’t solve it. According to the write-up, it seems there is a tool for reading YouTube data. By using that tool to extract data from the second video, the flag appeared, which felt anticlimactic.


Bob L'éponge


INTIGRITI{t4gs_4r3_m0stly_0bs0l3t3_zMlH7RH6psw}




Private Github Repository


Private Github Repository


I almost solved it, but due to using the wrong approach, I couldn’t complete the challenge. First, if you search for the user on GitHub, you will find…


Private Github Repository


You can find one user.


Private Github Repository


You can find an email in this format. Initially, I thought it might be the key value, but upon further investigation, I realized it was actually a PK (zip file).


Private Github Repository


When you open the zip file, you can find the private key.


Private Github Repository


So, I added the SSH key and…


git clone git@github.com:bob-193/1337up.git


Through that, I was able to download the repository.


Private Github Repository


I got stuck at this part during the CTF competition. Then, upon checking the write-up, it mentioned using the following command:


ssh -T git@github.com


When I asked GPT about the command, it responded as follows:


Execution Result If the SSH key is correctly registered:


Hi <GitHub-username>! You've successfully authenticated, but GitHub does not provide shell access.


Here, refers to the GitHub account name. This message indicates that the SSH key authentication was successful.


Private Github Repository


Through this, I was able to discover Tiffany’s account name. Then, I proceeded to fetch the repository along with Bob.


Private Github Repository


After that, I tried to clone the repository, but…


Private Github Repository


It didn’t proceed as expected, possibly due to the download, but in any case, the goal was to search for the flag among the git logs.




Misc



Quick Recovery


Quick Recovery


When you open the file, you will find…


Quick Recovery


Quick Recovery


Looking at the image, you can see a fragmented QR code. It seems that the goal is to reconstruct the QR code.


from PIL import Image, ImageDraw
from itertools import permutations
import subprocess

qr_code_image = Image.open("qr_code.png")
width, height = qr_code_image.size
half_width, half_height = width // 2, height // 2

squares = {
    "1": (0, 0, half_width, half_height),
    "2": (half_width, 0, width, half_height),
    "3": (0, half_height, half_width, height),
    "4": (half_width, half_height, width, height)
}


def split_square_into_triangles(img, box):
    x0, y0, x1, y1 = box
    a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)]
    b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)]

    def crop_triangle(points):
        mask = Image.new("L", img.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.polygon(points, fill=255)
        triangle_img = Image.new("RGBA", img.size)
        triangle_img.paste(img, (0, 0), mask)
        return triangle_img.crop((x0, y0, x1, y1))

    return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)


triangle_images = {}
for key, box in squares.items():
    triangle_images[f"{key}a"], triangle_images[f"{key}b"] = split_square_into_triangles(
        qr_code_image, box)

a_order = ["1", "2", "3", "4"]  # UPDATE ME
b_order = ["1", "2", "3", "4"]  # UPDATE ME

final_positions = [
    (0, 0),
    (half_width, 0),
    (0, half_height),
    (half_width, half_height)
]

reconstructed_image = Image.new("RGBA", qr_code_image.size)

for i in range(4):
    a_triangle = triangle_images[f"{a_order[i]}a"]
    b_triangle = triangle_images[f"{b_order[i]}b"]
    combined_square = Image.new("RGBA", (half_width, half_height))
    combined_square.paste(a_triangle, (0, 0))
    combined_square.paste(b_triangle, (0, 0), b_triangle)
    reconstructed_image.paste(combined_square, final_positions[i])

reconstructed_image.save("obscured.png")
print("Reconstructed QR code saved as 'obscured.png'")


The content of the source code is as follows: The solution code is as follows: (Provide the source code or solution code here.)


from PIL import Image, ImageDraw
from itertools import permutations

qr_code_image = Image.open("obscured.png")
width, height = qr_code_image.size
half_width, half_height = width // 2, height // 2

squares = {
    "1": (0, 0, half_width, half_height),
    "2": (half_width, 0, width, half_height),
    "3": (0, half_height, half_width, height),
    "4": (half_width, half_height, width, height)
}

def split_square_into_triangles(img, box):
    x0, y0, x1, y1 = box
    a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)]
    b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)]

    def crop_triangle(points):
        mask = Image.new("L", img.size, 0)
        draw = ImageDraw.Draw(mask)
        draw.polygon(points, fill=255)
        triangle_img = Image.new("RGBA", img.size)
        triangle_img.paste(img, (0, 0), mask)
        return triangle_img.crop((x0, y0, x1, y1))

    return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)

triangle_images = {}
for key, box in squares.items():
    triangle_images[f"{key}a"], triangle_images[f"{key}b"] = split_square_into_triangles(qr_code_image, box)

# 모든 순열을 시도
for a_order in permutations(["1", "2", "3", "4"]):
    b_order = a_order[::-1]  # a_order의 역순으로 b_order 설정
    
    final_positions = [
        (0, 0),
        (half_width, 0),
        (0, half_height),
        (half_width, half_height)
    ]
    
    reconstructed_image = Image.new("RGBA", qr_code_image.size)
    
    for i in range(4):
        a_triangle = triangle_images[f"{a_order[i]}a"]
        b_triangle = triangle_images[f"{b_order[i]}b"]
        combined_square = Image.new("RGBA", (half_width, half_height))
        combined_square.paste(a_triangle, (0, 0))
        combined_square.paste(b_triangle, (0, 0), b_triangle)
        reconstructed_image.paste(combined_square, final_positions[i])
    
    # 결과 이미지 저장
    filename = f"reconstructed_{''.join(a_order)}.png"
    reconstructed_image.save(filename)
    print(f"Reconstructed QR code saved as '{filename}'")


Since it tries all permutations, all possible versions of the image are generated. By running the source code, you can see various images, and from those, the correct one was selected.


Quick Recovery


Quick Recovery




Here’s the end of the write-up, and there’s a small episode I want to share. While solving a problem, a hint popped up mentioning “v8 version,” which made me think it was related to a v8 issue. I thought I’d check the write-up later. After the competition ended, I checked and found that one of the authors of the problem was a teammate from the CVE-2024-0517 analysis I worked on this summer. His blog was referenced in the write-up, which was really surprising. It also made me realize how important it is to continue working on v8 analysis. I’m still doing v8 analysis these days, but since it doesn’t always yield immediate results like CTFs, I plan to post my findings once the analysis is fully completed. Looking forward to more challenges ahead! I should make sure to be mentioned next time too 🤭🤭


episode


episode