Practical Binary Analysis - Chapter 5 - CTF walkthrough level 6
During the last month I have been busy reading this awesome book by Dennis Andriesse, which quickly became one of my favourite reads on the subject: it does an excellent job on covering the foundation about Linux’s binary analysis and also going above and beyond by providing the reader with all the necessary techniques to be highly proficient and effective when dealing with such alchemical matter.
Chapter 5 has the purpose of illustrating all these different tools of the trade which culminates with an intriguing CTF, whose goal is to challenge the reader to put in practice all the skills&tricks gained up to this point.
The CTF comprises 8 (or even more?) different levels and I have just cleared level 6. Sifting through search engines, I could not find any other walk-through about this level, hence the reason behind this post.
So, let the party begin by starting with the previous level’s hint:
$ ./oracle 0fa355cbec64a05f7a5d050e836b1a1f -h
Find out what I expect, then trace me for a hint
It seems to suggest that before running ltrace or strace we should find some other parameter.
Let’s first start with no parameters, so we can have a baseline. The binary is printing the first twenty-five prime numbers:
$ ./lvl6
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
However, when it comes to the first tracing tool, strace is giving nothing valuable:
$ strace ./lvl6
execve("./lvl6", ["./lvl6"], [/* 32 vars */]) = 0
brk(NULL) = 0x15bc000
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=98537, ...}) = 0
mmap(NULL, 98537, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc3b5fb3000
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) = 0x7fc3b5fb2000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc3b59dd000
mprotect(0x7fc3b5b9d000, 2097152, PROT_NONE) = 0
mmap(0x7fc3b5d9d000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fc3b5d9d000
mmap(0x7fc3b5da3000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc3b5da3000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc3b5fb1000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc3b5fb0000
arch_prctl(ARCH_SET_FS, 0x7fc3b5fb1700) = 0
mprotect(0x7fc3b5d9d000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7fc3b5fcc000, 4096, PROT_READ) = 0
munmap(0x7fc3b5fb3000, 98537) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
brk(NULL) = 0x15bc000
brk(0x15dd000) = 0x15dd000
write(1, "2 3 5 7 11 13 17 19 23 29 31 37 "..., 722 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
) = 72
exit_group(0) = ?
+++ exited with 0 +++
While ltrace just restates the obvious by listing each prime’s printfs:
binary@binary-VirtualBox:~/code/chapter5/level6$ ltrace ./lvl6
__libc_start_main(0x4005f0, 1, 0x7ffd949f3ef8, 0x400890 <unfinished ...>
__printf_chk(1, 0x400947, 2, 100) = 2
__printf_chk(1, 0x400947, 3, 0x7ffffffe) = 2
__printf_chk(1, 0x400947, 5, 0x7ffffffe) = 2
__printf_chk(1, 0x400947, 7, 0x7ffffffe) = 2
...[snip]...
__printf_chk(1, 0x400947, 97, 0x7ffffffd) = 3
putchar(10, 3, 0, 0x7ffffffd2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
) = 10
+++ exited (status 0) +++
Aiming for our first part of the hint, the command line argument, we can try giving string a run to see if anything fancy pops up.
$ strings lvl6
/lib64/ld-linux-x86-64.so.2
libc.so.6
__printf_chk
__stack_chk_fail
putchar
__sprintf_chk
strcmp
__libc_start_main
setenv
__gmon_start__
GLIBC_2.3.4
GLIBC_2.4
GLIBC_2.2.5
UH-`
AWAVA
AUATL
[]A\A]A^A_
DEBUG: argv[1] = %s
get_data_addr
0x%jx
DATA_ADDR
;*3$"
GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
.shstrtab
[..snip...]
.comment
Apart from symbols and section names, there are a few interesting strings that stand out. The ‘debug’ string is clearly telling us the expected argv[1] format, our first argument, which is indeed a string. Following along, the next value is ‘get_data_addr’ which might be a viable candidate. Let’s give it a go.
$ ltrace ./lvl6 get_data_addr
__libc_start_main(0x4005f0, 2, 0x7ffd56d2bba8, 0x400890 <unfinished ...>
strcmp("get_data_addr", "get_data_addr") = 0
__sprintf_chk(0x7ffd56d2b6a0, 1, 1024, 0x400937) = 8
setenv("DATA_ADDR", "0x4006c1", 1) = 0
__printf_chk(1, 0x400947, 2, 100) = 2
__printf_chk(1, 0x400947, 3, 0x7ffffffe)
Now we have started unveiling the mask! strcmp is expecting ‘get_data_addr’ as a right match, so it looks like we have completed our first milestone by finding the correct argument.
A longer way to accomplish the same goal (which I took) is via GDB: since we know that the binary is expecting a parameter, we can also try it out with the debugger and see what happens.
$gdb --args lvl6 testparameter
gdb-peda$ info file
...
Entry point: 0x400790
We have found the entry point of the stripped binary and we can dump all the instructions starting from the address, to verify when main gets called.
gdb-peda$ x/32i 0x400790
[...snip...]
0x4007ad: mov rdi,0x4005f0
0x4007b4: call 0x4005a0 <__libc_start_main@plt>
The address of main passed to libc_start_main is 0x4005f0, so we are able to dump the instructions starting from that point.
gdb-peda$ x/32i 0x4005f0
0x4005f0: push rbp
0x4005f1: push rbx
0x4005f2: mov ebp,edi
0x4005f4: mov rbx,rsi
0x4005f7: sub rsp,0x5a8
0x4005fe: mov edx,DWORD PTR [rip+0x200a60] # 0x601064
0x400604: mov rax,QWORD PTR fs:0x28
0x40060d: mov QWORD PTR [rsp+0x598],rax
0x400615: xor eax,eax
0x400617: test edx,edx
0x400619: jne 0x400732
0x40061f: cmp ebp,0x1
0x400622: mov QWORD PTR [rip+0x200a3b],0x4005b0 # 0x601068
0x40062d: jle 0x400645
0x40062f: mov rdi,QWORD PTR [rbx+0x8]
0x400633: mov esi,0x400929
0x400638: call 0x4005b0 <strcmp@plt>
Hey! Do you spot the call to ‘strcmp’ here? Let’s see if we can dump the matching string at runtime. In this code snippet, the expected string is being stored into the esi/rsi register.
gdb-peda$ b *0x400638
gdb-peda$ run
Breakpoint 1, 0x0000000000400638 in ?? ()
gdb-peda$ x/s $rsi
0x400929: "get_data_addr"
Gotcha. So now it’s time to review the second suggestion from the oracle: ’…then trace me for a hint’. If we pay close attention to the latest ltrace we ran (the one provided with the correct first argument) we can spot a new function being called.
setenv("DATA_ADDR", "0x4006c1", 1)
This function is supposed to set an environment variable called “DATA_ADDR” with a value of “0x4006c1”. That value seems to reside in our binary address space, so it is wise to have a look at it with the debugger. After some trying, it is clear that GDB is not willing to execute any instruction near that address, and if we dump them, they clearly look like gibberish:
gdb-peda$ x/16i 0x4006c1
0x4006c1: cs sub esi,eax
0x4006c4: rex.WX lsl rsp,WORD PTR [rsi+0x7f302aee]
0x4006cc: in al,dx
0x4006cd: enter 0xffc3,0x42
0x4006d1: lea rbp,[rsp+0x190]
0x4006d9: jmp 0x4006e9
0x4006db: nop DWORD PTR [rax+rax*1+0x0]
0x4006e0: add rbx,0x4
Until we get to the well-known ‘lea’, all the previous instructions are not common ones, so this region of memory does not contain code, but data instead. If we calculate the distance between the start and the end of the garbage instructions, we will know the amount of data we have to dump.
gdb-peda$ print/d (0x4006d1-0x4006c1)
$1 = 16
We have 16 bytes, let’s dump them
gdb-peda$ x/16bx 0x4006c1
0x4006c1: 0x2e 0x29 0xc6 0x4a 0x0f 0x03 0xa6 0xee
0x4006c9: 0x2a 0x30 0x7f 0xec 0xc8 0xc3 0xff 0x42
Coincidentally, these 16 bytes also are formed by 32 characters, which matches the expected flag length. We could give it a try and see if our intrepidness was worth the journey.
binary@binary-VirtualBox:~/code/chapter5$ ./oracle 2e29c64a0f03a6ee2a307fecc8c3ff42
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
| Level 6 completed, unlocked lvl7 |
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
Run oracle with -h to show a hint
level completed! :)