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.