最近遇到给定一个 DLL 文件,但是想快速定位内部函数(没有导出)的地址,想到 Sym 系列函数可以实现根据已知的 PDB 调试符号数据库直接分析出函数的地址,参考了 MSDN 可以知道我们需要依次调用如下函数:
SymInitialize -> SymSetOptions -> SymLoadModuleEx -> SymEnumSymbols -> CallBackProc
最后就是进入到我们的回调函数 CallBackProc 中处理信息。
SymEnumSymbols 支持第三个参数传入结构体指针,这里我们将需要的信息放在一个结构体中,以便于回调函数内部使用它们:
typedef struct _tagCALLBACKPACKAGE
{
LPCTSTR szDllPath = NULL; // 要检索的 Dll 文件路径,以 null 结尾的字符串
FILE* logfile = NULL; // 打开的日志文件的路径,必须具有写入权限
DWORD64 BaseOfDll = NULL; // 当前模块的基地址
}CALLBACKPACKAGE, * LPCALLBACKPACKAGE;
完整代码如下:
#include <iostream>
#include <Windows.h>
#include <imagehlp.h>
#include <locale.h>
#include <Shlwapi.h>
#pragma comment(lib, "DbgHelp.lib")
#pragma comment(lib, "Shlwapi.lib")
typedef struct _tagCALLBACKPACKAGE
{
LPCTSTR szDllPath = NULL;
FILE* logfile = NULL;
DWORD64 BaseOfDll = NULL;
}CALLBACKPACKAGE, * LPCALLBACKPACKAGE;
BOOL CALLBACK CallBackProc(
PSYMBOL_INFO pSymInfo,
ULONG SymbolSize,
PVOID UserContext
)
{
if (UserContext == nullptr)
{
SetLastError(ERROR_INVALID_PARAMETER);
printf("参数 UserContext 不能为空。\n");
return FALSE;
}
auto pkg = (LPCALLBACKPACKAGE)UserContext;
FILE* hLogFile = pkg->logfile;
DWORD64 BaseOfDll = pkg->BaseOfDll;
fprintf(hLogFile,
"函数名: %s\r\n地址: 0x%I64X \r\n\r\n",
pSymInfo->Name,
pSymInfo->Address - BaseOfDll);
return TRUE;
}
char* UnicodeToAnsi(const wchar_t* szStr, char* szDest)
{
int nLen = WideCharToMultiByte(CP_ACP, 0, szStr, -1, NULL, 0, NULL, NULL);
if (nLen == 0)
{
return NULL;
}
char* pResult = new char[nLen];
WideCharToMultiByte(CP_ACP, 0, szStr, -1, pResult, nLen, NULL, NULL);
strcpy_s(szDest, nLen, pResult);
delete[] pResult;
return szDest;
}
BOOL GetSymbolProc(LPCALLBACKPACKAGE* lpCallBackPackage)
{
if ((*lpCallBackPackage) == nullptr)
{
SetLastError(ERROR_INVALID_PARAMETER);
printf("参数 lpCallBackPackage 不能为空。\n");
return FALSE;
}
CALLBACKPACKAGE* pkg = (*lpCallBackPackage);
HANDLE hProcess = NULL;
hProcess = OpenProcess(PROCESS_ALL_ACCESS,
FALSE, GetCurrentProcessId()); // 获取当前进程的句柄
/* 初始化进程的符号处理程序。
// SymInitialize 函数用于初始化进程的符号处理程序。
// 在符号处理程序的上下文中,进程是收集符号信息时要使用的方便对象。
// 通常,调试器和其他工具使用符号处理程序,
// 这些工具需要为正在调试的进程加载符号。
*/
if (!SymInitialize(hProcess, NULL, FALSE))
{
CloseHandle(hProcess);
SetLastError(ERROR_DELAY_LOAD_FAILED);
printf("初始化进程的符号处理程序失败。\n");
return FALSE;
}
// 更改当前符号调试器的选项掩码
//
// 当应用程序使用库时,可以多次更改这些选项。
// 任何选项更改都会影响将来对符号处理程序的所有调用。
DWORD dwOpt = SymGetOptions();
SymSetOptions(
dwOpt | SYMOPT_DEFERRED_LOADS |
SYMOPT_UNDNAME | SYMOPT_CASE_INSENSITIVE);
// UNICODE 转换
char szFileName[MAX_PATH] = { 0 };
UnicodeToAnsi(pkg->szDllPath, szFileName);
size_t len = strlen(szFileName);
szFileName[len] = '\0';
szFileName[len - 1] = 'b';
szFileName[len - 2] = 'd';
szFileName[len - 3] = 'p';
if (!PathFileExistsA(szFileName))
{
SetLastError(ERROR_FILE_NOT_FOUND);
printf("找不到 PDB 文件。\n");
return FALSE;
}
szFileName[len - 1] = 'l';
szFileName[len - 2] = 'l';
szFileName[len - 3] = 'd';
// 符号处理程序为模块创建一个条目,如果已关闭延迟符号加载选项,
// 则尝试加载符号。 如果启用了延迟符号加载,则模块将
// 被标记为延迟,并且不会加载符号,直到对模块中的符号进行引用。
// 因此,在调用 SymLoadModuleEx 后,应始终调用
// SymGetModuleInfo64 函数。
DWORD64 dwSymModule = SymLoadModuleEx(
hProcess, NULL,
szFileName, NULL,
0, 0, NULL, 0);
if (0 == dwSymModule)
{
SymCleanup(hProcess);
SetLastError(ERROR_DELAY_LOAD_FAILED);
printf("加载符号模块失败。\n");
return -1;
}
// 模块基地址
wprintf(L"SymModuleBaseAddress: 0x%I64X\n", dwSymModule);
pkg->BaseOfDll = dwSymModule;
// 枚举模块信息
if (!SymEnumSymbols(
hProcess,
dwSymModule,
0,
CallBackProc,
pkg))
{
SymCleanup(hProcess);
SetLastError(ERROR_ACCESS_DENIED);
printf("枚举模块信息失败。err_code: [%d]\n", GetLastError());
return -1;
}
return SymCleanup(hProcess);
}
int main(int argc, char* argv[])
{
// 初始换变量,为结构体分配内存
FILE* hLogFile = NULL;
CALLBACKPACKAGE* pkg = new CALLBACKPACKAGE;
if (pkg == nullptr)
{
SetLastError(ERROR_STACK_OVERFLOW);
printf("系统当前内存不足。\n");
return 1;
}
// 打开指定的日志文件
fopen_s(&hLogFile, "F:\\storage_log.txt", "w");// 信息保存路径
if (hLogFile == NULL)
{
pkg->logfile = NULL;
delete pkg;
SetLastError(ERROR_FILE_NOT_FOUND);
printf("无法打开指定的文件。\n");
return 2;
}
// 设置结构体成员
pkg->logfile = hLogFile;
pkg->szDllPath = L"F:\\windows.storage.dll"; // 要查询的 DLL 路径
if (!GetSymbolProc(&pkg))// 调用遍历函数
{
fclose(hLogFile);
pkg->logfile = NULL;
delete pkg;
SetLastError(ERROR_ACCESS_DENIED);
printf("遍历 PDB 文件信息失败。err_code: [%d]\n", GetLastError());
return 3;
}
printf("操作已经完成。\n");
// 清理环境
fclose(hLogFile);
pkg->logfile = NULL;
delete pkg;
system("pause");
return 0;
}
测试结果是将分析的地址和函数名称保存至路径下的一个文本文档下,打开后效果如图:
可见地址都是相对于模块首地址的偏移,计算结果和 IDA 分析结果一致。