程序行为追踪(API TRACING)
对木马类程序处理多了,就渐渐觉得静/动态手工分析过程在很大程度上都是重复劳动。总要先花半个钟头了解程序特性,手工分析时还生怕漏掉某项隐蔽的关键操作,导致最终清除不彻底。其实只要在主动安装木马的时候将API调用序列及相应参数做完整记录,就能极大减轻分析和清除木马的工作量。
以前曾写过一个利用 API HOOKING 原理记录可疑程序对文件、注册表、服务和网络操作的小工具。API HOOKING 方式的优点在于,当调用 CreateFile 时可将文件名与句柄关联,等调用 WriteFile 对句柄操作时便能轻易取到文件名,对 hKey、socket 等句柄操作亦是如此。但该方式的缺点也显而易见,首先必须为每个感兴趣的 API 函数编写代码,“体力工作”繁重;其次我们不可能 HOOK 所有的 API 函数,由于缺乏完整的 API 调用序列作参考,在分析日志时很可能漏掉某些小动作。
另一种思路是采用调试技术,在所有被引入的 DLL 的各函数入口处预先设置断点,调试期间再通过堆栈信息获取参数。IDA pro 和 OllyDbg 都可用于动态调试,同时还提供了脚本/插件功能。上周在北京开会的时候,我利用酒醒的时间写了一个简单的 OllyDbg 插件,仅从 CALL 指令处通过 ESP 指针获取8个函数参数,不对函数返回后的 EAX 及堆栈内容进行记录,在对普通(未加壳)程序的测试中效果还算理想。只要先在“Search for -> All intermodular calls”窗口中执行“Set breakpoint on every command”设置断点,再运行插件的“Fast trace”功能即可。日志文件片断如下:
-------------------------------------------------------------------
004099EC: CALL DWORD PTR DS:[<&KERNEL32.GetModuleFileNameA>] (kernel32.GetModuleFileNameA)
-------------------------------------------------------------------
ESP+00 (0012F704): 00000000
ESP+04 (0012F708): 0012F824 ""
ESP+08 (0012F70C): 00000104 00000104 ???
ESP+0C (0012F710): 0012FA6D ""
ESP+10 (0012F714): 00000001 00000001 ???
ESP+14 (0012F718): 00000000
ESP+18 (0012F71C): 575C3A43 575C3A43 ???
ESP+1C (0012F720): 4F444E49 4F444E49 ???
-------------------------------------------------------------------
00409A00: CALL DWORD PTR DS:[<&KERNEL32.CopyFileA>] (kernel32.CopyFileA)
-------------------------------------------------------------------
ESP+00 (0012F704): 0012F824 "E:\trojan.exe"
ESP+04 (0012F708): 0012F71C "C:\WINDOWS\system32\trojan.exe"
ESP+08 (0012F70C): 00000000
ESP+0C (0012F710): 0012FA6D ""
ESP+10 (0012F714): 00000001 00000001 ???
ESP+14 (0012F718): 00000000
ESP+18 (0012F71C): 575C3A43 575C3A43 ???
ESP+1C (0012F720): 4F444E49 4F444E49 ???
-------------------------------------------------------------------
00409A94: CALL DWORD PTR DS:[<&ADVAPI32.OpenSCManagerA>] (ADVAPI32.OpenSCManagerA)
-------------------------------------------------------------------
ESP+00 (0012F704): 00000000
ESP+04 (0012F708): 00000000
ESP+08 (0012F70C): 000F003F 000F003F ???
ESP+0C (0012F710): 0012FA6D ""
ESP+10 (0012F714): 00000001 00000001 ???
ESP+14 (0012F718): 00000000
ESP+18 (0012F71C): 575C3A43 575C3A43 ???
ESP+1C (0012F720): 4F444E49 4F444E49 ???
-------------------------------------------------------------------
00409ACF: CALL DWORD PTR DS:[<&ADVAPI32.CreateServiceA>] (ADVAPI32.CreateServiceA)
-------------------------------------------------------------------
ESP+00 (0012F6DC): 0014F9C0
F8 F9 14 00 98 BA DC FE 00 00 00 00 B4 F9 CC 53 ...............S
82 6C FC 42 BF 8C 55 14 00 44 14 F4 AB AB AB AB .l.B..U..D......
AB AB AB AB EE FE EE FE 00 00 00 00 00 00 00 00 ................
20 00 07 00 09 07 18 00 58 FA C3 77 EF CD AB 89 .......X..w....
00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 ................
00 00 00 00 00 00 00 00 05 00 00 00 01 00 00 00 ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
06 00 00 00 0A 00 00 00 00 00 00 00 30 FB 14 00 ............0...
ESP+04 (0012F6E0): 0042008C "trojan"
ESP+08 (0012F6E4): 004200C0 "Back door for testing"
ESP+0C (0012F6E8): 000F01FF 000F01FF ???
ESP+10 (0012F6EC): 00000120 00000120 ???
ESP+14 (0012F6F0): 00000002 00000002 ???
ESP+18 (0012F6F4): 00000001 00000001 ???
ESP+1C (0012F6F8): 0012F71C "C:\WINDOWS\system32\trojan.exe -start"
......
这种简单粗糙的日志对我来说已经够用了。若希望以更友好的形式显示参数信息,就必须有一些数据文件来描述各 API 函数的调用方式、返回值类型、参数个数等内容。比如这样:
int LoadLibraryA([in] char *lpLibFileName);
int LoadLibraryW([in] wchar *lpLibFileName);
void *GetProcAddress([in] int hModule, [in] char *lpProcName);
int GetModuleFileNameA([in] int hModule, [out] char *lpFilename, [in] int nSize);
int GetModuleFileNameW([in] int hModule, [out] wchar *lpFilename, [in] int nSize);
编写一个简单的词法解析模块直接解析 VC 自带的 .h 文件,对使用者来说就更省事了。经过参数类型解析后的输出信息会好看很多:
------------------------------------------------------
004099F2 -> GetModuleFileNameA(
int hModule: 0 (unsigned = 0 / hex = 0),
char* lpFilename: [0012F824] = "",
int nSize: 260 (unsigned = 260 / hex = 104),
13 << results
int hModule: 0 (unsigned = 0 / hex = 0),
char* lpFilename: [0012F824] = "e:\trojan.exe" in stack of Thread,
int nSize: 260 (unsigned = 260 / hex = 104)
);
------------------------------------------------------
00409A06 -> CopyFileA(
char* lpExistingFileName: [0012F824] = "e:\trojan.exe" in stack of Thread,
char* lpNewFileName: [0012F71C] = "C:\WINDOWS\system32\trojan.exe" in stack of Thread,
int bFailIfExists: 0 (unsigned = 0 / hex = 0),
1 << results
char* lpExistingFileName: [0012F824] = "",
char* lpNewFileName: [0012F71C] = "",
int bFailIfExists: 0 (unsigned = 0 / hex = 0)
);
------------------------------------------------------
00409A9A -> OpenSCManagerA(
char* lpMachineName: [00000000] = (null),
char* lpDatabaseName: [00000000] = (null),
int dwDesiredAccess: 983103 (unsigned = 983103 / hex = F003F),
1374656 << results
char* lpMachineName: [00000000] = "",
char* lpDatabaseName: [00000000] = "",
int dwDesiredAccess: 983103 (unsigned = 983103 / hex = F003F)
);
------------------------------------------------------
00409AD5 -> CreateServiceA(
int hSCManager: 1374656 (unsigned = 1374656 / hex = 14F9C0),
char* lpServiceName: [0042008C] = "trojan" in main image (.data),
char* lpDisplayName: [004200C0] = "Back door for testing" in main image (.data),
int dwDesiredAccess: 983551 (unsigned = 983551 / hex = F01FF),
int dwServiceType: 288 (unsigned = 288 / hex = 120),
int dwStartType: 2 (unsigned = 2 / hex = 2),
int dwErrorControl: 1 (unsigned = 1 / hex = 1),
char* lpBinaryPathName: [0012F71C] = "C:\WINDOWS\system32\trojan.exe -start" in stack of Thread,
char* lpLoadOrderGroup: [00000000] = (null),
int* lpdwTagId: 00000000,
char* lpDependencies: [004201C4] = "" in main image (.data),
char* lpServiceStartName: [00000000] = (null),
char* lpPassword: [00000000] = (null),
1370392 << results
int hSCManager: 1374656 (unsigned = 1374656 / hex = 14F9C0),
char* lpServiceName: [0042008C] = "",
char* lpDisplayName: [004200C0] = "",
int dwDesiredAccess: 983551 (unsigned = 983551 / hex = F01FF),
int dwServiceType: 288 (unsigned = 288 / hex = 120),
int dwStartType: 2 (unsigned = 2 / hex = 2),
int dwErrorControl: 1 (unsigned = 1 / hex = 1),
char* lpBinaryPathName: [0012F71C] = "",
char* lpLoadOrderGroup: [00000000] = "",
int* lpdwTagId: 00000000,
char* lpDependencies: [004201C4] = "",
char* lpServiceStartName: [00000000] = "",
char* lpPassword: [00000000] = ""
);
......
dumbug 是一个开源的 API TRACING 工具,但被设计为仅对 trace 文件中定义的 API 调用进行跟踪。要想通过原始的 dumbug 获得完整 API 调用序列,工作量一点也不比 API HOOKING 方式小。而且就分析木马程序来说,我们并不需要记录 kernel32.dll 等系统链接库内部的 API 调用序列,所以还应根据 EXE 和 DLL 的入口地址、代码段长度进行过滤,最大限度减少冗余信息。在 dumbug 中,只要为 Tracer 对象的 ActivateTraces() 方法添加一些代码,并在其他地方也做相应的小修改,就可以输出上面的结果了。
附1 - dumbug 的源代码可以从这里获得:
[url]http://www.phenoelit.de/dumbug/dumbugVegasRelease.zip[/url]
附2 - 简单的 ApiTracing-plugin for OllyDbg 源代码:
// ApiTracing.c
#define STRICT // Avoids some type mismatches
#include <windows.h>
#include <stdio.h>
#include <dir.h>
#include "plugin.h"
#define VERSIONHI 1 // High plugin version
#define VERSIONLO 0 // Low plugin version
#define LOG_FILENAME "TraceApi.log" // Log filename
static HINSTANCE hinst; // DLL instance
static BOOL bFastTracing = TRUE;
static BOOL bStartTrace = FALSE;
int Execute(char *text,char *answer);
BOOL WINAPI DllEntryPoint(HINSTANCE hi, DWORD reason, LPVOID reserved)
{
FILE *fLog;
if (reason == DLL_PROCESS_ATTACH) {
hinst = hi; // Mark plugin instance
fLog = fopen(LOG_FILENAME, "w");
if (fLog) {
fprintf(fLog, "API tracing plugin v%i.%02i, written by glacier_at_xfocus.org\n",
VERSIONHI, VERSIONLO);
fclose(fLog);
}
}
return 1; // Report success
}
// Report plugin name and return version of plugin interface.
extc int _export cdecl ODBG_Plugindata(char shortname[32])
{
strcpy(shortname, "API tracing"); // Name of command line plugin
return PLUGIN_VERSION;
}
extc int _export cdecl ODBG_Plugininit(int ollydbgversion, HWND hw, ulong *features)
{
// This plugin uses some newest features,
// check that version of OllyDbg is correct.
if (ollydbgversion < PLUGIN_VERSION)
return -1;
return 0;
}
extc void _export cdecl ODBG_Pluginmainloop(DEBUG_EVENT *debugevent) {
}
// Function adds items to main OllyDbg menu (origin=PM_MAIN).
extc int _export cdecl ODBG_Pluginmenu(int origin, char data[4096], void *item)
{
if (origin != PM_MAIN)
return 0; // No pop-up menus in OllyDbg's windows
strcpy(data, "0 &Fast trace,1 &Slow trace|2 &About");
return 1;
}
// Receives commands from main menu.
extc void _export cdecl ODBG_Pluginaction(int origin, int action, void *item)
{
char szLine[MAX_PATH] = {0};
if (origin != PM_MAIN)
return;
switch (action) {
case 0: // Fast tracing
bFastTracing = TRUE;
bStartTrace = TRUE;
Sendshortcut(PM_MAIN, 0, WM_KEYDOWN, 0, 0, VK_F9);
break;
case 1: // Slow tracing
bFastTracing = FALSE;
bStartTrace = TRUE;
Sendshortcut(PM_MAIN, 0, WM_KEYDOWN, 0, 0, VK_F7);
break;
case 2: // "About", displays plugin info
sprintf(szLine, "API tracing plugin v%i.%02i",
VERSIONHI, VERSIONLO);
MessageBox(0, szLine, "API tracing", MB_OK|MB_ICONINFORMATION);
break;
default: break;
}
}
// User opens new or restarts current application.
extc void _export cdecl ODBG_Pluginreset(void)
{
bStartTrace = FALSE;
}
extc int _export cdecl ODBG_Pluginclose(void)
{
return 0;
}
extc void _export cdecl ODBG_Plugindestroy(void)
{
}
// 记录二进制内容
void LogBinToFile(char *szFileName, const char *pBuf, int nSize)
{
FILE *fLog;
int i, j;
unsigned const char *ptr = (unsigned const char *)pBuf;
fLog = fopen(szFileName, "a+");
if (!fLog) return;
if (nSize == 0)
nSize = strlen(pBuf);
for (i=0; i<nSize; i=i+0x10) {
fprintf(fLog, "\t\t");
for (j=i; j<i+0x10 && j<nSize; j++)
fprintf(fLog, "%02X ", ptr[j]);
fprintf(fLog, "\t");
for (j=i; j<i+0x10 && j<nSize; j++) {
if (IsCharAlpha(ptr[j]) || (ptr[j]>=0x20 && ptr[j]<0x7F))
fprintf(fLog, "%c", ptr[j]);
else
fprintf(fLog, "%c", '.');
}
fprintf(fLog, "\n");
}
fclose(fLog);
}
// 格式化记录日志
void LogToFile(char *szFileName, char *szFmt, ...)
{
FILE *fLog;
char buff[1024];
va_list arglist;
va_start(arglist, szFmt);
_vsnprintf(buff, sizeof(buff), szFmt, arglist);
va_end(arglist);
fLog = fopen(szFileName, "a+");
if (!fLog) return;
fprintf(fLog, "%s", buff);
fclose(fLog);
}
// 检查是否为ASCII字符串
BOOL CheckCharAlpha(char *szLine)
{
int i = 0;
while (szLine[i]) {
if (!IsCharAlpha(szLine[i]) && (szLine[i]<0x20 || szLine[i]>=0x7F))
return FALSE;
i++;
}
return TRUE;
}
extc int _export cdecl ODBG_Paused(int reason, t_reg *reg)
{
char szSrcDec[1024] = {0};
char szLine[1024] = {0};
unsigned long uEsp = 0, uAddr = 0, uTemp = 0;
int nSize = 0, i = 0;
if (!bStartTrace) return 0;
if (!reg) {
ShellExecute(0, "open", "notepad.exe", LOG_FILENAME, NULL, SW_SHOW);
return 0;
}
// 读取断点处指令
nSize = Readcommand(reg->ip, szLine);
if (nSize > 0) {
t_disasm disasm;
// 反汇编二进制指令
Disasm(szLine, nSize, reg->ip, szSrcDec, &disasm, DISASM_ALL, 0);
if (strstr(disasm.result, "CALL ")) { // 若为CALL指令
LogToFile(LOG_FILENAME, "\n%s\n",
"------------------------------------------------------");
LogToFile(LOG_FILENAME, "%08X: %s (%s)", reg->ip, disasm.result, disasm.comment);
LogToFile(LOG_FILENAME, "\n%s\n",
"------------------------------------------------------");
uEsp = reg->r[4];
// 由ESP读取8个堆栈参数
uTemp = uEsp;
for (i=0; i<8; i++) {
Readmemory(&uAddr, uTemp, sizeof(uAddr), MM_SILENT);
LogToFile(LOG_FILENAME, "\tESP+%02X (%08X): %08X", i*4, uTemp, uAddr);
if (uAddr == 0) nSize = 0;
else nSize = Decodeascii(uAddr, szLine, sizeof(szLine)-1, DASC_ASCII);
if (nSize > 0 && CheckCharAlpha(szLine)) {
LogToFile(LOG_FILENAME, "\t\t%s\n", szLine);
}
else {
memset(szLine, 0, sizeof(szLine));
nSize = Readmemory(szLine, uAddr, 128, MM_SILENT);
if (nSize > 0) {
LogToFile(LOG_FILENAME, "\n");
LogBinToFile(LOG_FILENAME, szLine, nSize);
}
else
LogToFile(LOG_FILENAME, "\n");
}
uTemp += 4;
}
}
// 继续执行
if (bFastTracing)
Sendshortcut(PM_MAIN, 0, WM_KEYDOWN, 0, 0, VK_F9);
else {
if (strstr(disasm.result, ".0")) {
Go(0, reg->ip, STEP_IN, 1, 0);
}
else {
Go(0, reg->ip, STEP_OVER, 1, 0);
}
}
}
return 0;
}