[re]複雜VM逆向:2020網鼎杯玄武組re baby_vm wp
可以明顯感覺到玄武組的題目難於其他三個組。這麼複雜的一道題也好意思叫baby???
題目分析
拿到題目發現是一個64位windows程序,結合題目名稱,懷疑是虛擬機逆向。
直接逆向分析:
沒有加殼,把所有函數的符號都去了,但字符串沒有混淆,可以根據“Tell Me Your Flag:”字符串找到函數的主邏輯:
向下分析可以的到flag的格式:
flag長度42,uuid格式,並且由0-9,a-f組成,也就是小寫16進制數。如:
flag{12345678-0123-5678-0123-567890123456}
接着將輸入的函數去掉flag{}和-之後轉換成16進制數,然後傳給chkflag函數:
注意中間的那些ifdebug函數出現了很多次,是用來檢測調試的,如果調試的過程中經過了這些函數,就會退出,所以可以直接patch掉,但太多,也可以直接就斷點避開這些函數。
然後查看chkflag函數:
這個函數的邏輯是:
- 先對輸入(flag去掉頭尾和-之後轉換成16進制數之後)進行md5
- 然後對flag進行某種變換(之後在分析)
- 對變換結果進行md5
- 分別取輸入(flag去掉頭尾和-之後轉換成16進制數之後)、輸入的md5、輸入的變換的md5的依次4個字節轉換成整數
- 將上述三個整數進入VM虛擬機進行計算,一共四輪
- 每一輪的計算結果(4字節)和輸入的md5的4字節一起存入一個數組,然後和一個全局變量校驗
- 返回校驗結果,如果相同則通過,輸出flag正確
關於md5是怎麼看出來的,首先點進這個函數能看到一堆跟hash有關的函數:
百度一下就知道這是windows計算哈希用的一些東西,然後根據輸入和輸出的結果,首先,我們選擇一個轉換成16進制數之後是可顯示的ascii嘛的輸入作爲輸入,如:flag{78797878-7878-7878-7878-787878787878}
轉換成16進制之後是:
然後對比md5計算的結果:
927B94B9FE8EE835AB2A400FF4219644
可以搜到:
注意上述步驟中的第六步,他VM計算的結果和我們輸入flag的md5一起存在了result的數組裏,每隔四個字節一個,那麼我們是不是就知道了flag的md5了呢?沒錯確實我們知道了flag的md5,但並沒啥用,查不出來,因爲這個md5的輸入並不是可見字符串,所以市面上的彩虹表都搜不出來。。。。
這是結果:
這裏每隔四個字節都是flag的md5的四個字節(轉換成整數後),取出來flag的md5就是:
9036d8c5ea6281976ca9321969262204
根本搜不出來(24小時都是騙人的,我當天就沒做出來,到現在寫wp都快一週了):
老老實實的來分析一下VM吧
虛擬機分析
首先將上文提到的三個數放在一個數組中,然後和opcode一起作爲輸入初始化虛擬機:
整個虛擬機的虛擬處理器和內存結構如下上面所示,每一個寄存器是4個字節,也就是32位的。初始化完成之後將虛擬機結構體返回,在內存中看一下:
值得注意的是寄存器0是用來存儲當前執行到opcode第幾個的:
然後就是對opcode進行分析,找到各個操作碼對應的操作。技巧在之前VM題目中已經提到過了,其實就是在執行一個opcode對應的函數之前和之後,記錄並對比寄存器和棧的狀態,大部分操作數都可以分析得出。可能有一些比如位移和異或不太容易看出,也可以直接進入到函數中,查看關鍵操作,比如這道題目裏的0x11操作數,輸入輸出如下:
78787878,07 -> f0f0f0f0
我最開始還以爲是每個字節分別異或0x07,結果正好是0xf0f0f0f0,但後來我發現不對,進入這個函數之後發現了IDA的ROR4宏,也就是循環右移:
所以這個0x11操作數就是循環右移x位。也就是說,簡單的操作數對比輸入輸出,複雜的操作數進去看關鍵操作指令,剩下的就是熟練度,熟練度越高,分析出虛擬機行爲越快。
將操作數導出:
D0 00 00 00 00 02 01 0D 01 02 04 01 10 00 00 00
02 01 01 00 00 00 00 02 03 01 01 00 00 00 02 02
09 01 02 0D 01 02 05 01 08 00 00 00 02 06 14 05
06 11 04 05 01 B9 79 37 9E 02 05 0C 04 05 0D 04
03 AD 00 00 00 0C 04 01 02 04 0E 01 03 07 06 19
00 00 00 D0 01 00 00 00 D0 02 00 00 00 02 02 02
03 0D 04 02 05 10 05 02 10 05 03 0D 05 0D 04 02
05 0F 05 02 0F 05 03 0D 05 0D 04 02 05 0F 05 02
0D 05 0D 04 02 05 0F 05 03 0D 05 0D 02 02 05 0F
05 03 02 06 0C 05 06 02 06 0C 05 06 02 06 0C 05
06 02 06 0C 05 06 0D 05 02 01 13 01 FF 00 08 00
00 00 01 01 03 00 00 00 02 02 08 01 02 E0 08 00
00 00 01 00 09 00 00 00 02 01 06 00 00 00 02 03
12 02 03 E0 09 00 00 00 02 04 00 00 00 00 00 00
90 36 D8 C5 CC 02 79 1F EA 62 81 97 15 3D AE 2F
6C A9 32 19 91 FE EB CE 69 26 22 04 42 AF F6 AF
然後根據操作數的順序,來一個一個的分析,遇到相同的就跳過,分析完畢之後沒出現的操作數也不用分析了。我分析的虛擬機操作數含義如下:
opcode:
00 xx 00 00 00 0x: 把棧偏移xx mov 給0x寄存器
E0 xx 00 00 00 0x: 把第x個寄存器mov到棧偏移xx處
01 xx xx xx xx:push xxxxxxxx 立即數
0d 0x : push xreg
02 0x : pop reg0x
03 AD 00 00 00 0C 04 01: call AD
04 : 返回
06 19 00 00 00 : jmp 19
07 : falg=~flag
08 0x 0y : regx+=regy
09 0x 0y : regx-=regy
0c 0x 0y : regx^=regy
0f 0x 0y : regx&=regy
10 0x 0y : regx|=regy
11 0x 0y : regx 循環右移regy
12 0x 0y : regx 循環左移regy
13 0x : 0x寄存器取反
14 0x 0y : regx 取餘 regy.
D0 0x 00 00 00 : 緩衝區 第x部分(輸入)拿到棧上
ff : 退出
比較麻煩的就是0x03 call操作數和0x04 ret操作數,這兩個操作數居然還會保存寄存器狀態和回退寄存器狀態,很強。
然後整理一下,整個虛擬機的操作,翻譯成python就是:
temp=input1
temp2=input2
temp3=input3
for i in range(16):
x=(15-i)%8
temp=((temp>>x)&0xffffffff|(temp<<(32-x))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<6)&0xffffffff|(temp>>(32-6))&0xffffffff)&0xffffffff
newtemp6=(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2)
newtemp6=newtemp6^0xffffffff
input1 input2 input3分別是輸入的flag轉換成16進制之後的4個字節、flag的md5轉換成16進制的4個字節和flag變換後md5後的4個字節。
可以看到對輸入的這三個內容進行了一些列的邏輯運算,然後得到的結果(newtemp6)就是要返回去對比的。
需要跟上文提到的result每隔4字節的四個字節對比:
解題
雖然VM部分分析完畢,但並不能直接就做出來,因爲咋一看去,這個VM是三個輸入,和一個輸出,我們無法通過一個輸出去反推三個輸入,而且三個輸入之間也沒有線性關係(一個輸入是flag,另外兩個都是md5)。
但仔細分析其實並不是,因爲輸入的另一個內容我們已經知道了,那就是flag的md5。我們不知道的其實只有兩個內容,flag變換後的md5和flag。
那麼我們這時候分析一下那個變換:
可以看到這個變換就是把輸入的16字節的flag進行逐字節的累加,然後對0x64取餘,然後根據取餘結果生成100字節的輸出。接下來就是對輸出進行md5了,那麼也就是說最後生成的內容是一個對0x64取餘的操作,那麼一共也就0x64種可能啊。所以,整理一下思路:
三個輸入,一個結果
一個結果和一個輸入已知,一個輸入有100中可能,一個輸入未知並且是要求的輸入
所以此題可解,我們爆破100種情況,然後分別反推這個輸入。並且我們知道輸入的內容的md5,進而可以從100種結果之中篩選出正確的結果。
還有一個難點就是,逆推這個邏輯關係:
newtemp6=(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2)
已知newtemp6,temp2,temp3來求temp。這裏我直接通過查表來進行復原,將所有可能打印出來,這裏我忘記打印帶取反結果的了,懶得改了,後文寫代碼的時候直接結果輸入之前取反:
a=[1,0]
for temp in a:
for temp2 in a:
for temp3 in a:
print temp2,temp3,(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2),temp
然後生成一個對應的表:
vmRetable={
"111":"1",
"100":"1",
"010":"1",
"001":"1",
"110":"0",
"101":"0",
"011":"0",
"000":"0"
}
對查到的結果拼接在一起之後就是前半段計算的temp:
for i in range(16):
x=(15-i)%8
temp=((temp>>x)&0xffffffff|(temp<<(32-x))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<6)&0xffffffff|(temp>>(32-6))&0xffffffff)&0xffffffff
然後再逆這段即可。完整代碼如下:
import hashlib
import binascii
result=[0xC5D83690, 0x1F7902CC, 0x978162EA, 0x2FAE3D15, 0x1932A96C,0xCEEBFE91, 0x04222669, 0xAFF6AF42] #結果數組
flagmd5='9036d8c5ea6281976ca9321969262204' #flag的md5
vmRetable={
"111":"1",
"100":"1",
"010":"1",
"001":"1",
"110":"0",
"101":"0",
"011":"0",
"000":"0"
}
def transform(input): #變換函數
output=""
for k in range(100):
a=0
input^=0xC3
output+=chr(input&0xff)
for j in range(8):
a^=((input&0xff)>>j)&1
input=(a|2*input)&0xff
m = hashlib.md5()
m.update(output)
output=m.hexdigest()
return output
def getinputbit(a,b,c): #查表復原最後胡的邏輯運算
return vmRetable[a+b+c]
def movere(temp): #復原邏輯運算前的循環位移
i=15
while i>=0:
x=(15-i)%8
temp=((temp>>6)&0xffffffff|(temp<<(32-6))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<x)&0xffffffff|(temp>>(32-x))&0xffffffff)&0xffffffff
i-=1
return temp
def big2small(data): #大小端轉換
return binascii.hexlify(binascii.unhexlify(data)[::-1])
for i in range(0x64): #循環生成變換內容
transoutput=transform(i)
flag=""
for i in range(4):
md5bin=bin(int(result[2*i]))[2:].rjust(32,'0')
transbin=bin(int(big2small(transoutput[8*i:8*i+8]),16))[2:].rjust(32,'0')
resultbin=bin(result[2*i+1]^0xffffffff)[2:].rjust(32,'0')
flagbin="" #先將三個輸入轉換成二進制
for j in range(32): #對二進制內容查表復原flag的位移後二進制形式
flagbin+=getinputbit(md5bin[j],transbin[j],resultbin[j])
flagtemp=movere(int(flagbin,2)) #復原flag的一部分
flagpart=""
for j in range(4): #將flag從整型轉換成字符串型(好md5)
flagpart+=chr((flagtemp>>(j*8))&0xff)
flag+=flagpart
m = hashlib.md5()
m.update(flag)
flagmd5_=m.hexdigest() #對flag求md5
flagstr="flag{"
if(flagmd5_==flagmd5): #如果和真flag的md5相同則計算正確,輸出
for i in range(len(flag)):
if(i==4 or i==6 or i==8 or i==10):
flagstr+='-'
flagstr+=hex(ord(flag[i]))[2:].rjust(2,'0')
flagstr+='}'
print flagstr
成功:
flag:
flag{9e573902-0e31-4837-a337-32a475ca007c}