ENEI CTF 2015 - Writeups

Introduction and Reverse Engineering

Posted by André on August 16, 2015


ENEI is a portuguese meeting for students of Computer Science. This year, it took place in my city, in the University of Coimbra.

NEI invited me and Leandro Braguês, from DOGNÆDIS, to organize a CTF for the event. We were the evil minds behind all the challenges whose solutions will be presented below.

Our server is not available anymore, but you can download the final scoreboard here.

Reverse Engineering

1 - A new friend

Can you find the secret inside this file?
Download: sorry

Let's execute it:

$ ./sorry

We need to inspect the program to see what it does besides printing this message. Disassembling the main function seems to be a good start.

40071c   mov    rbp, rsp
40071f   sub    rsp, 0x30
400723   mov    eax, 0x0
400728   call   check
40072d   test   eax, eax
40072f   je     0x40082b

If check returns true, the program jumps to 0x40082b:

40082b   mov    edi, 0x4008f7   ; "I'M S0RRY"
400830   call   [email protected]

We just need to bypass the check procedure, avoiding that jump. However, we can also try to make it return false.

4006f1   mov     rbp, rsp
4006f4   mov     ecx, 0x0       ; argument "data" for method [email protected]
4006f9   mov     edx, 0x0       ; argument "addr" for method [email protected]
4006fe   mov     esi, 0x0       ; argument "pid" for method [email protected]
400703   mov     edi, 0x0       ; argument "request" for method [email protected]
400708   mov     eax, 0x0
40070d   call    [email protected]
400712   shr     rax, 0x3f
400716   movzx   eax, al
400719   pop     rbp
40071a   ret

This function clearly inspects the presence of a debugger, since it calls ptrace. Let's run it using gdb!

$ gdb sorry
gdb-peda$ r
Starting program: sorry
Good job, GDB is now your friend :) 
Flag: e37b4a153e3151a817aced31bc4bf9a2448d4483

2 - Xiene is a thief!

We are investigating a new case: Xiene got a new sweatshirt for free!
As far as we know, he used an old executable from a webstore, that generates a payment hash trusted by the seller.
Can you help us find the hash he used?

Purchase details:

Product ID: 2893
Xiene's size: L
Date: 15 Mar 2015
Time: 18h:25m:49s
Download: sweat

The program produces the following output when we provide the input specified:

$ ./sweat 
Product ID: 2893
Size (S, M, L): L
Price: 0
Invalid ID.

Once again, we need to disassemble the program to see what's going on.

4019f9   mov     edi, 0x40643d                 ; "Product ID: "
4019fe   mov     eax, 0x0
401a03   call    [email protected]
401a19   call    [email protected]
401a45   call    [email protected]
401a85   call    [email protected]
401a8a   mov     qword [ss:rbp+var_40], rax
401a8e   lea     rax, qword [ss:rbp+var_40]
401a92   mov     rdi, rax                      ; argument "clock"
401a95   call    [email protected]
401ae1   cmp     dword [ss:rbp+var_14], 0x0
401ae5   jne     0x401af1
401ae7   mov     edi, 0x406463                 ; "360 NO SCOPE"
401aec   call    [email protected]
401af1   mov     rdi, qword [ss:rbp+var_38]
401af5   movsx   esi, byte [ss:rbp+var_1]      ; argument #2 for method _Z8calcHashicdiiiiii
401af9   mov     eax, dword [ss:rbp+var_2C]
401afc   mov     r9d, dword [ss:rbp+var_20]    ; argument #7 for method _Z8calcHashicdiiiiii
401b2a   call    _Z8calcHashicdiiiiii          ; calcHash(int, char, double, int, int, int, int, int, int)

The program calls calcHash using the input values. "360 NO SCOPE" was a fake flag and there was a lot of people submitting it. Not so easy! We need to understand what the calcHash procedure does.

401527   mov       rbp, rsp
40152a   push      rbx
40152b   sub       rsp, 0x1a8
401532   mov       dword [ss:rbp+var_184], edi
401538   mov       eax, esi
40153a   movsd     xmmword [ss:rbp+var_190], xmm0
401542   mov       dword [ss:rbp+var_194], edx
401548   mov       dword [ss:rbp+var_198], ecx
40154e   mov       dword [ss:rbp+var_19C], r8d
401555   mov       dword [ss:rbp+var_1A0], r9d
40155c   mov       byte [ss:rbp+var_188], al
401562   cmp       byte [ss:rbp+var_188], 0x53
401569   je        0x40158c
40156b   cmp       byte [ss:rbp+var_188], 0x4d
401572   je        0x40158c
401574   cmp       byte [ss:rbp+var_188], 0x4c
40157b   je        0x40158c
40157d   mov       edi, 0x4063f8                 ; "Invalid size.
401582   call      [email protected]
401587   jmp       0x4019e8
40158c   cmp       dword [ss:rbp+var_184], 0x0
401593   js        0x4015a1
401595   cmp       dword [ss:rbp+var_184], 0xa7a
40159f   jle       0x4015b0
4015a1   mov       edi, 0x406406                 ; "Invalid ID."
4015a6   call      [email protected]
4015ab   jmp       0x4019e8
4015b0   movsd     xmm0, qword [ds:0x406478]
4015b8   ucomisd   xmm0, xmmword [ss:rbp+var_190]
4015c0   jae       0x4015d4
4015c2   movsd     xmm0, qword [ss:rbp+var_190]
4015ca   ucomisd   xmm0, xmmword [ds:0x406480]
4015d2   jbe       0x4015e3
4015d4   mov       edi, 0x406412                 ; "Invalid price."
4015d9   call      [email protected]
4015de   jmp       0x4019e8
4015f0   mov       esi, 0x406421                 ; argument "format" for method [email protected]
4015f5   mov       rdi, rax                      ; argument "str" for method [email protected]
4015f8   mov       eax, 0x0
4015fd   call      [email protected]
40167f   mov       esi, 0x40642b                 ; "%d/%d/%d-%d:%d:%d", argument "format" for method [email protected]
401684   mov       rdi, rax                      ; argument "str" for method [email protected]
401687   mov       eax, 0x0
40168c   call      [email protected]
4016c9   mov       rdi, rax                      ; argument #1 for method _Z4sha1RKSs
4016cc   call      _Z4sha1RKSs                   ; sha1(std::string const&)
4019ec   leave
4019ed   ret

This function checks if the arguments are valid. Then, it builds a string containing these values and uses SHA-1 algorithm to calculate the hash.
The valid sizes are: "S" (0x53), "M" (0x4d) and "L" (0x4c).
The product ID is valid if 0 (0x0) < ID < 2682 (0xa7a).
The price is stored as double. Let's inspect the addresses used in this verification in order to understand it.

406478   dq   0x4014000000000000
406480   dq   0x409f3c0000000000

Thus, the price is valid if 5.0 (0x4014000000000000) < price <= 1999.0 (0x409f3c0000000000).

These verifications must be bypassed in order to obtain the desired hash. However, it's very to modify the stack. I'm going to set a breakpoint just after the last sprintf and before SHA-1 calls begin, and then search for the input values in memory.

$ gdb sweat
gdb-peda$ b *0x401699
gdb-peda$ r
Starting program: sweat
Product ID: 2001
Size (S, M, L): L
Price: 8
0000| 0x7fffffffdf00 --> 0x7fffffffdfd0 --> 0x7fffffffdfea --> 0x4560000000010000 
0008| 0x7fffffffdf08 --> 0x4020000000000000 ('')
0016| 0x7fffffffdf10 --> 0x7df0000000f 
0024| 0x7fffffffdf18 --> 0x1100000008 
0032| 0x7fffffffdf20 --> 0x4020000000000000 ('')
0040| 0x7fffffffdf28 --> 0x7d1ffffdf4c 
0048| 0x7fffffffdf30 ("16/8/2015-15:19:49")
0056| 0x7fffffffdf38 ("5-15:19:49")

Breakpoint 1, 0x0000000000401699 in calcHash(int, char, double, int, int, int, int, int, int) ()
gdb-peda$ find 8.00
Searching for '8.00' in: None ranges
Found 2 results, display max 2 items:
[stack] : 0x7fffffffd510 ("8.000000")
[stack] : 0x7fffffffdf70 ("8.000000")
gdb-peda$ find 2001
Searching for '2001' in: None ranges
Found 2 results, display max 2 items:
 [heap] : 0x618c78 --> 0x31303032 ('2001')
[stack] : 0x7fffffffdfb0 --> 0x31303032 ('2001')

The strings are stored in the heap and the stack. We just need to inject the correct values and let the program do the rest to get the flag!

gdb-peda$ set {char [5]} 0x618c78 = "2893"
gdb-peda$ set {char [5]} 0x7fffffffdfb0 = "2893"
gdb-peda$ set {char [9]} 0x7fffffffd510 = "0.000000"
gdb-peda$ set {char [9]} 0x7fffffffdf70 = "0.000000"
gdb-peda$ set {char [19]} 0x7fffffffdf30 = "15/3/2015-18:25:49"
gdb-peda$ c

Note: You can also modify int and double variables instead of strings.

3 - GooDBye, my friend!

This executable is protected with a password. Can you crack it?
Download: goodbye

The program asks for a password. Our goal is to find the correct one.

$ ./goodbye 
Please enter your password: 12345678  
Sorry, that didn't work! Please try again.

Main function disassembled:

400e68   mov    rbp, rsp
400e6b   mov    edi, 0x493e0                        ; argument "useconds" for method [email protected]
400e70   mov    eax, 0x0
400e75   call   [email protected]
400e7a   mov    edi, 0x400f9b                       ; "Please enter your password: ", argument "format" for method [email protected]
400e7f   mov    eax, 0x0
400e84   call   [email protected]
400e89   mov    rax, qword [ds:[email protected]@GLIBC_2.2.5]  ; [email protected]@GLIBC_2.2.5
400e90   mov    rdx, rax                            ; argument "stream" for method [email protected]
400e93   mov    esi, 0x64                           ; argument "size" for method [email protected]
400e98   mov    edi, 0x601540                       ; password, argument "str" for method [email protected]
400e9d   call   [email protected]
400ea2   mov    edi, 0x601540                       ; password, argument #1 for method check
400ea7   call   check
400eac   mov    eax, 0x0
400eb1   pop    rbp
400eb2   ret

The main procedure calls check, a function that processes the password provided. Let's see what it does:

400d1d   mov      dword [ss:rbp+var_14], 0x5a
400d24   mov      dword [ss:rbp+var_18], 0x7e
400d4d   mov      rdi, rax                          ; argument "s" for method [email protected]
400d50   call     [email protected]
400d55   mov      rdx, rax
400d58   mov      eax, dword [ss:rbp+var_18]
400d5b   cdqe
400d5d   cmp      rdx, rax
400d60   jne      0x400e54
400d66   mov      ebx, 0x0
400d6b   jmp      0x400dda

400d6d   cmp      ebx, 0x7                          ; LOOP BEGIN
400d70   jg       0x400da3
400d72   movsxd   rdx, ebx
400d75   mov      rax, qword [ss:rbp+var_28]
400d79   add      rax, rdx
400d7c   movzx    eax, byte [ds:rax]
400d7f   movsx    eax, al
400d82   sub      eax, dword [ss:rbp+var_14]
400d85   mov      r12d, eax
400d88   mov      edi, ebx                          ; argument #1 for method f1
400d8a   call     f1
400d8f   cmp      r12d, eax
400d92   je       0x400dd7
400d94   mov      eax, 0x0
400d99   call     error
400d9e   jmp      0x400e5e
400da3   movsxd   rdx, ebx
400da6   mov      rax, qword [ss:rbp+var_28]
400daa   add      rax, rdx
400dad   movzx    eax, byte [ds:rax]
400db0   movsx    eax, al
400db3   sub      eax, dword [ss:rbp+var_14]
400db6   mov      r12d, eax
400db9   lea      eax, qword [ds:rbx+0xfffffffffffffff8]
400dbc   mov      edi, eax                          ; argument #1 for method f2
400dbe   call     f2
400dc3   cmp      r12d, eax
400dc6   je       0x400dd7
400dc8   mov      eax, 0x0
400dcd   call     error
400dd2   jmp      0x400e5e
400dd7   add      ebx, 0x1
400ddd   mov      rax, qword [ss:rbp+var_28]
400de1   mov      rdi, rax                          ; argument "s" for method [email protected]
400de4   call     [email protected]
400de9   sub      rax, 0x1
400ded   cmp      r12, rax
400df0   jb       0x400d6d                          ; END OF LOOP
400df6   mov      edi, 0x400f52                     ; "Well done! Your flag is: "
400e52   jmp      0x400e5e
400e54   mov      edi, 0x400f70                     ; "Sorry, that didn't work! Please try again."
400e59   call     [email protected]
400e65   pop        rbp
400e66   ret

This procedure is a bit confusing, but let's try to translate it to pseudocode.

check(string s):
   a = 0x5a = 90;
   b = 0x7e = 126;
   (instructions involving variable b)
   if (len(s) == b):
      for i in (0, len(s)):
         tmp = s[i] - a;
         if (i < 8):
            if (tmp != f1(i)):
               print("Nice try!");
               if (tmp != f2(i-8)):
               print("Nice try!");
      print("Well done! Your flag is:")
      print("Sorry, that didn't work! Please try again.")

Guessing the correct length of the password seems a good idea. After a few attemps, the "Nice try!" message appears when the length is 16. Now, we just need to reverse f1 and f2. We can extract these functions from the binary. However, I'm going to use gdb to call the f1 and f2 using the proper arguments. But wait... If you try to run the program using gdb, you will get a segmentation fault.

$ gdb goodbye
gdb-peda$ r
Stopped reason: SIGSEGV
0x00007ffff776c5f8 in raise () from /usr/lib/libc.so.6

This happens because this executable has an anti-debugging protection. Fortunately, it can be bypassed using various techniques. I'm going to override ptrace, since it usually does the job.

$ echo "long ptrace(int a, int b, int c){return 0;}" > override.c
$ gcc override.c -shared -fPIC -o override.so
$ gdb goodbye
gdb-peda$ set environment LD_PRELOAD ./override.so
gdb-peda$ b main
gdb-peda$ r
Breakpoint 1, 0x0000000000400e6b in main ()
gdb-peda$ call f1(0)
$3 = 0x1d = 29
gdb-peda$ call f1(1)
$4 = 0xf = 15

29+90 = 119 = 'w', 15+90 = 105 = 'i', and so on.
The final password is "winter_is_coming".

$ ./goodbye 
Please enter your password: winter_is_coming
Well done! Your flag is: 77696e7465725f69735f636f6d696e67