Flare-on CTF 11-writeup
Author: 堇姬Naup
雖然不是玩reverse的,不過感覺蠻好玩的,所以還是跑來打flare-on CTF了
結果是解了 5/10,其中有解出第五題大家都說很難的題目
1-frog
首先是第一題算是簽到題,是一個pygame做的小遊戲,並且有給source code
看了一下,當我到達 victory_tile.x 跟 victory_tile.y 就會噴出flag
1 | # are they on the victory tile? if so do victory |
當到達 (10、10)
,也就是flare on雕像的右下角,就可以get flag
1 | victory_tile = pygame.Vector2(10, 10) |
這邊可以看到生成圍牆,如果我把一些block刪除掉,就可以到達victory了
1 | def BuildBlocks(): |
我刪掉了
1 | Block(5, 2, False), |
移動frog到終點就get flag
2 - checksum
golang逆向
前面有加減乘除,他會random round數以及哪兩個數字並相加
1 | while ( (unsigned __int64)&v135 <= *(_QWORD *)(v1 + 16) ) |
多做幾次他就會跳出來了
以上都是煙霧彈
當通過上面的部分後,會讓你輸入checksum,之後進入main_a
1 | len = v85; |
重點關注main_a(以下我有簡單改過變數名稱了)
1 | __int64 __golang main_a( |
checksum輸進去就是xor而已
他會去跟aTrueeeppfilepi[v19 + 3060]
開始做xor
1 | .rdata:00000000004C8030 db '2bf16FlareOn2024bad verb ',27h,'%0123456789_/dev/stdout/dev/stder' |
那位置就是 FlareOn2024
1 | from pwn import xor |
將他輸入到checksum(REAL_FLAREON_FLAG.JPG)後圖片會生成到你的電腦上
他有寫他輸出到哪裡不過我看不是很懂,所以直接用
把圖片從電腦裡抓出來
3 - aray
1 | import "hash" |
這個題目是個yara rule file,有點亂,我先寫個腳本dump出我的條件,並排序
1 | file_size |
先來解釋一下每種不同函數的意思
函數 | 意思 |
---|---|
uint8(x) | 一個bytes,x是索引 |
uint32(x) | 四個bytes,代表[x:x+4] |
hash.md5(x,y) | 代表[x:x+y]md5 |
hash.crc32(x,y) | 代表[x:x+y]crc32 |
hash.sha256(x,y) | 代表[x:x+y]sha256 |
只要我們找出一個長度是85字串能符合上面條件的就是flag
一開始還原就會遇到問題了,uint8給的條件太少,單看uint8基本上所有printable char都會符合
所以先來做hash還原,只有兩個bytes直接爆破
md5腳本
1 | import hashlib |
sha256腳本
1 | import hashlib |
crc32腳本
1 | import zlib |
可以爆破出
1 | md5 |
再來是uint32條件都是通過簡單加減法或xor運算,不是一個range,所以可以直接還原
1 | uint32(52) ^ 425706662 == 1495724241 |
做到這裡就會發現其實flag應該只有中間的一小段是
上述條件還原後只會差一兩個flag沒有拿到
我是直接推理,把字元填進去就拿到flag了
1RuleADayK33p$Malw4r3Aw4y@flare-on.com
原本想寫z3但遇到一些問題,後來手逆花了一小時多就解玩了
4 - Meme Maker 3000
一個網頁,是JS混淆題目
其中最重要的就是這段了
1 | function a0k() { |
當他滿足一些條件後就會alert出flag給你,那就來修復a、b、c、d分別是甚麼吧
a比較複雜,先看b、c、d
1 | b = a0l['textCo' + 'ntent'] |
b是a0l
1 | a0l = document[a0p(0xcd59) + a0p(0x11e77) + 'Id']('captio' + 'n1') |
a0p(0xcd59) + a0p(0x11e77) + 'Id'
是getElementById
所以整句其實就是
1 | document['getElementById']('caption1') |
也就是
1 | <div id="caption1" class="caption" contenteditable="" style="top: 10%; left: 75%;">FLARE On</div> |
textContent就是它包含的字
所以是比較上述東西有沒有等於(t=a0p)
1 | a0c[a0p(0x12d23) + 'f'](b) == 0xe |
也就是
1 | a0c['indexOf'](b) == 0xe |
a0c[0xe]是FLARE On
再來是c
1 | a0m = document[a0p(0xcd59) + a0p(0x11e77) + 'Id'](a0p(0x14b7b) + 'n2') |
修成
1 | document['getElementById']('caption2') |
也就是
1 | <div id="caption2" class="caption" contenteditable="" style="top: 55%; left: 75%;">Reading someone else's code</div> |
1 | a0m[a0p(0x10f5a) + a0p(0x125ab)] |
就是textContent -> 包含的字
1 | a0c[a0p(0x12d23) + 'f'](c) == a0c[a0p(0x1544d)] - 0x1 |
他的意思是
1 | a0c['indexOf'](c) == a0c[length] - 0x1 |
最後是 d,就不贅述,就是
1 | a0c[a0p(0x12d23) + 'f'](d) == 0x16 |
這樣就得到了
1 | b = 'FLARE On', |
回到a
1 | a = a0g[t(0xa1d)]['split']('/')[a0p(0x7e8)](); |
他需要滿足
1 | a !== Object[a0p(0x59c5)](a0e)[0x5] |
也就是
1 | a !== Object['key'](a0e)[0x5] |
這邊往上追可以看到這個
1 | const a0g = document[a0p(0xcd59) + a0p(0x11e77) + 'Id'](a0p(0x1b97) + a0p(0xf101)) |
還原完後變這樣
1 | const a0g = document.getElementById('meme-image'), |
既然他是抓a0g.alt,那就去追a0g相關條件
1 | a0g[s(0xa1d)] = a0j[s(0x3b9f)], |
追a0j.value是,也就是他圖片的名稱
1 | <select id="meme-template"> |
而Object.keys(a0e)[5]
是boy_friend0.jpg
這樣就還原了
1 | a = 'boy_friend0.jpg' |
最後直接改javascript的a b c d 值成我們要的就get flag
後來發現線上反混淆器可以很好的直接還原大多數東西
https://deobfuscate.relative.im/
5-sshd
這題非常有趣,首先我他提敘說因為crash,要我們找出crash的點,先gdb分析他的coredump(/home/naup/Desktop/flareon5/var/lib/systemd/coredump/sshd.core.93794.0.0.11.1725917676
)
用這樣來debug coredump
1 | gdb /home/naup/Desktop/flareon5/usr/sbin/sshd sshd.core.93794.0.0.11.1725917676 |
一開始看到他死在call null byt
用bt回朔,發現他是在liblzma.so.5,crash點在0x00007f4a18c8f88f
這區段被delete了,應該就是binary載入liblzma.so.5的區段
0x00007f4a18c8f88f-0x7f4a18c86000 = offset(0x988f)
先把liblzma抓下來分析吧
1 | .text:0000000000009887 mov rsi, rbp |
這邊應該就是crash的點,他call rax時,call到了Null pointer
這邊是一個decrypt相關的函數
1 | __int64 __fastcall sub_9820(unsigned int a1, _DWORD *a2, __int64 a3, __int64 a4, unsigned int a5) |
中間有一整塊很像寫shellcode的區塊
他需要滿足一些條件
他先mmap了一塊記憶體v12
之後copy記憶體大小的東西(unk_23960)到v12中並返回pointer
這東西看起來像是encrypt過的shellcode
之後進到了sub_9520(那猜測這東西就是decrypt shellcode的function)
之後會去call v13
也就是shellcode
這邊我們回去看assembly,嘗試從coredump紀錄的memory抓出可以用的東西
這裡是開頭,當他去call getuid後回傳值存到rax裡面,之後看eax是不是0,rbp是不是0xC5407A48,是的話就會跳進去shellcode的部分
1 | .text:0000000000009820 ; __unwind { |
這裡在做的事基本上跟上述說明的一樣
1 | .text:00000000000098C0 loc_98C0: ; CODE XREF: sub_9820+4E↑j |
之後進入,也就是透過dlsym解析成對應的function,之後call rax crash了
1 | .text:0000000000009870 loc_9870: ; CODE XREF: sub_9820+150↓j |
看到這裡應該是有點複雜了,所以我打算用動態的方式來去看,不過這是一個libc,所以我們寫一個簡單的libc program來去引用該libc
用dlopen開啟libc,之後去抓一個libc中隨便的function丟進dlsym來去抓到正確的libc位置
之後減去他的offset拿到libcbase,最後在找到decrypt函數的function跳進去就好了
1 | void* dlsym(void* handle,const char* symbol) |
該函數在<dlfcn.h>文件中。
handle是由dlopen打開動態鏈接庫後返回的指針,symbol就是要求獲取的函數的名稱,函數 返回值是void*,指向函數的地址,供調用使用
1 |
|
這樣編譯debug腳本
1 | gcc -o libczma_debugger exp.c -ldl -g |
我們先來整理一下每個函數
函數名稱 | 用途 | rdi | rsi | rdx | rcx |
---|---|---|---|---|---|
sub_9820 | libc function | a1 | a2 | a3 | a4 |
sub_93F0 | 推測是抓到key | v14 | a2+1 | a2+9 | 0LL |
mmap | 映射記憶體,存放shellcode | 0LL | dword_32360 | 7 | 34 |
memcpy | copy shellcode到mmap memory上 | v12 | &unk_23960 | dword_32360 | 無 |
sub_9520 | 解密shellcode | v14 | v13 | dword_32360 | 無 |
有了上面函數狀況,我們就gdb來看看吧,由於不知道這些參數代表甚麼,所以先跟進去看sub_9820
1 | ► 0x7ffff7f79820 endbr64 |
首先關注的是call function的rsi
他會傳入 sub_93F0 的 a2+1、a2+9,大概就可以猜測出這是一個offset,也就定位到key的位置,而rsi會copy 到 rbp(0x7ffff7f79836)
順帶一提,這邊想測試進到shellcode的部分,所以用這兩行跳過判斷
1 | set $rax = 0 |
之後就會進到這塊,原本的rbp位置+0x4被推入了r10,r10移入rsi,call sub_93F0,所以rbp上那塊布局應該是前4byte是 0xc5407a48,之後開始是shellcode,Dword(4 byte) * 8 = 32是key長度
1 | 0x7ffff7f798c0 lea r11, [rbp + 0x24] R11 => 0x55555555b0d4 ◂— 0 |
當我們回去看coredump時,發現rbp仍指向key位置,所以就把他dump下來(我多dump了一點)
這邊修改一下Debug腳本(前四byte+key)
1 | char key[] = "\x48\x7a\x40\xc5" |
另外我發現main第一個參數應該是shellcode長度,這邊填入0xf96
1 | dword_32360 dd 0F96h |
不過還是怪怪的,最後我發現key長度應該要是48 byte(看coredump記憶體上面的東西,直到48byte後才是正確的,所以修正一下)
1 |
|
shellcode部分看起來正常了,這樣我們就成功還原shellcode,只差看他在做甚麼了
用
1 | dump binary memory output.bin 0x7ffff7ffa000 0x7ffff7ffaf90 |
dump完後加上elf的header
1 | section .text |
把他包成elf後就可以用IDA分析了
1 | nasm -f elf64 -o shellcode.o shellcode.asm |
這邊是main function的部分,他會開個一socket,之後去call client的chacha20(這裡我逆過了)
1 | __int64 __fastcall MAIN() |
這裡追進去後他把key跟nonce傳進去了ENC_CHACHA_F
1 | __int64 __fastcall client_chacha20(__int64 a1, __int64 a2, const void *key, const void *nonce, __int64 a5) |
這裡有像是chacha20的key
(0x20)跟nonce
(0xC)
1 | _DWORD *__fastcall ENC_CHACHA_F(__int64 a1, __int64 a2, const void *key, const void *nonce) |
往上追可以找到offset,但是當你嘗試回去抓rbp或rsp offset會發現爛掉了,原因是這裡的rsp rbp已經經過了很多東西,是call rax
crash掉時的rsp rbp,所以要回推當時情況
這邊先知道兩個概念
call function後rsp rbp變化
原本都指在同一個位置1
2*RBP 0x7fffffffdc60 ◂— 1
*RSP 0x7fffffffdc60 ◂— 1當我們call function時,進到function會rsp-0x8(原因是stack儲存了返回位置,所以rsp會下推)
1
2*RBP 0x7fffffffdc60 ◂— 1
*RSP 0x7fffffffdc58 —▸ 0x5555555551f5 (main+18) ◂— mov eax, 0push 後 rsp rbp 變化
像是push rbp
、push rax
等操作後
rsp會-0x8
那就開始回推吧
這裡首先先call r8 進到backdoor
之後執行完shellcode後跳出
一路往下執行直到碰到call rax
這時候的rax已經call下去了,所以rsp已經被減掉0x8了
再來回頭看
call 進來 rsp(-0x8)
1 | public _start |
這裡的rsp 是 coredump中rsp + 0x8
push rbp
RSP = rsp - 0x8
rbp變成RSP (rsp - 0x8 + 0x8)
他去call MAIN (rsp-0x8)
這裡先 push 五次 (rsp-0x8 * 5)
rsp再給一次給rbp
有點亂,總之:
現在要減掉2次0x8(call)跟減掉6次0x8(push)並加回1次0x8(call rax回推)
1 | ; __int64 __fastcall MAIN() |
之後把東西放到stack
所以可以知道
我們要的 rbp(IDA上放到stack的rbp + 0x…) 就會是
rbp = rsp(coredump) + 0x8 - 0x8 * 8
就會是確切的nonce、key等位置了
從coredump中rsp位置是 0x7ffcc6601e98 去算
0x7ffcc6601e98 - 0x38 = 0x7ffcc6601e60(rbp)
item | address | IDA |
---|---|---|
key | 0x7ffcc6601e60 - 0x1278 | char ubuf[32]; // [rsp+410h] [rbp-1278h] BYREF |
nonce | 0x7ffcc6601e60 - 0x1258 | char v12[16]; // [rsp+430h] [rbp-1258h] BYREF |
filename | 0x7ffcc6601e60 - 0x1248 | char filename[256]; // [rsp+440h] [rbp-1248h] BYREF |
encrypt Flag | 0x7ffcc6601e60 - 0x1148 | char buf[4224]; // [rsp+540h] [rbp-1148h] BYREF |
抓出來
1 | KEY: |
最後就是寫腳本解密了,但是網路上chacha20的腳本都沒啥用
認真看了一下他跟原本的chacha20有所不同(其實就是他的constant的k變成K),所以自己根據逆向出來的東西手刻
chacha20實作參考這個
https://github.com/marcizhu/ChaCha20/
改這個庫裡面的 #define CHACHA20_CONSTANT "expand 32-byte K"
1 |
|