大神论坛 利用活跃变量分析来去掉vmp的大部分垃圾指令
聲明:為了把技術分享給更多的人,在 大神論壇 上發表了,這里也發一份
環境和工具
Windows7 7601
Ida7.5
Python3.8
qiling框架
簡介
首先簡單介紹一下數據流分析和活躍變量分析?;钴S變量分析屬于數據流分析的一種,編譯器的許多優化都依賴于數據流分析。龍書的簡介截圖如下
活躍變量分析的用途有刪除無用賦值和為基本塊分配寄存器。vmp 中的垃圾指令大部分都是些無用賦值,我們可以利用活躍變量分析來刪除這些垃圾指令
如上圖所示。如果把test指令看作是對eflags寄存器的賦值,其中的test指令就屬于無用賦值,因為eflags寄存器在當前指令被賦值之后沒有被使用,在下一條指令又被重新賦值。0064D721處對ebp的賦值在0064D748之前也沒有被使用,也是一個無用賦值。
記錄基本塊
此次活躍變量分析僅局限于合并直接跳轉后的基本塊內,不做全局的數據流分析,可以不用添加前驅和后繼,相當于做局部優化。本次使用的樣本是vmp3.5版本加的殼,只點了虛擬化,沒有做其它的處理,這個樣本的源碼在vmprotect3.5安裝目錄下Example\Code Markers\MSVC。以去除vmp1段中的垃圾代碼為例,也就是加殼后程序入口點處的那部分虛擬機代碼。由于vmp3.5展開了dispatch結構,并且利用間接跳轉干擾靜態分析工具的控制流重建。比如vmp使用大量的jmp register和push register; ret;等指令。為了構建控制流,我使用qiling框架來模擬執行來記錄這類指令的跳轉目標。qiling實現了一個pe加載器且模擬了部分系統api。經過驗證,是可以運行到原程序的入口點。有兩個api需要自己添加模擬,GetProcessAffinityMask和SetThreadAffinityMask。模擬的代碼如下:
然后利用qiling的hook_code的回調函數來跟蹤指令的走向,記錄所需的跳轉信息。回調函數的部分代碼如下:
def traceCode(ql, address, size, user_data=None):#print("trace address:0x%08x" % address)#trace = [address,size]#if(trace not in g_traceAddrList):#g_traceAddrList.append(trace)if(0x004012F5 == address): #真正的入口點print("execute to original entrypoint! address:0x%08x" % 0x004012F5)ql.emu_stop()#push reg;retif(1 == size and ql.mem.read(address,size)[0] == 0xc3):target = ql.unpack32(ql.mem.read(ql.reg.esp, 4))if(None != g_RetAddrDict.get(address)):g_RetAddrDict[address].add(target)else:g_RetAddrDict[address] = {target}md = ql.disassembermd.detail = TruebInsn = ql.mem.read(address,size)insn = list(md.disasm(bInsn, address))[0] #trace jmp Regif(capstone.x86_const.X86_INS_JMP == insn.id and capstone.x86_const.X86_OP_REG == insn.operands[0].type):target = ql.reg.read(insn.operands[0].reg)if(None != g_jmpRegAddrDict.get(address)):g_jmpRegAddrDict[address].add(target) else:g_jmpRegAddrDict[address] = {target}由于qiling框架模擬執行的有點慢,所以模擬到入口點結束后就把獲取到的信息通過json序列化保存到了文件中。代碼和文件我都會上傳,這里就不用一一展開了。獲取到信息后,就要記錄所有的基本塊。大體思路是以入口點的代碼為一個作為一個新的基本塊的開始,然后不斷的把后續指令加進去,直到碰到一個無條件跳轉、條件跳轉指令或其目的地址的指令為止。具體實現是從入口點開始掃描每一條指令,把push imm; call imm;當作直接跳轉,不分析直接跳轉后面的指令,繼續從直接跳轉的目的地址開始分析。需要注意的是push register;ret;和jmp register需要看成是含有多個分支的跳轉。然后利用一個隊列來保存待分析的基本塊首地址,代碼實現如下:
def GetVmp1BasicBlock(): EntryPoint = 0x400000 + 0x0037E533 insn = ida_ua.insn_t()qInsnAddr = queue.Queue() #保留待分析的跳轉分支起始地址qInsnAddr.put(EntryPoint)while(not qInsnAddr.empty()):ea = start_ea = qInsnAddr.get() if(IsRedundant(ea)): #已經加入基本塊,不用在分析continue#print("trace start_ea:0x%08x" % start_ea)#分析start_ea開始的基本塊while(ea != 0x006FDE0C): #0x006FDE0B為虛擬機出口 #print("\tea:0x%08x" % ea) InsnLen = ida_ua.decode_insn(insn, ea)if(0 == InsnLen):print("decode_insn(ea=0x%08x) failed!" % ea)return 0 if(insn.itype in g_callInsnList and insn.ops[0].type in g_immOprand):prevInsn = ida_ua.insn_t()prevAddr = ea - 5 #push immediate;占用5個字節prevLen = ida_ua.decode_insn(prevInsn, prevAddr)#調用decode_prev_insn可能會失敗if(0 == prevLen):print("decode_insn(0x%08x) failed!" % prevAddr)return 0#push xxx;call xxx;把call看出直接跳轉if(ida_allins.NN_push == prevInsn.itype and ida_ua.o_imm == prevInsn.ops[0].type):end_ea = ea + insn.sizevbb = VMPBasicBlock(start_ea, end_ea, insn.ea)g_vmp1BlockList.append(vbb)AdjustBlockByJccTarget(insn.ops[0].addr)qInsnAddr.put(insn.ops[0].addr)#print("\tqInsnAddr.put(0x%08x)" % insn.ops[0].addr)else:#暫不分析其它類型的callea = ea + insn.sizecontinuebreak elif(insn.itype in g_jccInsnList):end_ea = ea + insn.sizevbb = VMPBasicBlock(start_ea, end_ea, insn.ea)g_vmp1BlockList.append(vbb)qInsnAddr.put(end_ea) #jcc需要分析其后面的指令#print("\tqInsnAddr.put(0x%08x)" % end_ea)jccTarget = insn.ops[0].addrAdjustBlockByJccTarget(jccTarget)qInsnAddr.put(jccTarget)#print("\tqInsnAddr.put(0x%08x)" % jccTarget) breakelif(insn.itype in g_jmpInsnList):end_ea = ea + insn.sizevbb = VMPBasicBlock(start_ea, end_ea, insn.ea)g_vmp1BlockList.append(vbb)if(insn.ops[0].type in g_immOprand): #不分析jmp immediate后面的指令JmpTarget = insn.ops[0].addrAdjustBlockByJccTarget(JmpTarget)qInsnAddr.put(JmpTarget)#print("\tqInsnAddr.put(0x%08x)" % JmpTarget) elif(ida_ua.o_reg == insn.ops[0].type): #jmp reg;for JmpTarget in g_jmpRegDict[ea]:AdjustBlockByJccTarget(JmpTarget)qInsnAddr.put(JmpTarget)#print("\tqInsnAddr.put(0x%08x)" % JmpTarget) breakelif(ida_allins.NN_retn == insn.itype):#主要是push reg;ret;指令,還有少部分跳入vmp1的ret end_ea = ea + insn.sizevbb = VMPBasicBlock(start_ea, end_ea, insn.ea)g_vmp1BlockList.append(vbb)'''prevInsn = ida_ua.insn_t()prevInsnLen = ida_ua.decode_insn(prevInsn, ea - 1)#push占用一個字節if(0 == prevInsnLen):breakif(ida_allins.NN_push != prevInsn.itype or ida_ua.o_reg != prevInsn.ops[0].type):break'''if(None == g_RetAddrDict.get(insn.ea)): #會遍歷到沒有模擬執行過的ret指令,可能由條件分支指令造成的print("warning:cannot find ret target! address:0x%08x" % insn.ea)breakfor JmpTarget in g_RetAddrDict[insn.ea]: #g_RetAddrDict的key為ret的地址#if(0x004012F5 == JmpTarget):#0x004012F5為原入口點# continueif(JmpTarget < 0x63b000 or JmpTarget > 0x820000):#vmp1 segment boundprint("ret from 0x%08x to 0x%08x" % (ea, JmpTarget))continueAdjustBlockByJccTarget(JmpTarget)qInsnAddr.put(JmpTarget)#print("\tqInsnAddr.put(0x%08x)" % JmpTarget) breakelse:ea = ea + insn.size'''L1: xxx;...L2: xxx;...L3 jcc;當有一條先跳轉到L2的指令時,后面又有一條跳轉到L1的指令,會出現L1到L2的塊且包含L2到L3的塊'''if(IsRedundant(ea)): #檢測到另一個塊的起始地址vbb = VMPBasicBlock(start_ea, ea, insn.ea)g_vmp1BlockList.append(vbb)breakreturn 1執行完后發現有7000多個這樣的塊,所以需要合并一下那些直接跳轉的塊,也方便之后做活躍變量分析。合并和添加前驅和后繼的代碼就不展示了,具體參考源碼中的AddVMPBasicBlockPrevsAndSuccs和TryMergeBasicBlock兩個函數。
活躍變量分析
合并基本塊后就可以做活躍變量分析了,在和合并后基本塊內做活躍變量分析可以把每一條指令看作是一個結點,寄存器看作一個變量,然后利用使用和定值信息計算進入結點和結點后的活躍信息。這里給出《現代編譯原理-C語言描述》第10章活躍分析的一個例子,方便大家理解使用、定值和活躍性。
活躍性計算的方法如下:
按照上述方法計算后,會得到每一個結點的入口活躍信息和出口活躍信息。考慮到合并后的基本塊有1700多個,如果按照上面的迭代方法計算的話會很慢,所以具體實現要優化一下,加快數據流分析?;緣K內指令是線性執行的,不存在環和分支,活躍變量分析屬于逆向數據流問題。如果能夠安排每一個結點的計算都先于它的前驅,是可以通過對所有結點的一次遍歷就能完成數據流分析,得到每一個結點的入口活躍和出口活躍信息。獲取基本塊內每一個指令的use和def信息的話,可以使用capstone CsInsn類的regs_access方法,這還可以獲取到eflags寄存器。一開始是想使用ida的microcode API來實現的,但是我覺得ida提供的python接口不好用,沒有提供針對一條指令的轉換接口。
對合并后的基本塊的分析思路如下:
1、首先利用capstone反匯編基本塊內的指令,獲取每一條指令的use和def信息。
2、從基本塊的出口處的指令向入口計算每一條指令的出口活躍信息和入口活躍信息。
3、在獲取到指令的活躍信息后,然后根據每一條指令的def信息,如果def中的所有變量都不屬于出口活躍的,我們就可以刪掉這條指令。
具體代碼實現如下:
這里有幾點需要說明一下,capstone的al,ah,ax等8位或16位的寄存器是單獨定義的,需要轉換到32位,因為vmp有8位或16位寄存器參與到下一個handle地址的運算?;蛘咭部梢园堰@些寄存器添加到In[Exit]中,添加In[Exit]是為了方便計算,作為整個基本塊的出口活躍信息,不屬于任意一條指令。把一些通用寄存器添加到基本塊的出口活躍信息,也是為了保證不會nop掉有用的指令。那個IsRadical的判斷主要是為了處理push imm;call target中target處的虛擬機入口。只添加ebp,esp,esi和edi作為整個基本塊出口活躍信息是為了減少target處基本塊沒有nop掉的垃圾指令。在nop掉指令前,需要注意的是,編譯器在刪除死代碼的優化中會考慮到當前被刪除的指令是否有副作用,比如是否為訪存指令、call指令等。本次樣本中vmp的垃圾指令好像都沒有副作用,不存在那些有副作用的指令,所以就沒有考慮這些,nop完之后樣本是可以正常運行的。最后在處理以下垃圾指令,直接遍歷每一個基本塊遇到這類指令直接nop掉。
這里展示一下入口處進入虛擬機那部分nop掉垃圾指令后的代碼和部分x64dbg的trace截圖
可以看到已經去掉大部分垃圾指令了,剩下的漏網之魚也是很容易可以看出來的。
總結
根據實際運行效果,說明我的分析思路是大體正確的。這里沒有根據基本塊作為一個結點做全局的活躍變量分析是因為把通用寄存器作為活躍分析中的變量是不適合這么做的。因為通用寄存器的數量有限,是重復使用的資源,其活躍性很容易在下一個基本塊被殺死。要做全局的活躍變量分析的話,應先把整個流圖轉換到SSA形式,這樣應該可以干掉那些漏網之魚了??紤]到工作量有點大,就沒有這么做了(更多逆向分析資源請訪問 大神論壇)。上傳的源碼中我也實現了一個獲取合并后基本塊的出口活躍和入口活躍信息的函數,不是SSA形式的。只是寫來鞏固一下自己所學知識點而已,對去除垃圾指令也沒什么作用,大家有興趣的話可以參考一下。也沒有使用迭代的方法,而是做了一部分優化,通過工作表算法和對結點的深度優先搜索遍歷序號進行計算的。代碼實現在GlobalLiveness函數中。優化方法可以參考《現代編譯原理-C語言描述》17章的加快數據流分析部分。
本文所有的分析文件和源碼打包都在附件鏈接帖子末尾中 https://www.dslt.tech/article-97-1.html,歡迎下載交流學習,哈哈~~,更多逆向學習資料,可訪問 大神論壇
版權聲明:本文由 白云點綴的藍 原創,歡迎分享本文,轉載請保留出處
總結
以上是生活随笔為你收集整理的大神论坛 利用活跃变量分析来去掉vmp的大部分垃圾指令的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从企业实务角度解读 ITIL4 之14个
- 下一篇: 系统集成项目管理工程师知识点总结(错题记