首页
论坛
课程
招聘
[翻译]API Call Tracing - PEfile, PyDbg and IDAPython
2012-6-8 22:48 16119

[翻译]API Call Tracing - PEfile, PyDbg and IDAPython

2012-6-8 22:48
16119
原文: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() 

[公告]《安卓APP加固攻与防》训练营!Android逆向分析技能,干货满满,报名免费赠送一部手机!

上传的附件:
收藏
点赞1
打赏
分享
最新回复 (9)
雪    币: 681
活跃值: 活跃值 (16)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
zyqqyz 活跃值 1 2012-6-8 23:09
2
0
这个给力,必须顶
雪    币: 306
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
天法道 活跃值 2012-6-9 08:56
3
0
这个好。

补一补
雪    币: 1834
活跃值: 活跃值 (15)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
yingyue 活跃值 2012-6-9 09:06
4
0
恩,翻译辛苦了,收
雪    币: 4991
活跃值: 活跃值 (26)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
奘和 活跃值 2012-6-9 14:08
5
0
翻译不容易啊
谢谢了哈……
雪    币: 346
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
YwdxY 活跃值 2012-6-9 15:32
6
0
好文章,感谢~
雪    币: 7
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
LostFish 活跃值 2013-1-13 15:41
7
0
lz的翻译的这篇文章很有用,但我运行代码的时候出现了下面的错误:
pydbg.pdx.pdx: Failed setting breakpoint at 01012d6c
位于函数main()的这一行:dbg.bp_set(entrypoint,handler=entryhandler)
搜一篇帖子http://www.openrce.org/forums/posts/760说是
pydbg不能设置延期断点,不是太明白,想问lz是怎么解决的?
雪    币: 284
活跃值: 活跃值 (16)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
darkplayer 活跃值 2013-1-13 15:49
8
0
好贴,顶起!
雪    币: 7
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
LostFish 活跃值 2013-1-14 09:36
9
0
我从win7换到xp下运行脚本就没有问题了。
雪    币: 146
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
nsso 活跃值 2013-1-16 14:36
10
0
好文章。现在才看到。。。
游客
登录 | 注册 方可回帖
返回