前前言
本人的個(gè)人博客網(wǎng)址:www.QmSharing.space恶座,所有的文章都可以在里面找到柬帕,歡迎各位大佬前來(lái)參觀并留下寶貴的建議添瓷,大家一起學(xué)習(xí)一起成長(zhǎng) :-)
難度分析
本題屬于綜合題, 是有一定難度的, 但利用的漏洞我們?cè)谥暗念}目中基本都見(jiàn)到過(guò), 所以也不算特別的難. 這題唯一一個(gè)之前涉及不多的就是 canary 技術(shù), 不過(guò)它也并不是很難理解, 后面我會(huì)大概介紹一下. 這題總體是特別有意思的一道題, 難度與樂(lè)趣具備, 很值得你花點(diǎn)時(shí)間去鉆研一下.
基本檢查
- file
hash: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=89ebf47881a82f5a991199ae381f8284a46e0500, not stripped
基本的動(dòng)態(tài)鏈接的32位程序, 和之前大多數(shù)題目一樣.
- checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
嗯... 也是有 canary 和棧段不可執(zhí)行保護(hù), 看起來(lái)"好像"又不能進(jìn)行棧溢出了. 其他沒(méi)什么值得注意的地方了.
遇到的問(wèn)題
可能有些人會(huì)遇到和我一樣的問(wèn)題, 就是在直接執(zhí)行 hash 程序時(shí), 出現(xiàn)以下報(bào)錯(cuò):
./hash: error while loading shared libraries: libcrypto.so.1.0.0: cannot open shared object file: No such file or directory
這種類型的報(bào)錯(cuò)我之前見(jiàn)過(guò)的不少, 本質(zhì)原因就是程序在進(jìn)行外部鏈接時(shí)找不到所需的庫(kù), 導(dǎo)致程序無(wú)法執(zhí)行, 那么問(wèn)題就是這個(gè) libcrypto.so.1.0.0 是什么庫(kù)來(lái)的.
我大概 apt search 了一下, 就是下面這個(gè)東西:
libssl1.0.0/xenial-updates,xenial-security,now 1.0.2g-1ubuntu4.14 amd64 [installed]
Secure Sockets Layer toolkit - shared libraries
應(yīng)該是個(gè) SSL 的工具庫(kù), 但是可以看到我好像已經(jīng)裝了(不知道你們是不是), 怎么會(huì)報(bào)錯(cuò)呢? 其實(shí)問(wèn)題也很簡(jiǎn)單, 你運(yùn)行的這個(gè)程序是32位的, 而你安裝的卻是64位的動(dòng)態(tài)庫(kù), 能運(yùn)行才怪呢對(duì)吧. 因此, 你需要安裝 32 位的 ssl 庫(kù):
sudo apt install libssl1.0.0:i386
然后你再運(yùn)行程序應(yīng)該就不會(huì)有問(wèn)題了.
分析
進(jìn)入到 Rookiss 這個(gè)級(jí)別, 已經(jīng)基本不給你源碼進(jìn)行分析了, 所以是考察你的逆向能力. 這里我就稍微貼一下一些關(guān)鍵位置的逆向偽代碼, 其余的你就自己逆向后進(jìn)行分析吧.
int __cdecl main(int argc, const char **argv, const char **envp)
{
// 前面省略變量定義和緩沖區(qū)設(shè)定
puts("- Welcome to the free MD5 calculating service -");
v3 = time(0); // 用當(dāng)前時(shí)間做隨機(jī)數(shù)的種子, 這里稍微留意一下哈
srand(v3);
v6 = my_hash(); // 這個(gè)是生成驗(yàn)證碼(captcha)的邏輯
printf("Are you human? input captcha : %d\n", v6);
__isoc99_scanf("%d", &v5);
if ( v6 != v5 ) // 輸錯(cuò)驗(yàn)證直接結(jié)束
{
puts("wrong captcha!");
exit(0);
}
puts("Welcome! you are authenticated.");
puts("Encode your data with BASE64 then paste me!");
process_hash(); // 這里就是將輸入進(jìn)行 BASE64 解碼后再生成 MD5
puts("Thank you for using our service.");
system("echo `date` >> log"); // 這里就簡(jiǎn)單的將指令 date 的結(jié)果附加到 log 后面, 但我覺(jué)得這就是為了提供 system 函數(shù)給你用
return 0;
}
整個(gè)程序邏輯很簡(jiǎn)單, 就是把你的輸入轉(zhuǎn)換成 MD5, 展示后把當(dāng)前系統(tǒng)時(shí)間寫(xiě)入到 log. 繼續(xù)研究, 我當(dāng)時(shí)選擇先看 process_hash 函數(shù):
unsigned int process_hash()
{
// 省略 v0-v4 的變量定義
int v5; // [esp-20Ch] [ebp-20Ch]
unsigned int v6; // [esp-Ch] [ebp-Ch] 這個(gè)是 canary
v6 = __readgsdword(0x14u); // 寫(xiě)入 canary 值
memset(&v5, 0, 0x200u);
while ( getchar() != 10 )
;
memset(g_buf, 0, sizeof(g_buf)); // 初始化 g_buf
fgets(g_buf, 1024, stdin); // 從標(biāo)準(zhǔn)輸入讀取1024字節(jié)的值
memset(&v5, 0, 0x200u);
v1 = Base64Decode(g_buf, &v5, v0); // 將輸入進(jìn)行 BASE64 解碼, 結(jié)果寫(xiě)入到 v5, v1是解碼長(zhǎng)度
v3 = calc_md5(&v5, v1, v2); // 利用 v5 的值進(jìn)行 MD5 生成, 結(jié)果賦值給 v3
printf("MD5(data) : %s\n", v3);
free(v3);
return __readgsdword(0x14u) ^ v6; // 這里就是檢測(cè) canary 值是否被改變, 改變則證明出現(xiàn)了棧溢出
}
這里不管你用什么方法(輸入大量數(shù)據(jù)或直接分析代碼), 你會(huì)發(fā)現(xiàn) 1024 字節(jié)的 BASE64 值理論上解碼能得到 (1024*3/4=768)字節(jié)的數(shù)據(jù), 但你看清楚, 這個(gè) v5 僅僅分配了 512(0x200) 字節(jié)給它, 那么這里必然存在了棧溢出, 即 v5 就是注入點(diǎn). 但諷刺的是, 這個(gè)函數(shù)被 canary 保護(hù)著, 這和我們之前做過(guò)的題目好像有點(diǎn)不同啊. 那么我覺(jué)得, 這題應(yīng)該就是要我們進(jìn)行某種操作來(lái)繞過(guò) canary 值了, 如果你看到這里還是不知道什么是 canary 的話, 你應(yīng)該馬上停止閱讀, 并打開(kāi) canary 原理先進(jìn)行學(xué)習(xí)后, 再往下閱讀.
繞過(guò) canary 有不少, 但是鑒于本題的情況, 覆蓋 canary 或者劫持 got 的方法應(yīng)該是不太好實(shí)現(xiàn)的, 因此我覺(jué)得這很大可能是一個(gè)要泄漏 canary 值的題目, 因?yàn)槿绻阒懒?canary 值, 那么在整個(gè)程序的運(yùn)行周期內(nèi), 這個(gè)值都不會(huì)改變, 你就可以在注入時(shí)在它應(yīng)該存在的地方把它插入, 讓程序以為自己運(yùn)行并沒(méi)有出錯(cuò), 但實(shí)際我們已經(jīng)進(jìn)行了棧溢出攻擊了.
但是, 哪里有辦法讓我們泄漏出這個(gè) canary 值呢? 因?yàn)椴恢?canary 的情況下, 我們也不可能改變程序的走向, 而且 Base64 解碼和 MD5 生成邏輯中也沒(méi)什么可疑的地方(可以說(shuō)我并不想去看那里的代碼 (* ̄ω ̄)). 然后我就去翻翻 my_hash 函數(shù), 嗯... 發(fā)現(xiàn)了這樣一個(gè)有趣的地方:
int my_hash()
{
signed int i; // [esp-38h] [ebp-38h]
// 省略 v2 到 v9 的定義, 我這里的變量計(jì)數(shù)是從 v2 開(kāi)始的, 我也不知道為什么, 你們知道就好
unsigned int v10; // [esp-Ch] [ebp-Ch] 這個(gè)是 canary
v10 = __readgsdword(0x14u);
for ( i = 0; i <= 7; ++i )
*(&v2 + i) = rand(); // 為 v2-v9 分別賦值隨機(jī)數(shù)
return v6 - v8 + v9 + v10 + v4 - v5 + v3 + v7; // 返回 captcha, 嗯... v10 怎么在里面
}
可以看到, 這個(gè)返回值的計(jì)算中, 有把 canary 算進(jìn)去了. 這... 豈不是我知道了其它7個(gè)值, 我就知道 canary 是什么了嗎? 但是其它七個(gè)值是隨機(jī)數(shù)啊, 怎么可能破解呢? 我當(dāng)初在這里也卡了一段時(shí)間, 但是, 如果你還記得之前那到 random 的題目, 你就應(yīng)該知道, 隨機(jī)數(shù)生成算法生成的并不是真正的隨機(jī)數(shù), 只是根據(jù)一個(gè)種子進(jìn)行的計(jì)算出偽隨機(jī)數(shù), 如果你有同樣種子, 你就能生成一模一樣的隨機(jī)數(shù)了.
這里我感覺(jué)應(yīng)該是本題最難的地方之一了, 你除了需要知道隨機(jī)數(shù)并不是真正隨機(jī)的這個(gè)原理外, 你還要知道程序運(yùn)行時(shí)系統(tǒng)的時(shí)間戳是多少. 但這里題目當(dāng)時(shí)有給我們提示, 說(shuō)這個(gè)程序執(zhí)行的服務(wù)器和網(wǎng)站的服務(wù)器是同一個(gè). 那么這樣就好辦了, 我可以在執(zhí)行程序的時(shí)候, 立刻向 Pwnable.kr 網(wǎng)站發(fā)一個(gè)數(shù)據(jù)包, 根據(jù)結(jié)果返回的世界來(lái)判斷程序當(dāng)時(shí)執(zhí)行的時(shí)間(其實(shí)還有一個(gè)方法, 就是先通過(guò)之前一些題目的提供的賬號(hào), ssh 到服務(wù)器, 然后在執(zhí)行程序時(shí), 順便獲得 date +%s
指令的值, 這個(gè)值與程序執(zhí)行的時(shí)間基本是一致的). 提示到這里, 本題最難的地方應(yīng)該已經(jīng)解決了, 剩下的就是普通的棧溢出利用了, 如何重定向以執(zhí)行 system 函數(shù), 我相信大家都懂了, 如果大家有什么問(wèn)題, 歡迎大家在下方留言, 大家一起交流一下 : )
答案
解答步驟和 Writeup 可以在我的 Github 中找到: md5-calculator writeup