0x0 前言
刚刚结束春招,投了好几家公司,结果不是很理想,原因无外乎自身实力和行业寒冬。
这次春招面试题主要集中在PE相关,HOOK技术和DLL注入技术,还有一些杂七杂八的问题上面。从中也暴露处自己技能栈上的不足,通过这一段时间的沉淀希望可以补足。
四月初就开始谋划写一系列的文章,但是加上在校实习比较忙碌,所以进展很慢,这些文章主要面向受众是那些入坑新人,借此希望能够让那些小伙伴能够少走弯路。同时也能多多总结自身的不足,共同进步。
这系列文章取啥名?想了很久,决定参考某一师傅的系列文章—<一篇文章带你·····>,主要希望总结PE文件,HOOK,DLL注入,以及其他方面的知识,主要的参考文献是看雪加密解密第四版,以及其他资料。(打了广告,希望相关师傅记得打点广告费)
这系列是我边总结技术边写文章,可能部分内容会以后补全技能栈,例如R0下的DLL注入等。但是尽量做到不鸽,关于代码,不提供自己写的代码,原因有二,第一,这些代码网上都有现成的,我只是理解修改部分罢了,也怕自己的代码误导小伙伴们。第二,拒绝伸手党。
由于自身能力有限,文章中难免出现错误,希望各位师傅少喷我。
0x1 PE导入表
0x1.1 输入函数的调用
DLL动态链接库文件主要实现代码的复用。当一个程序调用DLL文件中的数据和代码的时候,有两种链接方式,第一种是隐式链接,这个过程是由windows装载器完成的,另外一种是显式链接,通过使用LoadLibrary和GetProcAddress这两个API函数实现的。
当隐式的调用一个API函数的时候,同样也存在类似于LoadLibrary和GetProcAddress函数的功能实现,但是,这个操作是由windows装载器完成的,所以称为隐式链接,当程序使用隐式链接调用DLL代码的时候,装载器需要完成以下几个步骤(IAT填充):
- 首先将所需要的DLL文件载入内存,Kernel32.dll等是通过映射的方式载入的
 - 定位IID,寻找IID的第四个字段Name。
 - 接着根据OrginalFirstThunk指向,获取INT。
 - 根据INT执行的IMAGE_IMPORT_BY_NAME结构获取函数名称
 - 利用类似于GetProcAddress函数功能的操作,获取函数地址VA
 - 将获取的API函数地址填充入IAT。
 - 断链,将FirstThunk断开
 
程序一般使用CALL-JMP的方式调用API方式,显然,这种方式是低效的,不然直接使用CALL高效,之所以使用这种方式,因为编译器无法判断哪些调用是API,哪些调用是普通函数。JMP的地址其实是IAT所在的地址VA。
0x1.2 导入表结构
         在PE文件可选头中,数据目录项的第一个成员指向的导入表。可以看到2040是一个RVA,这是在内存中的偏移量。我们需要将它转化为文件偏移。
    
         我们可以看到2040位于.rdata段中。可以使用公式section[i].PointOfRawData+(offset-VirtuallAddress)来计算文件偏移。计算出来的文件偏移为600+(2040-2000)=640.也就是说PE导入表在文件中640H的地方。
    
同样的,我们可以使用代码实现这一个需求,代码如下:
- 0.定位第一个节区地址
 - 1.获取节区数目
 - 2.判断RVA在那个节区
 - 3.计算:section[i].PointOfRawData+(offset-VirtuallAddress)
 
  | 
  | 
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD Characteristics;
        DWORD OriginalFirstThunk;            //INT(RBA)
    };
    DWORD TimeDateStamp;                    //时间戳
    DWORD ForwarderChain;
    DWORD Name;                                //DllName(RVA)
    DWORD FirstThunk;                        //IAT(RVA)
} IMAGE_IMPORT_DESCRIPTOR;
typedef struct _IMAGE_THUNK_DATA
{
    union
    {
         PBYTE ForwarderString;
         PDWORD Function;     //被导入的函数的入口地址
         DWORD Ordinal;       // 该函数的序数
         PIMAGE_IMPORT_BY_NAME AddressOfData;   // 一个RVA地址,指向IMAGE_IMPORT_BY_NAME
     }u1;
} IMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
     WORD    Hint;       //函数需序号
     BYTE    Name[1];    //函数名称
 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
if (!ReadFile(hFile, lpBaseAddress, dwFileSize, &dwNumberOfBytesRead, NULL))
{
    printf(“ReadFile:%d\n”, GetLastError());
    return FALSE;
}
PrintImportTable(lpBaseAddress);
//获取导入表地址
DWORD Rav_Import_Table = pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR ImportTable = PIMAGE_IMPORT_DESCRIPTOR((ULONG_PTR)lpBaseAddress + Rav_Import_Table);
for (i = 0; memcmp(ImportTable + i, &null_iid, sizeof(null_iid)); i++){}
DllName = (LPCSTR)((ULONG_PTR)lpBaseAddress + ImportTable->Name);
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((ULONG_PTR)lpBaseAddress + ImportTable[i].OriginalFirstThunk);
//遍历同一个IID下的OriginalFirstThunk
for (j = 0; memcmp(pThunk + j, &null_thunk, sizeof(PIMAGE_THUNK_DATA)); j++){}
if (pThunk[j].u1.AddressOfData&IMAGE_ORDINAL_FLAG)   //按标号导入
{
    //
}
else   //按名称导入
{
    //
}
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD Characteristics;
    DWORD TimeDateStamp;            //输出表的创建时间
    WORD MajorVersion;                      //输出表的主版本号。未使用设置为0
    WORD MinorVersion;                      //输出表的次版本号。未使用设置为0
    DWORD Name;                //指向一个与输出函数关联的文件名的RVA
    DWORD Base;                //导出函数的起始序号
    DWORD NumberOfFunctions;        //导出函数的总数
    DWORD NumberOfNames;            //以名称导出的函数总数
    DWORD AddressOfFunctions;        //指向到处函数地址表的RVA
    DWORD AddressOfNames;            //指向函数名地址表的AVA
    DWORD AddressOfNameOrdinals;            //指向函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORYM, *pIMAGE_EXPORT_DIRECTORY
DWORD Rav_Export_Table = pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY ExportTable = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)lpBaseAddress + Rav_Export_Table);
//获取导出函数名数组
DWORD dwAddressOfNames =(DWORD)((ULONG_PTR)lpBaseAddress+ RvaToOffset(ExportTable->AddressOfNames,lpBaseAddress));
//获取导出函数数组
DWORD dwAddressOfFunctions = (DWORD)((ULONG_PTR)lpBaseAddress + RvaToOffset(ExportTable->AddressOfFunctions, lpBaseAddress));
//获取导出函数索引数组
DWORD dwAddressOfNameOrdinals = (DWORD)((ULONG_PTR)lpBaseAddress + RvaToOffset(ExportTable->AddressOfNameOrdinals, lpBaseAddress));
for (i = 0; i < dwNumberOfFunctions; i++){…}
if ((WORD)(dwAddressOfNameOrdinals + j * sizeof(WORD)) == i)
{}
//函数名称
FunName = (LPCSTR)((ULONG_PTR)lpBaseAddress + dwAddressOfNames[j  sizeof(WORD)]);  //VA值
//函数索引
FunOrdinal = (WORD)(dwAddressOfNameOrdinals + j  sizeof(WORD));
//函数地址   i=(dwAddressOfNameOrdinals + j  sizeof(WORD))   其实是索引值
//这里需要取其值,注意(DWORD)
FunAddress = (DWORD)(dwAddressOfFunctions + FunOrdinal  sizeof(DWORD));
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;//RVA
    DWORD   SizeOfBlock;   //重定位数据大小
    WORD    TypeOffset;    // 重定位项数组
} IMAGE_BASE_RELOCATION,* PIMAGE_BASE_RELOCATION;
//重定位表地址
DWORD RelocTableRva = pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
//    printf(“\t\t []RelocTableRva:%p\n”, RelocTableRva);
PIMAGE_BASE_RELOCATION RelocTable =(PIMAGE_BASE_RELOCATION)((ULONG)lpBaseAddress+RvaToOffset(RelocTableRva,lpBaseAddress));
printf(“\t\t []RelocTable:%p\n”, RelocTable);
DWORD VirtualAddress = RelocTable->VirtualAddress;
printf(“\t\t [*]VirtualAddress:%p”, VirtualAddress);
DWORD Cout = (RelocTable->SizeOfBlock - 8) / 2;
WORD RecAddr = (WORD)((BYTE*)RelocTable + 8);
//取第三位地址,并加上VirtualAddress才是真的RVA
DWORD offset = VirtualAddress + (RecAddr[j] & 0x0FFF);
//TYPE
DWORD type = RecAddr[j] >> 12;
printf(“\t\t Type:[%d] \t RVA:[%p]\n”, type, offset);
RelocTable = (IMAGE_BASE_RELOCATION )((BYTE )RelocTable + RelocTable->SizeOfBlock);
```