原文:http://securityxploded.com/api-call-tracing-with-pefile-pydbg-and-idapython.php#API-CALL-PEfile-PyDbg
API Call Tracing 是非常强大的技术.它可以让我们充分的把握一个可执行文件的运行机制,某些情况下我们只要得到
api的调用日志就可以了解到程序的行为.我经常利用这种自动化分析技术来完成对恶意软件的分析工作.
这篇帖子我将展示一下我所使用的一些方法
以下情况使用这些方法会使效率大大的提高:
1.脱壳
2.程序行为分析
3.找出二进制文件中你所感兴趣的函数
这里,我将使用PyDbg脚本去记录API的调用,最后用IDAPython脚本自动完成一些手工的操作.
API Calls Logging with PEfile & PyDbg
基于以上情形我们需要以下信息来编写我们的脚本:
1.返回地址- 调用这个API位置?
2.API的名字-哪一个函数被调用了?
我们需要在每个API上设置断点,这样的话我们需要API的名字和地址,如果得到API的名称我们就可以去解析他的地址然后
设置断点.有个地址我们就可以直接设置断点,问题是怎么得到API的名称?
这可以用PEfile来解决.我们先枚举可执行文件的导入表然后解析出地址然后通过PyDbg来设置断点.
但这样的话又会有以下问题
1.某些API函数是使用LoadLibrary()加载的(通过导入表得不到地址和名称 译者注).
2.二进制文件被加壳了,导入表是在运行过程中才被创建.
在解决这个问题之前让我们先看看外壳程序在运行时是怎样构建导入表的.
通常会使用LoadLibrary 去加载某个dll,然后用GetProcAddress 获取API的地址.LoadLibrary 和GetProcAddress由
kernel32.dll导出,默认情况下所有的Windows进程都会加载.
所以如果我们中断在GetProcAddress上我们就可以通过此时的堆栈来得到函数的名称,这样的话就可以对每个API设置断
点了.Here I am ignoring the call for GetProcAddress with API Ordinal because it is not a common approach.
不过还有另外一种在运行时构建导入表的方法,以某个恶意软件为代表.
反汇编代码如下
push dword ptr fs:[30h] ; PEB
pop eax
mov eax,[eax+0ch] ; LDR
mov ecx,[eax+0ch] ; InLoadOrderModuleList
mov edx,[ecx]
push edx
mov eax,[ecx+30h]
这里是PEB结构的截图
这种方法,loader首先定位kernel32.dll的基址(after ntdll.dll in InLoadOrderModuleList link list),然后遍历
kernel32.dll的导出表得到LoadLibrary()的基址.
loader会使用以下方法来加载依赖的dll以及解析API的地址:
1.GetProcAddress -和之前的方法一样.
2.遍历每个已载入的dll的导入表
对于这个处理方式我们需要使用全局钩子或者SSTD hooks ,已经超出了本文的范围。
以下是API Call Tracing的具体步骤:
1.遍历二进制文件的导出表在没个API上设置断点
2.在GetProcAddress 上也设置断点.
3.如果不是中断在GetProcAddress上 则直接记录堆栈中的返回地址以及函数的名称.
4.如果中断在GetProcAddress上,则取出函数的名称和返回地址,并在返回地址上设置断点
5.如果中断在返回上则取出EAX的值并设置断点
至此,我们编写PyDbg脚本
'''
Author: Amit Malik
http://www.securityxploded.com
'''
import sys,struct
import pefile
from pydbg import *
from pydbg.defines import *
def log(str):
global fpp
print str
fpp.write(str)
fpp.write("\n")
return
def addr_handler(dbg):
global func_name
ret_addr = dbg.context.Eax
if ret_addr:
dict[ret_addr] = func_name
dbg.bp_set(ret_addr,handler=generic)
return DBG_CONTINUE
def generic(dbg):
global func_name
eip = dbg.context.Eip
esp = dbg.context.Esp
paddr = dbg.read_process_memory(esp,4)
addr = struct.unpack("L",paddr)[0]
addr = int(addr)
if addr < 70000000:
log("RETURN ADDRESS: 0x%.8x\tCALL: %s" % (addr,dict[eip]))
if dict[eip] == "KERNEL32!GetProcAddress" or dict[eip] == "GetProcAddress":
try:
esp = dbg.context.Esp
addr = esp + 0x8
size = 50
pstring = dbg.read_process_memory(addr,4)
pstring = struct.unpack("L",pstring)[0]
pstring = int(pstring)
if pstring > 500:
data = dbg.read_process_memory(pstring,size)
func_name = dbg.get_ascii_string(data)
else:
func_name = "Ordinal entry"
paddr = dbg.read_process_memory(esp,4)
addr = struct.unpack("L",paddr)[0]
addr = int(addr)
dbg.bp_set(addr,handler=addr_handler)
except:
pass
return DBG_CONTINUE
def entryhandler(dbg):
getaddr = dbg.func_resolve("kernel32.dll","GetProcAddress")
dict[getaddr] = "kernel32!GetProcAddress"
dbg.bp_set(getaddr,handler=generic)
for entry in pe.DIRECTORY_ENTRY_IMPORT:
DllName = entry.dll
for imp in entry.imports:
api = imp.name
address = dbg.func_resolve(DllName,api)
if address:
try:
Dllname = DllName.split(".")[0]
dll_func = Dllname + "!" + api
dict[address] = dll_func
dbg.bp_set(address,handler=generic)
except:
pass
return DBG_CONTINUE
def main():
global pe, DllName, func_name,fpp
global dict
dict = {}
file = sys.argv[1]
fpp = open("calls_log.txt",'a')
pe = pefile.PE(file)
dbg = pydbg()
dbg.load(file)
entrypoint = pe.OPTIONAL_HEADER.ImageBase + pe.OPTIONAL_HEADER.AddressOfEntryPoint
dbg.bp_set(entrypoint,handler=entryhandler)
dbg.run()
fpp.close()
if __name__ == '__main__':
main()
执行脚本输出如下
RETURN ADDRESS: 0x004030e8 CALL: kernel32!GetModuleHandleA
RETURN ADDRESS: 0x004030f3 CALL: kernel32!GetCommandLineA
RETURN ADDRESS: 0x00404587 CALL: kernel32!GetModuleHandleA
RETURN ADDRESS: 0x00404594 CALL: kernel32!GetProcAddress
RETURN ADDRESS: 0x004045aa CALL: kernel32!GetProcAddress
RETURN ADDRESS: 0x004045c0 CALL: kernel32!GetProcAddress
下面我们看一些具体的示例
1) UPX脱壳
跟踪upx的加壳程序.仔细观察输出记过你能看出哪些函数包含了OEP吗?
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress
RETURN ADDRESS: 0x00784b9e CALL: GetProcAddress
RETURN ADDRESS: 0x00784bc8 CALL: KERNEL32!VirtualProtect
RETURN ADDRESS: 0x00784bdd CALL: KERNEL32!VirtualProtect --> 1
RETURN ADDRESS: 0x0045ac09 CALL: GetSystemTimeAsFileTime --> 2
RETURN ADDRESS: 0x0045ac15 CALL: GetCurrentProcessId
RETURN ADDRESS: 0x0045ac1d CALL: GetCurrentThreadId
RETURN ADDRESS: 0x0045ac25 CALL: GetTickCount
RETURN ADDRESS: 0x0045ac31 CALL: QueryPerformanceCounter
RETURN ADDRESS: 0x0044e99f CALL: GetStartupInfoA
RETURN ADDRESS: 0x0044fd9c CALL: HeapCreate
标注1位置的'Return Address'为0x00784bdd,标注2处为0x0045ac09,2个地址之间的跨度比较大,因此0x0045ac09就是包含了OEP的函数了.
可以通过OD进行验证
目前多数的恶意软件都有自定义的外壳(custom packers),我发现这种方法在脱壳界是非常管用的(od有很多脱壳脚本 不过感觉python脚本更好一些 译者注).
2)二进制文件行为分析
仔细看下面的输出结果,你能说说它都干了些什么吗?
RETURN ADDRESS: 0x004012ce CALL: msvcrt!fopen --> 1
RETURN ADDRESS: 0x00401311 CALL: msvcrt!fseek
RETURN ADDRESS: 0x0040131c CALL: msvcrt!ftell
RETURN ADDRESS: 0x0040133a CALL: msvcrt!fseek
RETURN ADDRESS: 0x00401346 CALL: msvcrt!malloc --> 2
RETURN ADDRESS: 0x00401387 CALL: msvcrt!fread --> 3
RETURN ADDRESS: 0x00401392 CALL: msvcrt!fclose
RETURN ADDRESS: 0x004013b4 CALL: KERNEL32!OpenProcess --> 4
RETURN ADDRESS: 0x004013ee CALL: KERNEL32!VirtualAllocEx --> 5
RETURN ADDRESS: 0x00401425 CALL: KERNEL32!WriteProcessMemory --> 6
RETURN ADDRESS: 0x0040146b CALL: KERNEL32!CreateRemoteThread --> 7
RETURN ADDRESS: 0x004014a4 CALL: msvcrt!exit
很清晰的看出程序读了一个文件然后注入代码到另外的进程中.
3)查找感兴趣的函数
这是另外一个程序的API日志
RETURN ADDRESS: 0x00443c29 CALL: inet_ntoa --> point 1
RETURN ADDRESS: 0x0044a6ee CALL: KERNEL32!HeapAlloc
RETURN ADDRESS: 0x00446866 CALL: KERNEL32!GetLocalTime
RETURN ADDRESS: 0x0044a6ee CALL: KERNEL32!HeapAlloc
RETURN ADDRESS: 0x00443f79 CALL: socket --> point 2
RETURN ADDRESS: 0x00443fb5 CALL: setsockop
RETURN ADDRESS: 0x00443fd0 CALL: setsockopt
RETURN ADDRESS: 0x00444045 CALL: ntohl
RETURN ADDRESS: 0x0044404f CALL: ntohs
RETURN ADDRESS: 0x00444063 CALL: bind --> point 3
RETURN ADDRESS: 0x0044412c CALL: ntohl
RETURN ADDRESS: 0x0044413c CALL: ntohs
RETURN ADDRESS: 0x0043adf6 CALL: WSAAsyncSelect
RETURN ADDRESS: 0x0044416b CALL: connect --> point 4
RETURN ADDRESS: 0x00444176 CALL: WSAGetLastError
RETURN ADDRESS: 0x00441979 CALL: USER32!DispatchMessageA
RETURN ADDRESS: 0x00444ce0 CALL: KERNEL32!GetTickCount
RETURN ADDRESS: 0x00444cfa CALL: KERNEL32!QueryPerformanceCounter
RETURN ADDRESS: 0x00444499 CALL: recv --> point 5
RETURN ADDRESS: 0x0044a8c6 CALL: KERNEL32!HeapFre
RETURN ADDRESS: 0x0043adf6 CALL: WSAAsyncSelect
RETURN ADDRESS: 0x004441f7 CALL: closesocket
RETURN ADDRESS: 0x0044a8c6 CALL: KERNEL32!HeapFree
标注的地方显示程序调用网络函数的位置
使用IDAPython扩展API Tracing
我们可以进一步的将导出的地址信息和IDA结合以便利用IDA的函数识别和交叉引用功能
接下来的IDAPython 脚本读取输出的日志文件并对地址进行着色.
'''
Author: Amit Malik
http://www.securityxploded.com
'''
from idaapi import *
from idc import *
import sys
class logparse():
def __init__(self,file_path):
self.file_path = file_path
self.fp = open(self.file_path,'r')
self.data = self.fp.readlines()
def parser(self):
dict = {}
for line in self.data:
line_slice = line.split()
address = line_slice[2]
name = line_slice[4]
dict[address] = name
for ea in dict.keys():
print dict[ea]
ea_c = PrevHead(ea)
SetColor(ea_c,CIC_ITEM,0x8CE6F0)
return
def main():
file_path = AskFile(0,"*.*","Enter file name: ")
logobj = logparse(file_path)
logobj.parser()
return
if __name__ == '__main__':
main()
[培训]《安卓高级研修班(网课)》月薪三万计划,掌
握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法