TBTL CTF 2024

https://ctftime.org/event/2324



CHALL’S SOLVED

CategoryChallenge
CryptoFence Building
IntroSanity.py
RevFlagcheck

Crypto

Fence Building

Overview

Description: I’ve recently got into woodworking and have build a beautiful fence just like this one.

Now I’m working on a flag, but it turns out all garbled for some reason…

T0n40g5BG03cmk0D1hr}T{dFe_3g_3buL_5_n0

In this crypto challenge, only a ciphertext is given which needs to be decoded and the hint is also given.

This classic challenge ciphertext is text that is encoded using Rail Fence Encoding (also called zigzag cipher) is a transposition cipher. The message is written in a zigzag pattern on an imaginary fence, thus its name. It is not strong as the number of keys is small enough to brute force them.

We can just decode using CyberChef, here’s the recipe Railfence Ciphertext Decode

or here’s the solver implementation using python and bruteforcing the key

solver.py
 1import re
 2
 3ciphertext = 'T0n40g5BG03cmk0D1hr}T{dFe_3g_3buL_5_n0'
 4
 5def rail_fence_decrypt(ciphertext, rails):
 6    matrix = [['' for _ in range(len(ciphertext))] for _ in range(rails)]
 7    idx = 0
 8    for rail in range(rails):
 9        p = (rail != 0 and rail != (rails - 1))
10        r = rail
11        while r < len(ciphertext):
12            matrix[rail][r] = '*'
13            if p:
14                r += 2 * (rails - rail - 1)
15            else:
16                r += 2 * rail
17            p = not p
18    r = 0
19    for i in range(rails):
20        for j in range(len(ciphertext)):
21            if matrix[i][j] == '*':
22                matrix[i][j] = ciphertext[r]
23                r += 1
24    plaintext = ''
25    for i in range(len(ciphertext)):
26        for j in range(rails):
27            if matrix[j][i] != '':
28                plaintext += matrix[j][i]
29    return plaintext
30
31def find_string(plaintext):
32    pattern = r'TBTL{.*}'
33    match = re.search(pattern, plaintext)
34    if match:
35        return match.group()
36    return None
37
38for rails in range(2, len(ciphertext)):
39    plaintext = rail_fence_decrypt(ciphertext, rails)
40    found_string = find_string(plaintext)
41    if found_string:
42        print(f'Key: {rails} \nFlag: {found_string}')
43        break
$ python3 solver.py
Key: 4
Flag: TBTL{G00d_F3nce5_m4k3_g00D_n31ghb0ur5}
FLAG

TBTL{G00d_F3nce5_m4k3_g00D_n31ghb0ur5}



Intro

Sanity.py

Overview

"}FTC_3h7_y0jn3-!d3s54P_kc3hC_y71n4S{LTBT"[::-1]

This Sanity check is a pretty simple, we can just reverse the bytes [::-1] of it.

$ python3
>>> flag = "}FTC_3h7_y0jn3-!d3s54P_kc3hC_y71n4S{LTBT"
>>> print(flag[::-1])
TBTL{S4n17y_Ch3ck_P45s3d!-3nj0y_7h3_CTF}
>>>
FLAG

TBTL{S4n17y_Ch3ck_P45s3d!-3nj0y_7h3_CTF}



Rev

Flagcheck

Overview

Description: This one is simple — you provide the flag, and the binary tells you if its correct.

chall

Move on to rev category, this challenge is like Flagchecker of ELF 64 Binary

Binary Information
$ file flagcheck
flagcheck: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=68da80acf7353d56f047fa725e2506428b7c6864, for GNU/Linux 3.2.0, not stripped

Let’s try to executing the binary file

$ chmod +x chall
$ ./chall
Let me check your flag: idk ma boahh
Nope...

Here’s the decompiled of main function,

decompiled main
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ebx
  int seed; // [rsp+4h] [rbp-6Ch]
  int i; // [rsp+8h] [rbp-68h]
  int j; // [rsp+Ch] [rbp-64h]
  char s[72]; // [rsp+10h] [rbp-60h] BYREF
  unsigned __int64 v9; // [rsp+58h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  printf("Let me check your flag: ");
  __isoc99_scanf("%s", s);
  if ( strlen(s) != 63 )
    no();
  seed = 1;
  for ( i = 0; i < strlen(s); ++i )
    seed *= s[i];
  srand(seed);
  for ( j = 0; j < strlen(s); ++j )
  {
    v3 = s[j];
    if ( ((rand() % 256) ^ v3) != target[j] )
      no();
  }
  puts("Correct!");
  return 0;
}

From the decompiled code looks like an xor program and I’m quite curious about what the target array will do.

.rodata:0000000000002020 ; _DWORD target[63]
.rodata:0000000000002020 _ZL6target      dd 33h, 84h, 3Dh, 3Fh, 2Ah, 93h, 7Bh, 82h, 1Ah, 0ACh, 8Eh
.rodata:0000000000002020                                         ; DATA XREF: main+E4↑o
.rodata:0000000000002020                 dd 0F4h, 0B1h, 0CBh, 8Dh, 21h, 0Eh, 0B7h, 67h, 96h, 2Ch
.rodata:0000000000002020                 dd 81h, 0D3h, 0BCh, 29h, 6Ch, 4Bh, 0Dh, 0, 0EDh, 0FDh
.rodata:0000000000002020                 dd 0EEh, 56h, 40h, 52h, 0D5h, 5, 6Dh, 90h, 3Eh, 7Ah, 1Bh
.rodata:0000000000002020                 dd 69h, 23h, 1Fh, 0B6h, 1Dh, 0BCh, 98h, 0D1h, 0A6h, 83h
.rodata:0000000000002020                 dd 0E9h, 0EBh, 13h, 21h, 3Dh, 0F8h, 2Bh, 79h, 53h, 4Fh
.rodata:0000000000002020                 dd 0A1h

Aha nice! the value of target array has discovered from .rodata section.

So here’s the conclusion, The program will reads the flag, checks its length, calculates a seed for the random number generator from the flag, and then checks each character of the flag against a value in the target array.

The target array is XORed with the result of rand() % 0x100. Since the seed for rand() is determined by the flag, we can predict the sequence of random numbers. However, that the seed is calculated by multiplying all the ASCII values of the characters in the flag, which makes it difficult to reverse.

Here’s the high-level approach to solve this:

  • Brute force the seed: Since the seed is a product of ASCII values, and ASCII printable characters range from 32 to 126, the maximum value for the seed is 126^63. However, this is not feasible to brute force. But, considering that the flag format is TBTL{.*}, we can reduce the search space. The seed is also an uint (32-bit), so the maximum value is 2^32-1. This is feasible to brute force.
  • Predict the random sequence: Once the seed are correct, we can generate the same sequence of random numbers as the program.
  • Recover the flag: Now that the same rand() sequence, we can manage to recover the flag. For each character in the flag, calculate target[i] ^ (rand() % 0x100) to get the original character.

Here’s the solver,

solver.py
 1import ctypes
 2
 3# The target array from the .rodata
 4target = [0x33, 0x84, 0x3D, 0x3F, 0x2A, 0x93, 0x7B, 0x82, 0x1A, 0xAC, 0x8E, 0xF4, 0xB1, 0xCB, 0x8D, 0x21, 0xE, 0xB7, 0x67, 0x96, 0x2C, 0x81, 0xD3, 0xBC, 0x29, 0x6C, 0x4B, 0xD, 0x0, 0xED, 0xFD, 0xEE, 0x56, 0x40, 0x52, 0xD5, 0x5, 0x6D, 0x90, 0x3E, 0x7A, 0x1B, 0x69, 0x23, 0x1F, 0xB6, 0x1D, 0xBC, 0x98, 0xD1, 0xA6, 0x83, 0xE9, 0xEB, 0x13, 0x21, 0x3D, 0xF8, 0x2B, 0x79, 0x53, 0x4F, 0xA1]
 5
 6# The libc rand() function
 7libc = ctypes.CDLL('libc.so.6')
 8rand = libc.rand
 9rand.argtypes = []
10rand.restype = ctypes.c_int
11
12# The libc srand() function
13srand = libc.srand
14srand.argtypes = [ctypes.c_uint]
15srand.restype = None
16
17# Brute force the seed
18for seed in range(2**32):
19    srand(seed)
20    flag = []
21    for i in range(len(target)):
22        r = rand() % 0x100
23        flag.append(chr(target[i] ^ r))
24    if flag[0] == 'T' and flag[1] == 'B' and flag[2] == 'T' and flag[3] == 'L':
25        print(''.join(flag))
26        break
$ python3 solver.py
TBTL{l1n3a4_C0ngru3n7i41_6en3r4t0r_b453d_Fl4G_Ch3ckEr_G03z_8rr}
FLAG

TBTL{l1n3a4_C0ngru3n7i41_6en3r4t0r_b453d_Fl4G_Ch3ckEr_G03z_8rr}