CSCAMP CTF 2014 - ELF2 (Reversing 100)
We are given the following binary to reverse: elf2. (It’s a zipped file)
Just as an overview, let’s run strace on the binary:
amon@Evanna$ strace ./run
execve("./run", ["./run"], [/* 68 vars */]) = 0
brk(0) = 0x2021000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f839daea000
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=145062, ...}) = 0
mmap(NULL, 145062, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f839dac6000
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\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1845024, ...}) = 0
mmap(NULL, 3953344, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f839d504000
mprotect(0x7f839d6bf000, 2097152, PROT_NONE) = 0
mmap(0x7f839d8bf000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bb000) = 0x7f839d8bf000
mmap(0x7f839d8c5000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f839d8c5000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f839dac5000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f839dac3000
arch_prctl(ARCH_SET_FS, 0x7f839dac3740) = 0
mprotect(0x7f839d8bf000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f839daec000, 4096, PROT_READ) = 0
munmap(0x7f839dac6000, 145062) = 0
ptrace(PTRACE_TRACEME, 0, 0x1, 0) = -1 EPERM (Operation not permitted)
exit_group(0) = ?
+++ exited with 0 +++
Something immediately, jumps at you:
ptrace(PTRACE_TRACEME, 0, 0x1, 0) = -1 EPERM (Operation not permitted)
This means that there is some anti-debugging trickery going on :)
Let’s identify where the ptrace call is in Hopper.
Looking at the xrefs.
So it’s actually running the call in the initiatialisation section.
Now let’s patch it so that we can debug dynamically.
We export it as run.fixed. Let’s test with strace again:
amon@Evanna$ strace ./run.fixed
execve("./run.fixed", ["./run.fixed"], [/* 68 vars */]) = 0
brk(0) = 0x1998000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2c93de8000
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=145062, ...}) = 0
mmap(NULL, 145062, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2c93dc4000
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\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1845024, ...}) = 0
mmap(NULL, 3953344, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2c93802000
mprotect(0x7f2c939bd000, 2097152, PROT_NONE) = 0
mmap(0x7f2c93bbd000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1bb000) = 0x7f2c93bbd000
mmap(0x7f2c93bc3000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2c93bc3000
close(3) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2c93dc3000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2c93dc1000
arch_prctl(ARCH_SET_FS, 0x7f2c93dc1740) = 0
mprotect(0x7f2c93bbd000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ) = 0
mprotect(0x7f2c93dea000, 4096, PROT_READ) = 0
munmap(0x7f2c93dc4000, 145062) = 0
exit_group(0) = ?
+++ exited with 0 +++
Great! We can see system calls as it runs now. Let’s move onto the static analysis of the binary. Since it’s a really simple ELF binary, we can cheat and use Hopper’s decompilation feature :D
The main function checks if the argc is larger than 1 (i.e. there are arguments
to the binary) else it exits. If so, it runs xxyy(xx(argv[1]))
then exits.
(var_10 + 0x8
is the second pointer in the argv array since it’s a 64 bit
binary).
The xx function simply converts the argument to integer.
The xxyy function takes an integer and does the following:
def xxyy(number):
if number < 0:
number += 3
if number >> 0x2 == 0x10:
result = xxyyzz()
return result
So we simply have to pass it the value of 0x10 « 0x2 which is 0x40 (64). xxyyzz is probably our flag function.
This function opens a file .flag.txt and writes our flag to it. Unfortunately it seems like the decompilation isn’t exactly right since its missing an additional argument to the string pointer for the format string so we can check the disassembly.
And we have our flag.
To verify, we can test it by running:
amon@Evanna$ strace ./run.fixed 64
execve("./run.fixed", ["./run.fixed", "64"], [/* 68 vars */]) = 0
<snip>
brk(0x25f1000) = 0x25f1000
open(".flag.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd712d44000
write(3, "Flag: xxyyzz", 12) = 12
close(3) = 0
munmap(0x7fd712d44000, 4096) = 0
exit_group(0) = ?
+++ exited with 0 +++
amon@Evanna$ cat .flag.txt
Flag: xxyyzz
Flag: xxyyzz
Note: there are missing images when porting over the blog from an old version.
Leave a Comment