A few days ago I have been writing about my solution of the levels 1(2) to 4 of the CTF which is left as an exercise at the end of Chapter 5 of Practical Binary Analysis.

In this post I will discuss my solution for the fifth level of this CTF. I am not ashamed of the fact that I totally overengineered this level and it took me way more time to solve it than necessary. Despite this, it was a very good occasion to learn and familiarize myself with the tools of the trade, gdb in particular.

Level 5

As per usual, the binary to “crack” to solve this level is called lvl5.

The file is a normal 64-bit stripped ELF.

$ file lvl5
lvl5: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=1c4f1d4d245a8e252b77c38c9c1ba936f70d8245, stripped

Running the program just prints “nothing to see here” and exits.

$ ltrace ./lvl5
__libc_start_main(0x400500, 1, 0x7ffdeec9e6c8, 0x4006f0 <unfinished ...>
puts("nothing to see here"nothing to see here) = 20
+++ exited (status 1) +++
$ strace ./lvl5
execve("./lvl5", ["./lvl5"], [/* 30 vars */]) = 0
brk(NULL)                               = 0x23ed000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=100346, ...}) = 0
mmap(NULL, 100346, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f5658bb4000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5658bb3000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f56585de000
mprotect(0x7f565879e000, 2097152, PROT_NONE) = 0
mmap(0x7f565899e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7f565899e000
mmap(0x7f56589a4000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f56589a4000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5658bb2000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5658bb1000
arch_prctl(ARCH_SET_FS, 0x7f5658bb2700) = 0
mprotect(0x7f565899e000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7f5658bcd000, 4096, PROT_READ) = 0
munmap(0x7f5658bb4000, 100346)          = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
brk(NULL)                               = 0x23ed000
brk(0x240e000)                          = 0x240e000
write(1, "nothing to see here\n", 20nothing to see here
)   = 20
exit_group(1)                           = ?
+++ exited with 1 +++

Both ltrace and strace do not give any useful information.

Strings command (or dumping .rodata section) shows the following interesting strings:

key = 0x%08x
decrypted flag = %s
nothing to see here

One we expected, but the others clearly are used somewhere else, and their semantic is pretty clear.

The next step is to disassemble the binary and look at the code. At a first glance, there seem to be no anomalies or weird things in the code.

Let’s use gdb to trace the execution of the code.

$ gdb lvl5
(gdb) info files
Symbols from "/home/binary/code/chapter5/lvl5".
Local exec file:
`/home/binary/code/chapter5/lvl5', file type elf64-x86-64.
        
Entry point: 0x400520
    0x0000000000400238 - 0x0000000000400254 is .interp
    0x0000000000400254 - 0x0000000000400274 is .note.ABI-tag
    [...]
(gdb) b *0x400520
Breakpoint 1 at 0x400520
(gdb) set pagination off
(gdb) set logging on
Copying output to gdb.txt.
(gdb) set logging redirect on
Redirecting output to gdb.txt.
(gdb) run
(gdb) display/i $pc
(gdb) while 1
 >si
 >end
nothing to see here
(gdb)

Now it’s convenient to examine the output of the trace with

$ grep "=> 0x4" gdb.txt

In this case I am grepping for the instructions with 0x4xxxxx because I have seen that the instructions in the .txt are all in the 0x400500-0x400762 range. This will spare me the noise of the instructions which happen inside library functions.

=> 0x400520:    xor    %ebp,%ebp
=> 0x400522:    mov    %rdx,%r9
=> 0x400525:    pop    %rsi
=> 0x400526:    mov    %rsp,%rdx
=> 0x400529:    and    $0xfffffffffffffff0,%rsp
=> 0x40052d:    push   %rax
=> 0x40052e:    push   %rsp
=> 0x40052f:    mov    $0x400760,%r8
=> 0x400536:    mov    $0x4006f0,%rcx
=> 0x40053d:    mov    $0x400500,%rdi
=> 0x400544:    callq  0x4004d0 <__libc_start_main@plt>
=> 0x4004d0 <__libc_start_main@plt>:    jmpq   *0x200b52(%rip)        # 0x601028
=> 0x4004d6 <__libc_start_main@plt+6>:  pushq  $0x2
=> 0x4004db <__libc_start_main@plt+11>: jmpq   0x4004a0
=> 0x4004a0:    pushq  0x200b62(%rip)        # 0x601008
=> 0x4004a6:    jmpq   *0x200b64(%rip)        # 0x601010
=> 0x4006f0:    push   %r15
=> 0x4006f2:    push   %r14
=> 0x4006f4:    mov    %edi,%r15d
=> 0x4006f7:    push   %r13
=> 0x4006f9:    push   %r12
=> 0x4006fb:    lea    0x20070e(%rip),%r12        # 0x600e10
=> 0x400702:    push   %rbp
=> 0x400703:    lea    0x20070e(%rip),%rbp        # 0x600e18
=> 0x40070a:    push   %rbx
=> 0x40070b:    mov    %rsi,%r14
=> 0x40070e:    mov    %rdx,%r13
=> 0x400711:    sub    %r12,%rbp
=> 0x400714:    sub    $0x8,%rsp
=> 0x400718:    sar    $0x3,%rbp
=> 0x40071c:    callq  0x400480
=> 0x400480:    sub    $0x8,%rsp
=> 0x400484:    mov    0x200b6d(%rip),%rax        # 0x600ff8
=> 0x40048b:    test   %rax,%rax
=> 0x40048e:    je     0x400495
=> 0x400495:    add    $0x8,%rsp
=> 0x400499:    retq   
=> 0x400721:    test   %rbp,%rbp
=> 0x400724:    je     0x400746
=> 0x400726:    xor    %ebx,%ebx
=> 0x400728:    nopl   0x0(%rax,%rax,1)
=> 0x400730:    mov    %r13,%rdx
=> 0x400733:    mov    %r14,%rsi
=> 0x400736:    mov    %r15d,%edi
=> 0x400739:    callq  *(%r12,%rbx,8)
=> 0x4005f0:    mov    $0x600e20,%edi
=> 0x4005f5:    cmpq   $0x0,(%rdi)
=> 0x4005f9:    jne    0x400600
=> 0x4005fb:    jmp    0x400590
=> 0x400590:    mov    $0x601048,%esi
=> 0x400595:    push   %rbp
=> 0x400596:    sub    $0x601048,%rsi
=> 0x40059d:    sar    $0x3,%rsi
=> 0x4005a1:    mov    %rsp,%rbp
=> 0x4005a4:    mov    %rsi,%rax
=> 0x4005a7:    shr    $0x3f,%rax
=> 0x4005ab:    add    %rax,%rsi
=> 0x4005ae:    sar    %rsi
=> 0x4005b1:    je     0x4005c8
=> 0x4005c8:    pop    %rbp
=> 0x4005c9:    retq   
=> 0x40073d:    add    $0x1,%rbx
=> 0x400741:    cmp    %rbp,%rbx
=> 0x400744:    jne    0x400730
=> 0x400746:    add    $0x8,%rsp
=> 0x40074a:    pop    %rbx
=> 0x40074b:    pop    %rbp
=> 0x40074c:    pop    %r12
=> 0x40074e:    pop    %r13
=> 0x400750:    pop    %r14
=> 0x400752:    pop    %r15
=> 0x400754:    retq   
=> 0x400500:    sub    $0x8,%rsp
=> 0x400504:    mov    $0x400797,%edi
=> 0x400509:    callq  0x4004b0 <puts@plt>
=> 0x4004b0 <puts@plt>: jmpq   *0x200b62(%rip)        # 0x601018
=> 0x4004b6 <puts@plt+6>:       pushq  $0x0
=> 0x4004bb <puts@plt+11>:      jmpq   0x4004a0
=> 0x4004a0:    pushq  0x200b62(%rip)        # 0x601008
=> 0x4004a6:    jmpq   *0x200b64(%rip)        # 0x601010
=> 0x40050e:    mov    $0x1,%eax
=> 0x400513:    add    $0x8,%rsp
=> 0x400517:    retq   
=> 0x4005d0:    cmpb   $0x0,0x200a71(%rip)        # 0x601048
=> 0x4005d7:    jne    0x4005ea
=> 0x4005d9:    push   %rbp
=> 0x4005da:    mov    %rsp,%rbp
=> 0x4005dd:    callq  0x400550
=> 0x400550:    mov    $0x60104f,%eax
=> 0x400555:    push   %rbp
=> 0x400556:    sub    $0x601048,%rax
=> 0x40055c:    cmp    $0xe,%rax
=> 0x400560:    mov    %rsp,%rbp
=> 0x400563:    jbe    0x400580
=> 0x400580:    pop    %rbp
=> 0x400581:    retq   
=> 0x4005e2:    pop    %rbp
=> 0x4005e3:    movb   $0x1,0x200a5e(%rip)        # 0x601048
=> 0x4005ea:    repz retq 
=> 0x400764:    sub    $0x8,%rsp
=> 0x400768:    add    $0x8,%rsp
=> 0x40076c:    retq

Until 0x400500 there are all init instructions, then there are basically 3 instructions of the ‘main’ function and the rest are all fini instructions.

Cross referencing the trace of the execution with the disassembled binary, it is possible to create a primitive flow chart and it is also possible to notice that a large chunk of instructions are completely unused.

This chunk ranges from 0x4005fd to 0x4006ea.

Having a look at the code, we can observe the following (commented) code.

  4005fd:       0f 1f 00                nopl   (%rax)
  400600:       b8 00 00 00 00          mov    $0x0,%eax
  400605:       48 85 c0                test   %rax,%rax
  400608:       74 f1                   je     4005fb <__printf_chk@plt+0x11b>
  40060a:       55                      push   %rbp
  40060b:       48 89 e5                mov    %rsp,%rbp
  40060e:       ff d0                   callq  *%rax
  400610:       5d                      pop    %rbp
  400611:       e9 7a ff ff ff          jmpq   400590 <__printf_chk@plt+0xb0>
  400616:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40061d:       00 00 00 
  # Save base pointer
  400620:       53                      push   %rbx
  # esi = 0x400774 (@40774 = "key = %08x\n")
  400621:       be 74 07 40 00          mov    $0x400774,%esi
  # edi = 1
  400626:       bf 01 00 00 00          mov    $0x1,%edi
  # rsp = rsp - 48bytes = 0x7fffffffe370
  40062b:       48 83 ec 30             sub    $0x30,%rsp
  # rax = fs[0x28] ; rax = 0x375ec5c0cae7ab00 (stack canary)
  40062f:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400636:       00 00 
  # rsp+40bytes = 0x375ec5c0cae7ab00
  # 0x7fffffffe398: 0x00    0xab    0xe7    0xca    0xc0    0xc5    0x5e    0x37
  400638:       48 89 44 24 28          mov    %rax,0x28(%rsp)
  # eax = 0
  40063d:       31 c0                   xor    %eax,%eax
  # rax = 0x6223331533216010
  40063f:       48 b8 10 60 21 33 15    movabs $0x6223331533216010,%rax
  400646:       33 23 62 
  # rsp+32bytes = 0 (1 byte)
  # 0x7fffffffe390: 0x00 (string terminator)
  400649:       c6 44 24 20 00          movb   $0x0,0x20(%rsp)
  # rsp = 0x6223331533216010
  #0x7fffffffe370: 0x10    0x60    0x21    0x33    0x15    0x33    0x23    0x62
  40064e:       48 89 04 24             mov    %rax,(%rsp)
  # rax = 0x6675364134766545
  400652:       48 b8 45 65 76 34 41    movabs $0x6675364134766545,%rax
  400659:       36 75 66 
  # rsp + 8 = 0x6675364134766545
  40065c:       48 89 44 24 08          mov    %rax,0x8(%rsp)
  # rax = 0x6675364134766545
  400661:       48 b8 17 67 75 64 10    movabs $0x6570331064756717,%rax
  400668:       33 70 65 
  # rsp + 16 = 0x6675364134766545
  40066b:       48 89 44 24 10          mov    %rax,0x10(%rsp)
  # rax = 0x6671671162763518
  400670:       48 b8 18 35 76 62 11    movabs $0x6671671162763518,%rax
  400677:       67 71 66 
  # rsp + 24 = 0x6671671162763518
  40067a:       48 89 44 24 18          mov    %rax,0x18(%rsp)
  # At this point the encrypted flag is all in memory, split in 4 pieces
  # ebx = *0x400540 = 0x400500
  40067f:       8b 1c 25 40 05 40 00    mov    0x400540,%ebx
  # eax = 0
  400686:       31 c0                   xor    %eax,%eax
  # edx = 0x400500
  400688:       89 da                   mov    %ebx,%edx
  # printf("key = %08x\n", 0x400500) 
  40068a:       e8 51 fe ff ff          callq  4004e0 <__printf_chk@plt>
  # rdx = rsp + 32 (end of the key) = 0x7fffffffe390
  40068f:       48 8d 54 24 20          lea    0x20(%rsp),%rdx
  # rax = rsp (beginning of the key) = 0x7fffffffe370
  400694:       48 89 e0                mov    %rsp,%rax
  # NOP
  400697:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  40069e:       00 00 
  # *eax = *eax xor ebx; 4 bytes pointed by rax are XOR'd with the key (0x400500)
  4006a0:       31 18                   xor    %ebx,(%rax)
  # eax += 4
  4006a2:       48 83 c0 04             add    $0x4,%rax
  # if rdx == rax (string is finished); done otherwise make this loop again. 
  # loop is executed 8 times
  4006a6:       48 39 d0                cmp    %rdx,%rax
  4006a9:       75 f5                   jne    4006a0 <__printf_chk@plt+0x1c0>
  # eax = 0
  4006ab:       31 c0                   xor    %eax,%eax
  # rdx = rsp = beginning of flag
  4006ad:       48 89 e2                mov    %rsp,%rdx
  # eax += 4
  4006a2:       48 83 c0 04             add    $0x4,%rax
  # if rdx == rax (string is finished); done otherwise make this loop again. 
  # loop is executed 8 times
  4006a6:       48 39 d0                cmp    %rdx,%rax
  4006a9:       75 f5                   jne    4006a0 <__printf_chk@plt+0x1c0>
  # eax = 0
  4006ab:       31 c0                   xor    %eax,%eax
  # rdx = rsp = beginning of flag
  4006ad:       48 89 e2                mov    %rsp,%rdx
  # esi = 0x400782
  # 0x400782:       "decrypted flag = %s\n"
  4006b0:       be 82 07 40 00          mov    $0x400782,%esi
  # edi = 1 (file pointer for stdout)
  4006b5:       bf 01 00 00 00          mov    $0x1,%edi
  # printf("decrypted flag = %s\n", 0x7fffffffe370)
  4006ba:       e8 21 fe ff ff          callq  4004e0 <__printf_chk@plt>
  # eax = 0
  4006bf:       31 c0                   xor    %eax,%eax
  # rcx = rsp + 40
  4006c1:       48 8b 4c 24 28          mov    0x28(%rsp),%rcx
  # Check canary
  4006c6:       64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
  4006cd:       00 00 
  # If canary got changed, throw error
  4006cf:       75 06                   jne    4006d7 <__printf_chk@plt+0x1f7>
  # else: rsp = rsp + 48 (clean the stack)
  4006d1:       48 83 c4 30             add    $0x30,%rsp
  # rbx = saved rbx
  4006d5:       5b                      pop    %rbx
  # return
  4006d6:       c3                      retq   
  4006d7:       e8 e4 fd ff ff          callq  4004c0 <__stack_chk_fail@plt>
  4006dc:       0f 1f 40 00             nopl   0x0(%rax)
  4006e0:       bf 97 07 40 00          mov    $0x400797,%edi
  4006e5:       e9 c6 fd ff ff          jmpq   4004b0 <puts@plt>
  4006ea:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)

In pseudocode the code above would be something on the lines of:

rsp = rsp-48
rsp[0-8]=0x6223331533216010
rsp[8-16]=0x6675364134766545
rsp[16-24]=0x6675364134766545
rsp[24-32]=0x6671671162763518
rsp[32]=0
ebx = *0x400540
printf("key = %08x\n", ebx)
for (i=0; i<32; i = i+8):
    rsp[i] = rsp[i] ^ ebx
printf("decrypted flag = %s\n", rsp)

In practice, first the ‘encrypted’ flag is laid in memory on the stack. Then the key is retrieved from 0x400540 address, and then the flag is decryped XOR-ing the key with the encrypted flag.

Address 0x400540 is part of what the ‘primitive flow chart’ calls entrypoint:

[1] 0000000000400520 .entry
  400520:       31 ed                   xor    %ebp,%ebp
  400522:       49 89 d1                mov    %rdx,%r9
  400525:       5e                      pop    %rsi
  400526:       48 89 e2                mov    %rsp,%rdx
  400529:       48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
  40052d:       50                      push   %rax
  40052e:       54                      push   %rsp
  40052f:       49 c7 c0 60 07 40 00    mov    $0x400760,%r8
  400536:       48 c7 c1 f0 06 40 00    mov    $0x4006f0,%rcx
  40053d:       48 c7 c7 -->00 05 40 00 <--    mov    $0x400500,%rdi
  400544:       e8 87 ff ff ff          callq  4004d0 <__libc_start_main@plt>

At that address there is the 0x(00)400500 value, which is the address of the first instruction of our ‘main’ function.

Trying to hijack the control flow with gdb and manually passing control to the “unused” function leads to a corrupted flag.

(gdb) b *0x400500
Breakpoint 1 at 0x400500
(gdb) run
Starting program: /home/binary/code/chapter5/lvl5 

Breakpoint 1, 0x0000000000400500 in ?? ()
(gdb) set $rip=0x400620
(gdb) continue
Continuing.
key = 0x00400500
decrypted flag = ea36cbE`64A35fb5d60e06bb1f
[Inferior 1 (process 9855) exited normally]

As we can see, the “key” is 0x00400500 as expected, but cleary is not the correct one.

Instead of abusing gdb to redirect the ‘main’ to our unused function, we can let the code do i directly.

We can see that instruction

  40052f:       49 c7 c0 60 07 40 00    mov    $0x400760,%r8
  400536:       48 c7 c1 f0 06 40 00    mov    $0x4006f0,%rcx
  40053d:       48 c7 c7 00 05 40 00    mov    $0x400500,%rdi
  400544:       e8 87 ff ff ff          callq  4004d0 <__libc_start_main@plt>

prepare the arguments for the main function and pass control to it. If instead of 0x00400500 we make it call 0x400620 the ‘main’ function will now be automatically our ‘secret’ routine. This will not only pass control to the code that prints the flag, but will also change the key used to decrypt, as it will now be 0x400620.

So let’s change

40053d:       48 c7 c7 00 05 40 00    mov    $0x400500,%rdi

to

40053d:       48 c7 c7 20 06 40 00    mov    $0x400620,%rdi

We can now just run the binary:

$ ./lvl5 
key = 0x00400620
decrypted flag = 0fa355cbec64a05f7a5d050e836b1a1f

The flag looks decrypted correctly this time, so we can use it to unlock the next level!

$ ./oracle 0fa355cbec64a05f7a5d050e836b1a1f
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 5 completed, unlocked lvl6         |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint

For any correction, feedback or question feel free to drop a mail to security[at]coolbyte[dot]eu.