HeroCTF v3

The files can be found here

Here is a Write-Up of some RE tasks solved by supersnail.

sELF control (75 pts)

I found a program to read the flag but it seems to be broken… Could you help me patching patching two bytes to make it functional ?

Challenge : nc chall0.heroctf.fr 2048

Format : Hero{}

Author : SoEasY

The binary given is an ELF File, but IDA detects it as “IA64” ELF. Looking up at it on radare2 show that’s machine code is indeed x86_64, and the ELF header is messed up.

To fix that, we must patch the header to correct the architecture, which is the field e_machine of the ELF header, at offset 0x12, with the value EM_X86_64, defined to 0x3e.

Now, trying to run the binary triggers a segfault, so we open GDB to check what happens and we see this:

#0  0x0000555d375c90a1 in _start ()
(gdb) disass
Dump of assembler code for function _start:
   0x0000555d375c90a0 <+0>:	xor    ebp,ebp

The low byte of the start address (e_entry) at 0x18 is wrong, and we need to patch it to 0xa0, too. After those patches, the binary runs correctly, and we can connect to the server:


Position of the byte to patch in hex (example: 08) : 12
Value to put at this offset in hex (example: 17) : 3e

Position of the byte to patch in hex (example: 03) : 18
Value to put at this offset in hex (example: 04) : a0

[+] Execution : 
Hero{W0w_s0_y0u_4r3_4n_ELF_h34d3r_M4sT3r???}

JNI (90 pts)

Find the flag in this android application.

Format : Hero{}

Author : xanhacks

We are given an Android application (in APK). But, as the challenge title says, the flag verification check happens in a native library, which is called by the application through Java Native Interface.

So, we decompress the APK (which is just a zip file), and extract the “lib/” folder which contains native libs. Luckily we have a x86_64 version of the native lib, which spares us the burden of reversing ARM assembly.

In IDA, we find an interesting function, Java_fr_heroctf_jni_MainActivity_checkFlag, which seems to be the flag checker. Let’s look at its code:

.text:00000000000007AF                 mov     rdi, [rbp+s]    ; s
.text:00000000000007B3                 call    _strlen
.text:00000000000007B8                 mov     [rbp+var_18], rax
.text:00000000000007BC
.text:00000000000007BC loc_7BC:                                ; CODE XREF: Java_fr_heroctf_jni_MainActivity_checkFlag+7A↑j
.text:00000000000007BC                 cmp     [rbp+var_18], 3
.text:00000000000007C1                 jnz     loc_802
.text:00000000000007C7                 mov     rax, [rbp+var_58]
.text:00000000000007CB                 movsx   ecx, byte ptr [rax]
.text:00000000000007CE                 cmp     ecx, 36h ; '6'
.text:00000000000007D1                 jnz     loc_802
.text:00000000000007D7                 mov     rax, [rbp+var_58]
.text:00000000000007DB                 movsx   ecx, byte ptr [rax+1]
.text:00000000000007DF                 cmp     ecx, 36h ; '6'
.text:00000000000007E2                 jnz     loc_802
.text:00000000000007E8                 mov     rax, [rbp+var_58]
.text:00000000000007EC                 movsx   ecx, byte ptr [rax+2]
.text:00000000000007F0                 cmp     ecx, 36h ; '6'
.text:00000000000007F3                 jnz     loc_802
.text:00000000000007F9                 mov     [rbp+var_31], 1
.text:00000000000007FD                 jmp     loc_80B

It’s pretty obvious that the input string is compared with “666”, and we can validate the challenge with Hero{666}.

Password Keeper (100 pts)

You are mandated to pentest a new password manager application. Try to log your self to the application !

Format : Hero{user:password}

Author : SoEasY

This time we are given a Mach-O executable from OS X. As usual, we open it in IDA, which warns us it’s written in Objective-C, and asks us if we want to analyze this (yes plz).

We have an interesting function -[ViewController logMe:], which is decompiled like this with hex-rays:

this = self;
location[1] = (id)a2;
location[0] = 0LL;
objc_storeStrong(location, a3);
dico = (id)*((_QWORD *)this + 3);
user_txt = objc_msgSend(*((id *)this + 1), "text"); // fetches user name
usertxtbuf = objc_retainAutoreleasedReturnValue(user_txt);
v4 = objc_msgSend(dico, "objectForKey:", usertxtbuf);
v14 = objc_retainAutoreleasedReturnValue(v4);
v5 = objc_msgSend(*((id *)this + 2), "text"); // fetches password
v13 = objc_retainAutoreleasedReturnValue(v5);
v12 = (unsigned __int8)objc_msgSend(v14, "isEqualToString:", v13);
objc_release(v13);
objc_release(v14);
objc_release(usertxtbuf);
if ( (v12 & 1) != 0 )
{
  v6 = objc_msgSend(
         &OBJC_CLASS___UIAlertController,
         "alertControllerWithTitle:message:preferredStyle:",
         CFSTR("Good password"),
         CFSTR("\nYou found the good password ! But you didn't store anything here ¯\\_(ツ)_/¯"),
         1LL);

We can see that the contents of the user input is used as key in a dictionary, whose value is the password, which is then compared to the password input. Now, we have to find where this dictionary is constructed. There is a -[ViewController viewDidLoad] function that seems interesting:

// call the "viewDidLoad" method to the superclass
v21.super_class = (Class)&OBJC_CLASS___ViewController;
objc_msgSendSuper2(&v21, "viewDidLoad");
// Call "GetRandomNumberBetween1and10" method with the string Sw4gGP4ssw0rd
swag = objc_retain(CFSTR("Sw4gGP4ssw0rd"));
md5swag = objc_msgSend(swag, "GetRandomNumberBetween1and10");
md5 = objc_retainAutoreleasedReturnValue(md5swag);
// Concatenate Sw4gGP4ssw0rd, - and the string computed before
swag_tiret = objc_msgSend(swag, "stringByAppendingString:", CFSTR("-"));
swgt = objc_retainAutoreleasedReturnValue(swag_tiret);
v4 = objc_msgSend(swgt, "stringByAppendingString:", md5);
v17 = objc_retainAutoreleasedReturnValue(v4);
// Decode base64-encoded string
v16 = objc_retain(CFSTR("eFhENHJLX0szdjFuWHg="));
v5 = objc_alloc(&OBJC_CLASS___NSData);
v15 = objc_msgSend(v5, "initWithBase64EncodedString:options:", v16, 0LL);
v6 = objc_alloc(&OBJC_CLASS___NSString);
location = objc_msgSend(v6, "initWithData:encoding:", v15, 4LL);
v7 = objc_msgSend(&OBJC_CLASS___NSArray, "arrayWithObjects:", v17, 0LL);
v13 = objc_retainAutoreleasedReturnValue(v7);
v8 = objc_msgSend(&OBJC_CLASS___NSArray, "arrayWithObjects:", location, 0LL);
v12 = objc_retainAutoreleasedReturnValue(v8);
v9 = objc_msgSend(&OBJC_CLASS___NSDictionary, "dictionaryWithObjects:forKeys:", v13, v12);
v10 = (NSDictionary *)objc_retainAutoreleasedReturnValue(v9);
dico = v23->dico;
v23->dico = v10;

The GetRandomNumberBetween1and10 method just computes the MD5 sum of the string given to it:

data = (const char *)objc_msgSend(v2, "UTF8String");
v3 = strlen(data);
CC_MD5(data, v3, v19);
// then code to format md5 hash to hex

So, let’s decode the base64 string, so we get the username: xXD4rK_K3v1nXx. And the password is Sw4gGP4ssw0rd-d6e3698efe051ace727202e0d8bc56a1, which gives us the flag: Hero{xXD4rK_K3v1nXx:Sw4gGP4ssw0rd-d6e3698efe051ace727202e0d8bc56a1}.

RustInPeace (100 pts)

Could you break my super encryption algorithm ?

Format : Hero{}

Author : SoEasY

This challenge, as the name says, is a Linux binary written in Rust. Unfortunately, the binary is stripped, making it a bit harder to reverse, because we’ll have to reverse rust standard library along with useful code.

Let’s have a look in the main function:

push    rax
movsxd  rax, edi
lea     rdi, rust_main
mov     [rsp+8+var_8], rsi
mov     rsi, rax
mov     rdx, [rsp+8+var_8]
call    rust_init
pop     rcx
retn

It just calls Rust’s runtime init function with the real rust main function as argument (which will be called by the runtime).

The disassembly of this function is quite scary (thanks Rust compiler which generate ugly code, even worse than C++), and I executed the program inside IDA’s debugger to have a global overview.

Then, I used my best friend hex-rays which generated unreadable pseudo-C code, because analysis engine had trouble identifying the correct number of arguments used in functions. During debugging sessions, I figured out that the code used a lot of Rust strings (which can be seen as a C struct with a pointer to the buffer and a length), and a “slice” which represents a subset of the string (and contains a pointer to the buffer, a pointer to the start of the slice in the bufer and a pointer to the end).

So, after defining those structs in IDA and identifying functions and redefining prototypes, I was able to get a nice pseudocode.

The crackme first expects two arguments: a number (which must be “60”), and a path to the file which contains the flag:

argv_elt2 = (void *)get_list_elt((__int64)argv_list, 2LL);
arg2_ptr = getbuf(argv_elt2);
CreateFile(wrap_fileobj, arg2_ptr);
file_unwrap(file_obj, wrap_fileobj);
v1 = getbuf(file_obj);
strend = adder_lol(v1.s_ptr, v1.len);
s_endptr = strend.s_ptr;
s_endbuf = strend.len;
v3 = getbuf(file_obj);
v4 = adder_lol(v3.s_ptr, v3.len);
strlen_utf8(v4.s_ptr, v4.len);
correctinput = 1;
first_arg = get_list_elt((__int64)argv_list, 1LL);
if ( (string_compare(&first_arg, &str_60) & 1) != 0 )
{
correctinput = 0;
}

Then the crackme does slice stuff with the string, check if the file content begins with “FLAG=” string, and then does this:


if ( pos2 >= 47 )
  ZN4core9panicking18panic_bounds_check17h4b3d0dcda831e378E();
if ( __CFADD__(xorkey, pos2) )
  core::panicking::panic();
if ( encryptedflag[pos2] != ((xorkey + pos2) ^ chr2) )
  correctinput = 0;
  break;

The input flag is xored with xorkey + pos where “pos” is the i-th char of the user input being read. So, we can recover the flag:

flag = [163, 221, 65, 246, 49, 9, 39, 49, 43, 62, 2, 118, 44, 22, 51, 123, 57, 18, 37, 33, 96, 38, 13, 103, 54, 101, 35, 35, 7, 11, 47, 110, 40, 2, 44, 108, 22, 82, 16, 16, 85, 11, 1, 88, 87, 20, 96]
print(bytes([x ^ (0x3c + i) for i,x in enumerate(flag)][5:]))

which gives Hero{D1d_y0u_kn0w_4b0ut_Ru5t_r3v3rs1ng??}

ARMada (100 pts)

You are commissioned to test a new military-grade encryption, but apparently the developers haven’t invented much…

nc chall0.heroctf.fr 3000

Format : Hero{}

Author : SoEasY

This time, the binary is a 32-bit ARM C++ program (I couldn’t use hex-rays because I don’t have 32-bit decompilers yay, and Ghidra’s decompiler gave shitty useless code), so let’s dig in the assembly.

The binary starts by asking the user an input and computes its length:

MOV     R0, #0x40 ; '@' ; unsigned int
BL      _Znaj           ; operator new[](uint)
MOV     R3, R0
STR     R3, [R11,#user_input]
LDR     R1, =aEntrezUnInput ; "Entrez un input : "
LDR     R0, =_ZSt4cout__GLIBCXX_3.4
BL      _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::ostream &,char const*)
MOV     R3, #0xA        ; delimiter
MOV     R2, #0x41 ; 'A' ; buf_len
LDR     R1, [R11,#user_input] ; buffer
LDR     R0, =0x23148    ; this
BL      _ZNSi7getlineEPcic ; std::istream::getline(char *,int,char)

Then it allocates a vector from the user buffer, and another copy that will be given to a “yolo” function, along with a string that will contain encrypted input:

SUB     R3, R11, #-encbuf
SUB     R2, R11, #-input_copy
MOV     R1, R2          ; input
MOV     R0, R3          ; encrypted
;   try {
BL      _Z4yoloB5cxx11St6vectorIhSaIhEE ; yolo(std::vector<uchar>)

The “yolo” function seems to be a bit scary at first, because of those

LDR     R3, =0xAAAAAAAB
UMULL   R2, R3, R3, R1
MOV     R2, R3,LSR#1

which are just a division by 3, optimized by the compiler. So, this function just read the string by 3 chars, and for each block, does something like this:

unsigned long block = (*buf++) << 16 + (*buf++) << 8 + (*buf++);
out.append(encodeLookup[((block >> 18) & 0x3f)] ^ 0x42);
out.append(encodeLookup[((block >> 12) & 0x3f)] ^ 0x42);
out.append(encodeLookup[((block >> 6) & 0x3f)] ^ 0x42);
out.append(encodeLookup[((block) & 0x3f)] ^ 0x42);

Some people may have recognized that really looks like base64 encoding algorithm. And actually, if we xor the “encodeLookup” array with 0x42 we get this: CTwGhcJj+nKSqARsQ27omX0Iley91ufDbPxVY4ar5UgMNt/L3BvzkFiHZW6dOp8E, which really looks like a custom base64 ;)

Now, let’s have a look to the server given in the challenge nc chall0.heroctf.fr 3000:

=================== ARMada (by SoEasY) ===================

New cipher : XipH+jukexCE
--> Your answer : 

it looks like we’ll have to “decrypt” this string, and sending back the cleartext. Let’s reimplement the algorithm and automate answers with pwntools:

import pwn

look = bytes.fromhex("011635052A210828692C0911330310311370752D2F1A720B2E273B7B7337240620123A141B7623307717250F0C366D0E7100343829042B0A181574260D327A07")
look = bytes([x ^ 0x42 for x in look])

def dec(lol):
    buf = b""
    for i in range(0, len(lol) // 4):
        bloc = lol[4*i:4*i+4]
        i0 = look.find(bloc[0])
        i1 = look.find(bloc[1])
        i2 = look.find(bloc[2])
        i3 = look.find(bloc[3])
        if i3 != -1:
            tmp = (i3) | (i2 << 6) | (i1 << 12) | (i0 << 18)
            buf += bytes([(tmp >> 16), (tmp >> 8) & 0xff, (tmp) & 0xff])
            continue
        if i2 != -1:
            tmp = (i2 << 6) | (i1 << 12) | (i0 << 18)
            tmp >>= 8
            buf += bytes([(tmp >> 8) & 0xff, (tmp) & 0xff])
            continue
        if i1 != -1:
            tmp = i1 | (i0 << 6)
            buf += bytes([tmp >> 4])
    return buf
    


blop = {}
for i in look:
    if chr(i) not in blop:
        blop[chr(i)] = 0
    blop[chr(i)] += 1

remote = pwn.remote("chall0.heroctf.fr", 3000)
i = 0
for i in range(40):
    remote.recvuntil("New cipher : ")
    deco = remote.recvline()
    bop = dec(deco).rstrip()
    remote.recvuntil(" answer : ")
    remote.sendline(bop)
print(remote.recvall())

We finally get the flag after a short time: Hero{0h_w0W_s0_y0u_not1c3d_1t_w4s_cust0m_b64_?}

fatBin (125 pts)

You’ll never find my flag.

Format : Hero{}

Author : SoEasY

So, challenge accepted ;). It’s (again) a Mach-O binary, more precisely, according to “file” command: fatBoy: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>] [arm64:Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>].

As usual we open it on IDA, which asks us which binary we want to analyze. Because Intel assembly is cool, I choosed the x86_64 bin to reverse. The algorithm is pretty straightforward (thanks Hex-Rays again):

// key initialization
key[0] = ~KEY[0];
key[1] = KEY[1] ^ 0xAB;
key[2] = KEY[2] ^ 0xE5;
key[3] = KEY[3] ^ 0x8A;
key[4] = KEY[4] ^ 0x5C;
key[5] = KEY[5] ^ 0x44;
key[6] = KEY[6] ^ 0x20;
key[7] = KEY[7] ^ 0x47;
key[8] = 0;
inputlen = strlen(a1);
v15 = strlen(key);
v12 = v5;
v9 = inputlen;
// snipped useless code
i = 0;
v13 = 0;
__s1 = (char *)v5;
while ( i < (int)inputlen )
{
  if ( v13 == v15 )
  v13 = 0;
  v8[i++] = key[v13++];
}
v8[i] = 0;
for ( i = 0; i < (int)inputlen; ++i )
  __s1[i] = (v8[i] + __s[i]) % 26 + 'B';
v3 = __s1;
__s1[i] = 0;
v5[5] = puts(v3);

It seems to be some kind of Caesar/Vigenere cipher, but weirdly implemented, so we’ll need extra care to decrypt it, which gives us as script:


def dec(key, txt):
  res = []
  for i in range(len(txt)):
      byte = txt[i] - 0x42
      k = key[i % len(key)] % 26
      if byte < k:
          res.append(byte + 26 - k)
      else:
          res.append(byte - k)
  return res

k1 = b"BESTRONG"

buf = []
for c in dec(k1, b"KRLIJGMIWYMB[HWZPTMNZTTSCL"):
    tmp = 3*26 + c
    if tmp > ord("Z"):
        tmp = tmp - 26
    buf.append(tmp)
print(bytes(buf))

Unfortunately, this gives us: IMSORRYBUTTHISISNOTTHEFLAG, which isn’t our flag oubviously. But remember, we have an ARM64 binary to reverse inside our “fat” binary. Let’s open it on IDA. Luckily for us, it’s the same algorithm, but the key and ciphertext changed. The key decodes as FATMACHO this time, and when running the script this those arguments:

k1 = b"FATMACHO"

buf = []
for c in dec(k1, b"CUZVTWPXYGOPLLVVLJFRGRZBGU"):
    tmp = 3*26 + c
    if tmp > ord("Z"):
        tmp = tmp - 26
    buf.append(tmp)
print(bytes(buf))

We get WTFISTHISFUCKINGFILEFORMAT, and the flag is Hero{WTFISTHISFUCKINGFILEFORMAT}

WTF (350 pts)

Find the flag in this android application.

Format : Hero{}

Author : SoEasY

This time, the challenge author is lying to us and the “android” application is again a Mach-O binary (he’s probably an OSX lover).

The function starts by filling a 81-byte buffer with spaces, and then puts some numbers between 0 and 9 at “random” places in this buffer. Then the function looks likes this:


v3 = *user_input
while ( 1 )
{
  input0 = v3 - '1';
  input1 = user_input[1] - '1';
  input2 = user_input[2];
  if ( checkfunc(v11, input0, user_input[1] - '1', input2) )
    v11[9 * input0 + input1] = input2;
  user_input += 3;
  v7 = 0;
  for ( i = 8LL; i != 89; i += 9LL )
    v7 += (v10[i] != 32)
        + (v10[i + 1] != 32)
        + (v10[i + 2] != 32)
        + (v10[i + 3] != 32)
        + (v10[i + 4] != 32)
        + (v10[i + 5] != 32)
        + (v10[i + 6] != 32)
        + (v10[i + 7] != 32)
        + (v11[i] != 32);
  if ( v7 == 81 )
    break;
  v3 = *user_input;
  if ( !*user_input )
  {
    puts("Nope.");
    return 1LL;
  }
}

The checkfunc is quite ugly so I won’t paste it here, but we can deduce that the 81-byte buffer is actually a 9x9 grid, and “input0” and “input1” are (x,y) coordinates in this grid, while “input2” is the number to place in the grid. Also the functions ends if we reached the end of user input, or if there are no “space” chars left in the grid.

Those things made me to think this check function was just checking if the grid was still a valid Sudoku grid after trying to add the number. So, I wrote a little program that printed the buffer into a 9*9 grid:


v11  = "   546  9 2      7  39    49 5    7 7      2     93    56  8    1  39         8 6"

for i in range(9):
  print("|".join([x for x in v11[9*i:9*(i+1)]]))

which gave me:

 | | |5|4|6| | |9
 |2| | | | | | |7
 | |3|9| | | | |4
9| |5| | | | |7| 
7| | | | | | |2| 
 | | | |9|3| | | 
 |5|6| | |8| | | 
 |1| | |3|9| | | 
 | | | | | |8| |6

So I copied this grid into an online Sudoku solver which solved it, and copied back the completed Sudoku grid line by line, which gave the flag: Hero{178546239429381567563927184935214678741865923682793415256478391814639752397152846}