在Windows平臺下,應用程式為了保護自己不被偵錯程式偵錯會通過各種方法限制程序偵錯自身,通常此類反偵錯技術會限制我們對其進行軟體逆向與漏洞分析,下面是一些常見的反偵錯保護方法:
我們以第一種IsDebuggerPresent
反偵錯為例,該函數用於檢查當前程式是否在偵錯程式的環境下執行。函數返回一個布林值,如果當前程式正在被偵錯,則返回True,否則返回False。
函數通過檢查特定的記憶體地址來判斷是否有偵錯程式在執行。具體來說,該函數檢查了PEB(程序環境塊)
資料結構中的_PEB_LDR_DATA
欄位,該欄位標識當前程式是否處於偵錯狀態。如果該欄位的值為1,則表示當前程式正在被偵錯,否則表示當前程式沒有被偵錯。
獲取PEB的方式有許多,雖然LyScript外掛內提供了get_peb_address(dbg.get_process_id())
系列函數可以直接獲取到程序的PEB資訊,但為了分析實現原理,筆者首先會通過程式碼來實現這個功能;
如下程式碼,通過在目標程式中建立一個堆空間並向其中寫入組合指令,最後將程式的EIP暫存器設定為堆空間的首地址,以使得程式執行時執行堆空間中的組合指令。
具體來說,該程式碼通過呼叫MyDebug
類的create_alloc
方法建立一個堆空間,並通過呼叫assemble_at
方法向堆空間寫入組合指令。該程式碼先寫入mov eax,fs:[0x30]
指令,該指令將FS暫存器
的值加上0x30
的偏移量存入EAX
暫存器,從而得到_PEB
資料結構的地址。
然後,程式碼再寫入mov eax,[eax+0x0C]
指令,該指令將EAX暫存器加上0x0C
的偏移量後的值存入EAX暫存器,從而得到_PEB_LDR_DATA
資料結構的地址。最後,寫入jmp eip
指令,以使得程式回到原來的EIP
位置。最後,程式碼通過呼叫set_register
方法設定EIP暫存器的值為堆空間的首地址,以使得程式執行時執行堆空間中的組合指令。
from LyScript32 import MyDebug
if __name__ == "__main__":
dbg = MyDebug(address="127.0.0.1")
dbg.connect()
# 儲存當前EIP
eip = dbg.get_register("eip")
# 建立堆
heap_addres = dbg.create_alloc(1024)
print("堆空間地址: {}".format(hex(heap_addres)))
# 寫出組合指令
# mov eax,fs:[0x30] 得到 _PEB
dbg.assemble_at(heap_addres,"mov eax,fs:[0x30]")
asmfs_size = dbg.get_disasm_operand_size(heap_addres)
# 寫出組合指令
# mov eax,[eax+0x0C] 得到 _PEB_LDR_DATA
dbg.assemble_at(heap_addres + asmfs_size, "mov eax, [eax + 0x0C]")
asmeax_size = dbg.get_disasm_operand_size(heap_addres + asmfs_size)
# 跳轉回EIP位置
dbg.assemble_at(heap_addres+ asmfs_size + asmeax_size , "jmp {}".format(hex(eip)))
# 設定EIP到堆首地址
dbg.set_register("eip",heap_addres)
dbg.close()
當這段讀入組合指令被執行時,此時PEB入口
地址將被返回給EAX
暫存器,使用者只需要取出該暫存器中的引數即可實現讀取程序PEB
的功能。
當PEB入口地址得到之後,只需要檢查PEB+2
的位置標誌,通過write_memory_byte()
函數向此處寫出0即可繞過反偵錯,從而讓程式可以被正常偵錯。
from LyScript32 import MyDebug
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 通過PEB找到偵錯標誌位
peb = dbg.get_peb_address(dbg.get_process_id())
print("偵錯標誌地址: 0x{:x}".format(peb+2))
flag = dbg.read_memory_byte(peb+2)
print("偵錯標誌位: {}".format(flag))
# 將偵錯標誌設定為0即可過掉反偵錯
nop_debug = dbg.write_memory_byte(peb+2,0)
print("反偵錯繞過狀態: {}".format(nop_debug))
dbg.close()
這裡筆者繼續拓展一個新知識點,如何實現繞過程序列舉功能,病毒會利用程序列舉函數Process32FirstW
及Process32NextW
列舉所有執行的程序以確認是否有偵錯程式在執行,我們可以在特定的函數開頭處寫入SUB EAX,EAX RET
指令讓其無法呼叫列舉函數從而失效,寫入組合指令集需要依賴於set_assemble_opcde
函數,只需要向函數內傳入記憶體地址,則自動替換地址處的組合指令集;
from LyScript32 import MyDebug
# 得到所需要的機器碼
def set_assemble_opcde(dbg,address):
# 得到第一條長度
opcode_size = dbg.assemble_code_size("sub eax,eax")
# 寫出組合指令
dbg.assemble_at(address, "sub eax,eax")
dbg.assemble_at(address + opcode_size , "ret")
if __name__ == "__main__":
# 初始化
dbg = MyDebug()
dbg.connect()
# 得到函數所在記憶體地址
process32first = dbg.get_module_from_function("kernel32","Process32FirstW")
process32next = dbg.get_module_from_function("kernel32","Process32NextW")
print("process32first = 0x{:x} | process32next = 0x{:x}".format(process32first,process32next))
# 替換函數位置為sub eax,eax ret
set_assemble_opcde(dbg, process32first)
set_assemble_opcde(dbg, process32next)
dbg.close()
當上述程式碼被執行後,則Process32FirstW
與Process32FirstW
函數位置將被依次寫出返回指令,從而讓程序列舉失效,輸出效果圖如下所示;