猪年送安康,祝你们新一年健康、快乐。愿你们都作一个勤奋努力、真诚奉献的人,幸运会永远的眷顾大家。
引子:
某一天饶有兴趣在卡饭上浏览着帖子,故事的相遇就那么简单。当时一条评论勾起个人好奇心,那么好逆向开始。
根据个人习惯,拿到样本我会线上恶意代码分析,直接拉到virustotal之类的网站上,看看是否已经被大多数杀毒软件所能识别,看一些有价值的数据,以下图所示:
图片一:基本信息
当看到这个页面时候,看到最后的分析日期是18年11月,又看了一下导出表的函数信息,是一款老病毒。根据各大厂商对这个病毒行为特性、分析定位为特洛伊、假装等,定位不一很正常......,其实兴趣下降了一大半,并非新鲜品种,但不能这样侮辱一个病毒!接着习惯性拉入到IDA中,当我看到熟悉的汇编以后,以下图所示:
图片二:GetProcAddress实现
当点进去其中的一个函数,看到了fs寄存器,且一大堆比较复杂的操做,看到熟悉的汇编指令之后,心中已有定数,这是一个本身实现的GetProcAddress函数。
c++
理论篇 | 汇编篇 |
---|---|
保护模式,定时器,PE杂谈 | 手动实现GetProcAddress函数及Hash加密字符比对 |
1、理论篇
先来看病毒样本中的一段代码,以下图所示:
图片三:CreateTimerQueueTimer
还记着之前分析熊猫烧香时候的定时器,以下图所示:
图片四:SetTimer
恶意代码大多都会利用到WinAPI提供的定时器操做,从而实现有规划、周期性的恶意代码,既然那么重要,因此咱们先来聊聊那些定时器。
常常用ARK工具的朋友,应该都使用过遍历定时器相关的功能,有用户层定时器,IO定时器,DCP定时器,包括咱们的时钟中断机制,都是具备定时器相关操做的。
咱们先从用户层入手,windbg下深刻分析一下上面提到的两个定时器操做,NtSetTimer汇编源码以下所示:
注:(为何SetTimer会调用NtSetTimer,请看https://blog.51cto.com/13352079/2343452)
函数原型以下:算法
UINT_PTR SetTimer( HWND hWnd, // 窗口句柄 UINT_PTR nIDEvent, // 定时器ID,多个定时器时,能够经过该ID判断是哪一个定时器 UINT nElapse, // 时间间隔,单位为毫秒 TIMERPROC lpTimerFunc // 回调函数 );
为了更好的理解定时器的汇编代码,简单分析一下函数调用的过程,就是如何获取当前线程。数据库
kd> u PsGetCurrentProcess nt!PsGetCurrentProcess: mov eax,dword ptr fs:[00000124h] mov eax,dword ptr [eax+50h] ret
保护模式:
那么根据书籍或者相关资料,咱们知道fs寄存器的值恒定(注意windows7 32位测试的),内核态是fs = 0x30,用户态 fs = 0x3B,fs在内核态指向_KPCR,用户态指向_TEB.。什么依据呢?凭什么说fs指向KPCR? 这里属于保护模式得内容,可是这里仍是想与你们一块儿分享其中的原理,那么先说说段寄存器,为了方便理解作了一个简陋的图,以下所示:
图片五:段寄存器
其实段寄存器共96位,只有其中的16位是可见的,剩余部分隐藏,可见的部分就是咱们能查询到的当即数,也叫作选择子。隐藏部分只能够被CPU操做,不可使用指令进行操做。
GDT全局描述符表,系统中按照不一样的属性、类型进行描述,因此这些描述符统一存储到内存中,而且造成了一个数组,这就是GDT。全局描述符的索引保存在了可见部分16位的选择子中,这就是GDT与段选择子的关联。如何从选择子中知道索引呢?以下图所示:
图片六:选择子
高13位是索引号,也就是下标。TI = 0 表明GDT,TI = 1表明LDT。RPL是当前请求特权级别,权限检查会用到,这里不对权限检测作详细介绍。
清楚了上面的知识后,咱们分析一下内核态fs = 30,16位选择子内容,以下图所示:
图片七:解析fs寄存器
经过上述分解,咱们知道了fs在GDT中的第六项(0开始),接着获取gdtr,而且获取段描述符的属性状态,以下图所示:
图片八:gdtr寄存器
段描述符如何来分解?段描述符都有那些属性呢?以下图所示:编程
图片九:通用描述符
介绍一些主要属性:
windows
L | D/B | P | S | DPL | TYPE | G |
---|---|---|---|---|---|---|
64位代码段 | 默认操做大小 | 段有效值 | 描述符类型 | 描述符特权级别 | 段类型 | 粒度 |
咱们按照上图分解,取Base Address,按照想对应的规则10101100 01001000 10000100 01000000进行地址拼接,其实这个就获取到了KPCR的结构。
fs寄存器其实拥有那么的数据量,本质是是从结构数据中获取,便于操做。推荐一下bochs这款x86硬件平台的开源模拟器,学习保护模式,除了书中获取相关知识之外,还能够多多阅读源码,才能更深层的学习理解。
回到主题,咱们既然知道fs在内核态指向的是什么了,咱们观察一下fs:[00000124h]是什么?结构体相关内容之前介绍过,这里不罗嗦,以下图所示:数组
图片十:_KPRC
fs寄存器内核态指向的是_KPRC,fs:[0x124]指向CurrentThread(_EPROCESS),有了这些基础之后,咱们继续分析NtSetTimer得调用过程。
NtSetTimer汇编代码:(由于排版 因此就上图了)
图片十一:NtSetTimer解析1
如上图所示,先是获取_ETHREAD,而后获取了ETHREAD+0x13a(Previous Mode),以下图所示:
图片十二:网络
什么是Previous Mode?,简单来讲调用Nt或Zw版本时,系统调用机制将调用线程捕获到内核模式,断定参数是否来源于用户模式标志。app
The native system services routine checks the PreviousMode field of the calling thread to determine whether the parameters are from a user-mode source.
详细得内容介绍参考:https://msdn.microsoft.com/zh-cn/windows/desktop/ff559860
PreviousMode其中得两个状态值:
一、UserMode 状态码是1
二、KernelMode 状态码是0ide
定时函数分析:
因此上图中与0进行判断,判断当前是否内核态,是则跳转0x8402fdd。咱们先来看看若是是内核态,是怎样一条执行路线,以下图所示:
图片十三:定时器ID断定
第二个参数必须大于等于0,不然会抛出异常,继续看,以下图所示:
图片十四:内核态汇编解析
OD中咱们跟中一下看是否真的追加了第五个参数,以下图所示:
图片十五:NtUserSetTimer函数
若是为0则跳转,跳转位置以下图所示:
图片十六:ExpSetTimer
咱们会发现,SetTimer->NtUserSetTimer->Wow64得函数(若是32位运行在64位)-->KiFastSystemCall->ExSetTimer-->ObReferenceObjectByHandle-->..........
因此SetTimer在内核态得过曾仍是比较复杂得,你们能够经过函数栈来观察到底如何运做得,这告诉咱们一个道理,谁HOOK得函数越底层,谁就有可能作更多得事情。
若是Previous Mode = UserMode呢?如何执行?以下图所示:
图片十七:用户态汇编分析
在作了一些判断赋值及参数保存操做之后,又跳回了与内核态执行得流程,因此说不论怎样最终还会调用那些函数。
关于SetTimer函数简单得分析到这里,咱们下面接着看CreateTimerQueueTimer函数,先来看函数原型:
BOOL WINAPI CreateTimerQueueTimer( _Out_ PHANDLE phNewTimer, _In_opt_ HANDLE TimerQueue, _In_ WAITORTIMERCALLBACK Callback, _In_opt_ PVOID Parameter, _In_ DWORD DueTime, _In_ DWORD Period, _In_ ULONG Flags ); 图三中已经对参数进行了详细得介绍,这里再也不作介绍
OD中咱们动态观察一下,以下图所示:
图片十八:CreateTimerQueueTimer
函数内部调用了RtlCreateTimer,咱们继续动态跟踪,以下所示:
内部调用了大量的函数,其中包括TpSetTimer也在其中,基本肯定内部是调用TpSetTimer来实现该函数功能,在windbg中简答了分析一下,内部调用了TppTimerpSet,且使用了Slim读写锁机制,由于触碰到了盲区,感受不太准确,也找不到相关的参考因此有兴趣的朋友能够深刻分析一下,这里就不讲解了。
图片十九:TppTimerpSet
这里以上是给你们提供一些函数分析的思路罢了,有时间的话写一篇相关的话题一块儿讨论一下。
PE杂谈 :
关于PE知识虽然看起来杂乱,但仍是比较有序的。PE涉猎的范围较广,PE文件是指一种格式,如可执行文件、动态连接库、驱动等等,都属于PE格式的文件。
想深刻学习的朋友,推荐一本书籍《Windows PE权威指南》,里面内容是win32汇编撰写而成。
咱们这里只对用到的基本知识和导出表作介绍,PE结构体大概分为几个部分,以下图所示:
图片二十:PE大致结构
上面顺序是必定的,PE是一个有序结构,标准的PE格式每一个结构体对应的偏移是固定的,固然也有不少恶意代码会对PE结构体进行数据压缩等技术,达到隐匿、免杀的目的。
咱们介绍一下DOS头的数据介绍,其实咱们用VS编程的时候就能够获取到结构体,这里再也不windbg下获取了,以下所示:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
上面结构体是DOS头部的所有信息,其中DOS中两个重要属重点介绍一下:
e_magi |
---|
“魔术”标志,判断是否PE格式第一道防线,恒定值为0x4D5A(MZ) |
e_lfanew |
---|
Dos头与NT头之间有一部分Dos Stub的数据(Dos的数据)大小不肯定,意味着NT头偏移不肯定,因此 e_lfanew记录了该模块NT的偏移 |
如何找到NT头?模块基址 + e_lfanew = NT的位置。第二部分咱们会用汇编获取且深刻学习,用C/C++如何实现呢?以下代码所示:
// 1.获取PE格式文件 m_strNamePath = PathName; // 2.打开文件 HANDLE hFile = CreateFile(PathName, GENERIC_READ | GENERIC_WRITE, FALSE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if ((int)hFile <= 0){ AfxMessageBox(L"当前进程有可能被占用或者意外错误"); return FALSE; } HANDLE hFile = NULL; // 3.获取文件大小 DWORD dwSize = GetFileSize(hFile, NULL); // 4.申请堆空间 PuPEInfo::m_pFileBase = (void *)malloc(dwSize); memset(PuPEInfo::m_pFileBase, 0, dwSize); DWORD dwRead = 0; OVERLAPPED OverLapped = { 0 }; void* pFileBaseAddress = nullptr; // 5.读取文件到内存 int nRetCode = ReadFile(hFile, pFileBaseAddress, dwSize, &dwRead, &OverLapped); // 6.转换成DOS头结构体 PIMAGE_DOS_HEADER pDosHander = (PIMAGE_DOS_HEADER)pFileBaseAddress; // 7.Dos起始地址 + e_lfanew = NT头 PIMAGE_NT_HEADERS pHeadres = (PIMAGE_NT_HEADERS)(pDosHander->e_lfanew + (LONG)pFileBaseAddress);
如上述代码,获取可执文件路径,建立(获取文件句柄)、打开文件、读取文件大小、申请堆空间、读取文件数据到内存(加载到了内存)、获取NT头,第7步正式上述所表达的 模块基址 + e_lfanew。
NT头内部是如何?以下所示:
图片二十一:NT结构
如上所示,NT分为三部分,介绍以下:
Signature | FileHeader | OptionalHeader |
---|---|---|
标记,判断是否PE格式第二道防线,恒定值为0x4550(PE) | 文件头,存储这PE文件的基本信息 | 存储着关于PE文件的附加信息 |
既然已经介绍了PE格式两条应规定,两道标杆,若是判断是不是一个PE格式的文件呢?以下代码所示:
//断定是不是PE文件 BOOL IsPE(char* lpBase) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase; if (pDos->e_magic != IMAGE_DOS_SIGNATURE/*0x4D5A*/) { return FALSE; } PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase); if (pNt->Signature != IMAGE_NT_SIGNATURE/*0x4550*/) { return FALSE; } return TRUE; }
FileHeader结构体以下:
// File header format. typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine | NumberOfSections | TimeDateStamp | NumberOfSymbols |
---|---|---|---|
文件运行平台 | 区段的数量 | 文件建立时间 | 符号个数 |
SizeOfOptionalHeader | PointerToSymbolTable | Characteristics |
---|---|---|
扩展头大小 | 符号表偏移 | PE文件属性 |
补充:
一、Machine:0x014c表明i386,平时intel32为平台,0x0200表示Intel 64为平台。
二、NumberOfSymbols:这个很重要了,你遍历节表先要获取数量,这个就是。
三、Characteristics:PE的文件属性值,以下所示:
数值 | 介绍 | 宏定义 |
---|---|---|
0x0001 | 从文件中删除重定位信息 | IMAGE_FILE_RELOCS_STRIPPED |
0x0002 | 可执行文件 | IMAGE_FILE_EXECUTABLE_IMAGE |
0x0004 | 行号信息无 | IMAGE_FILE_LINE_NUMS_STRIPPED |
0x0008 | 符号信息无 | IMAGE_FILE_LOCAL_SYMS_STRIPPED |
0x0010 | 强制性缩减工做 | IMAGE_FILE_AGGRESIVE_WS_TRIM |
0x0020 | 应用程序能够处理> 2GB的地址 | IMAGE_FILE_LARGE_ADDRESS_AWARE |
0x0080 | 机器字的字节相反的 | IMAGE_FILE_BYTES_REVERSED_LO |
0x0100 | 运行在32位平台 | IMAGE_FILE_32BIT_MACHINE |
0x0200 | 调试信息从.DBG文件中的文件中删除 | IMAGE_FILE_DEBUG_STRIPPED |
0x0400 | 若是文件在可移动媒体上,则从交换文件复制并运行。 | IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP |
0x0800 | 若是在网络存储介质中,则从交换文件中复制并运行。 | IMAGE_FILE_NET_RUN_FROM_SWAP |
0x1000 | 系统文件 | IMAGE_FILE_SYSTEM |
0x2000 | DLL文件 | IMAGE_FILE_DLL |
0x4000 | 单核CPU运行 | IMAGE_FILE_UP_SYSTEM_ONLY |
0x8000 | 机器字的字节相反的 | IMAGE_FILE_BYTES_REVERSED_HI |
OptionalHeader结构体介绍:
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
挑重点介绍一下:
Magic | AddressOfEntryPoint | BaseOfData |
---|---|---|
标志一个文件什么类型 | 程序入口点RVA | 起始数据的相对虚拟地址(RVA) |
ImageBase | SizeOfImage | SizeOfHeaders |
---|---|---|
默认加载基址0x400000 | 文件加载到内存后大小(对齐后) | 全部头部大小 |
NumberOfRvaAndSizes | DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] | SizeofStackReserve |
---|---|---|
数据目录个数(通常是0x10) | 数据目录表 | 栈可增加大小 |
补充:
一、文件中的数据是0x200对齐的(FileAlinment),内存中是以0x1000对齐的(SectionAlignment),对齐什么意思?打个比方,假如从0开始,数据只占用了0x88字节,那么下一段数据会在0x200开始,中间填充0。
二、DataDirectory这是一个数组,IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16。因此共有16项,每一项对于整个执行程序来讲都有特殊的意义,固然不是每一个程序每一项数据表都有内容。下面咱们介绍的导出表,即是这16项中的第1项,下标为0。
那么DataDirectory是什么样结构呢?以下所示:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每个数组都保存了这样的一个结构体指针,VirtualAddress是什么?就是相对虚拟地址RVA,而Size意味着数据的大小。
术语介绍:
**虚拟地址**: 在一个程序运行起来的时候,会被加载到内存中,而且每一个进程都有本身的4GB,这个4GB叫作**虚拟地址**,由物理地址映射过来的,4GB的空间,并无所有被用到。 **物理地址**:在物理内存中存在的地址。在windows中是没有表现出来的,由于windows使用了保护模式。 **全部的数据都存储在了相应的区段(节)**,rdata存储只读数据,data存储的全局数据,text存储的代码,rsrc存储的是资源。 **入口点(OEP)**:他保存的是一个 **RVA** ,而后使用 OEP + Imagebase == 入口点的VA,一般状况下,OEP指向的不是main函数,是一个用于初始化(实际加载地址) **加载基址**:默认由PE文件指定,可是一般开启随机基址后,它的位置是由系统指定的 **镜像大小**: 就是exe在文件中展开以后的大小, = 最后一个区段的RVA + 最后一个区段的size 再按照0x1000对齐。 **代码/数据基址**:第一个代码区段和第一个数据区段的RVA **虚拟地址(VA)**:在进程4GB中所处的位置。 **相对虚拟地址(RVA)**:相对于内存(映像)中<u>加载基址</u>的一个偏移, **文件偏移(FOA)**:相对于文件(镜像)起始位置的偏移。 **文件块对齐:** 0x200(512),一个区段在文件的大小必须是0x200的倍数 **内存块对齐:**0x1000(4kb),一个区段在内存中的大小必须是0x1000的倍数 **关系:** 数据段(有效数据长度是0x100) => 文件对齐 => (0x200) => 映射到内存 => 0x1000 文件对齐力度和内存对齐力度能够本身改变,可是文件对齐力度必须不大于内存对齐力度 **标志字:**标识可运行的平台,x86,x64 **子系统**:窗口WinMain,控制台main **特征值**: 对应的是文件头中的Characteristics,标识当前模块有哪些属性(重定位已分离=>动态基址) **可选头的大小**:可选头有多少个字节,和操做系统的位数有关,x86/x64
节表就再也不这里过多的介绍,说说导出表,也就是数据目录表的第1项,下标为0。
导出表是干什么的?PE文件导出的供其余使用的函数、变量等行为。当查找导出函的时候,可以方便快捷找到函数的位置。
看一看导出表的结构体,以下所示:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
图片二十一:Export Format
Characteristics | TimeDateStamp | MajorVersion | NumberOfFunctions |
---|---|---|---|
保留值, 为0 | 时间 | 主版本号 | 函数数量 |
MinorVersion | Name | Base | NumberOfNames |
---|---|---|---|
次版本号 | PE名称 | 序号基数 | 函数名称数量 |
AddressOfFunctions | AddressOfNames | AddressOfNameOrdinals |
---|---|---|
函数地址表RVA | 函数名称表RVA | 函数序号表RVA |
补充:
导出表通常会被安排到.edata中,通常也都合并到.rdata中。上述中有三个字段分别是AddressOfFunctions,AddressOfNames和AddressOfNameOrdinals,对应着三张表,上面三个字段保存了相对虚拟地址,且有关联性,下面来看一下三个表的关联性,以下所示:
图片二十二:Table关联
如上图所示,序号表与名称表一一对应,下标与下标中存储的值是相关联的,这三张表设计巧妙,利用了关系型数据库的概念。
须要注意的是,序号不是有序的,并且会有空白。地址表中有些没有函数名,也就是地址表有地址却没法关联到名称表中,这时候用序号调用,序号内容加上Base序号基址才是真正的调用号,且注意序号表是两个字节WORD类型。
了解这三张表以后,C/C++代码实际应用获取一下,代码以下:
// lpBase就是读取文件申请的缓冲区(把文件读到内存后的首地址) // 1. 找到导出表 PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpBase; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + lpBase); PIMAGE_DATA_DIRECTORY pDir = &pNt->OptionalHeader.DataDirectory[0]; DWORD dwExportFOA = RVAtoFOA(pDir->VirtualAddress); // 2. 导出表在文件中的位置 PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY) (dwExportFOA + lpBase); printf("模块名称%s\n", (RVAtoFOA(pExportTable->Name) + lpBase)); // 3. 获取函数数量 DWORD dwFunCount = pExportTable->NumberOfFunctions; // 3.1 获取函数名称数量 DWORD dwOrdinalCount = pExportTable->NumberOfNames; // 4. 获取地址表 DWORD* pFunAddr = (DWORD*)(RVAtoFOA(pExportTable->AddressOfFunctions) + lpBase); // 5. 获取名称表 DWORD* pNameAddr = (DWORD*)(RVAtoFOA(pExportTable->AddressOfNames) + lpBase); // 6. 获取序号表 WORD* pOrdinalAddr = (WORD*)(RVAtoFOA(pExportTable->AddressOfNameOrdinals) + lpBase); // 7. 循环遍历 for (DWORD i = 0; i < dwFunCount; i++) { // 7.1 若是为0说明是无效地址,直接跳过 if (pFunAddr[i] == 0) { continue; } // 7.2 遍历序号表中是否有此序号,若是有说明此函数有名字 BOOL bFlag = FALSE; for (DWORD j = 0; j < dwOrdinalCount; j++) { if (i == pOrdinalAddr[j]) { bFlag = TRUE; DWORD dwNameRVA = pNameAddr[j]; printf("函数名:%s,函数序号:%04X,函数序号:%04X\n", RVAtoFOA(dwNameRVA) + lpBase, i + pExportTable->Base); } } // 7.3 若是序号表中没有,说明此函数只有序号没有名字 if (!bFlag) { printf("函数名【NULL】,函数序号:%04X\n", i + pExportTable->Base); } }
上述代码是对导出表进行的遍历,上述中也许有一些细节性的知识表达的不够到位,若是你能对以上的知识都很熟悉且汇编还不错,那么用汇编获取函数导出表也许对你来讲是一件比较轻松的事情。
第二部分咱们一块儿学习一下如何用汇编手动获取函数名称表及对应的函数地址(上面三张表关系必定搞清楚),用汇编实现本身的GetProcAddress,且Hash加密字符串进行与名称表进行对比,理论知识先告一段落。
2、汇编篇:
经过理论篇的阅读,熟悉了如何使用C/C++(其余语言思路不变)来获取且遍历导出表,那么如图二,当分析一段恶意代码或者正向代码,咱们发现这些汇编指令如何去作?IDA中转换成C语言?其实我不多使用IDA中的转换,应为看汇编与看c差距并非特别大,特别对于算法,想要还原规则及代码,汇编最为真实可靠。固然若是说有大量工做需求,没有太多时间去研究,只是对部分规则,逻辑进行分析造成报告,那么就另说了......
上面介绍了保护模式相关内容及fs寄存器,分析了内核态的fs:[0x124],那么用户态fs:[0x30]呢?,以下图所示:
图片二十三:TEB
图片二十四:PEB
什么是TEB什么是PEB呢?在之前的博客中介绍过一些相关的内容,这里在简单的说一说。
TEB(Thread Environment Block),线程环境块,也就是说每个线程都会有TEB,用于保存系统与线程之间的数据,便于操做控制,经过理论篇述保护模式知识能够本身分析一下,用户态取fs寄存器的段描述符的BaseAddress拼接后地址为TEB地址,之前的NT类系统上地址是固定的,每4KB是一个TEB,经过分解的段描述符,内存中是向下扩展。
PEB(Process Environment Block),进程环境块,保存进程相关的信息,一样每一个进程都是由本身的进程信息的。
获取PEB有那些途径?
一、fs寄存器偏移+0x30 PEB的地址
二、EPROCESS+0x1a8 PEB地址
以上偏移不是必定的根据环境而定,经过以上咱们两种方式咱们在编程中就能够轻易的找到PEB了。
图片二十四中,PEB结构体中标红了+0x00c偏移处,指向的是一个_PEB_LDR_DATA的结构体,以下所示:
kd> dt _PEB_LDR_DATA nt!_PEB_LDR_DATA +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr32 Void +0x00c InLoadOrderModuleList : _LIST_ENTRY +0x014 InMemoryOrderModuleList : _LIST_ENTRY +0x01c InInitializationOrderModuleList : _LIST_ENTRY +0x024 EntryInProgress : Ptr32 Void +0x028 ShutdownInProgress : UChar +0x02c ShutdownThreadId : Ptr32 Void
这个结构意味着什么?其实就是包含有关进程的已加载模块的信息。并且微软给他标记了This structure may be altered in future versions of Windows,此结构可能会在Windows的将来版本中更改。咱们在windbg下(windwos7 32bit)与官网查询到的结构体成员数量不同,以下所示:
typedef struct _PEB_LDR_DATA { BYTE Reserved1[8]; PVOID Reserved2[3]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
前两个参数只给了一样的介绍,Reserved for internal use by the operating system,供系统内部使用,而第三个参数则是一个双向链表头部,包含进程的已加载模块。 列表中的每一个项目都是指向LDR_DATA_TABLE_ENTRY结构的指针。
在windbg下+0x00c,+0x014,+0x01c三个都是双线链表有什么不一样呢?
InLoadOrderModuleList | InMemoryOrderModuleList | InInitializationOrderModuleList |
---|---|---|
模块加载顺序 | 模块在内存中的顺序 | 模块初始化装载顺序 |
LDR_DATA_TABLE_ENTRY是怎样一个双向链表呢?以下所示:
图片二十五:关联
LDR_DATA_TABLE_ENTRY结构体,以下所示:
图片二十六:LDR_DATA_TABLE_ENTRY
代码中会用到如下属性,简单理解以下,其实一个驱动的加载过程这个结构体很重要:
DLLBase | FullDllName | BaseDllName |
---|---|---|
模块基址 | 文件路径 | 模块名称 |
汇编如何获取呢?以下图所示:
图片二十七:获取DLLBase
补充:上面一段汇编代码,咱们经过fs获取了PEB,经过PEB偏移+0x0C获取_PEB_LDR_DATA,加上偏移+0x1c是InInitializationOrderModuleList为双向链表进行的遍历。
接着获取了字符串,而后经过Hash比对,注意模块名称存储是宽字符,比对成功获取DLLBase基地址,咱们能够遍历获取想要的模块基址如krnel32.dll等。
PE获取:
PE如何用c++获取导出表且遍历,理论篇已给出完整代码。汇编如何实现呢?对于标准的PE来
说,相对于基址偏移是必定的以下:
0x3c | 0x78 |
---|---|
PE标头 | 导出目录表的相对虚拟地址(RVA) |
以下图所示:
图片二十八:获取Export Table
由于是汇编来实现操做,关键的步骤都写到了注释当中,下面贴上完整的汇编代码,实现函数以下:
puGetModule | puGetProcAddress |
---|---|
获取模块基址,参数1:Hash值 | 获取函数地址 参数1:模块基址,参数2:Hash值 |
关于Hash值的算法,你们能够逆向一下下面代码中的汇编代码,用c语言实现一下,贴出本代码中测试使用的Hash值,以下:
0xec1c6278; kernel32.dll 0xc0d832c7; LoadlibraryExa 0x4FD18963; ExitPorcess 0x5644673D User32.dll 0x1E380A6A MessageBoxA 0x9EBC86B RtlExitUserProcess 0xF4E2F2C8 GetModuleHandleW 0xBB7420F9 CreateSolidBrush 0xBC05E48 RegisterClassW
puGetModule汇编代码以下:
DWORD puGetModule(const DWORD Hash) { DWORD nDllBase = 0; __asm{ jmp start /*函数1:遍历PEB_LDR_DATA链表HASH加密*/ GetModulVA: push ebp; mov ebp, esp; sub esp, 0x20; push edx; push ebx; push edi; push esi; mov ecx, 8; mov eax, 0CCCCCCCCh; lea edi, dword ptr[ebp - 0x20]; rep stos dword ptr es : [edi]; mov esi, dword ptr fs : [0x30]; mov esi, dword ptr[esi + 0x0C]; mov esi, dword ptr[esi + 0x1C]; tag_Modul: mov dword ptr[ebp - 0x8], esi; // 保存LDR_DATA_LIST_ENTRY mov ebx, dword ptr[esi + 0x20]; // DLL的名称指针(应该指向一个字符串) mov eax, dword ptr[ebp + 0x8]; push eax; push ebx; // +0xC call HashModulVA; test eax, eax; jnz _ModulSucess; mov esi, dword ptr[ebp - 0x8]; mov esi, [esi]; // 遍历下一个 LOOP tag_Modul _ModulSucess : mov esi, dword ptr[ebp - 0x8]; mov eax, dword ptr[esi + 0x8]; pop esi; pop edi; pop ebx; pop edx; mov esp, ebp; pop ebp; ret /*函数2:HASH解密算法(宽字符解密)*/ HashModulVA : push ebp; mov ebp, esp; sub esp, 0x04; mov dword ptr[ebp - 0x04], 0x00 push ebx; push ecx; push edx; push esi; // 获取字符串开始计算 mov esi, [ebp + 0x8]; test esi, esi; jz tag_failuers; xor ecx, ecx; xor eax, eax; tag_loops: mov al, [esi + ecx]; // 获取字节加密 test al, al; // 0则退出 jz tag_ends; mov ebx, [ebp - 0x04]; shl ebx, 0x19; mov edx, [ebp - 0x04]; shr edx, 0x07; or ebx, edx; add ebx, eax; mov[ebp - 0x4], ebx; inc ecx; inc ecx; jmp tag_loops; tag_ends: mov ebx, [ebp + 0x0C]; // 获取HASH mov edx, [ebp - 0x04]; xor eax, eax; cmp ebx, edx; jne tag_failuers; mov eax, 1; jmp tag_funends; tag_failuers: mov eax, 0; tag_funends: pop esi; pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x08 start: /*主模块*/ pushad; push Hash; call GetModulVA; add esp, 0x4 mov nDllBase, eax; popad; } return nDllBase; }
puGetProcAddress函数以下:
DWORD puGetProcAddress(const DWORD dllvalues, const DWORD Hash) { DWORD FunctionAddress = 0; __asm{ jmp start // 自定义函数计算Hash且对比返回正确的函数 GetHashFunVA: push ebp; mov ebp, esp; sub esp, 0x30; push edx; push ebx; push esi; push edi; lea edi, dword ptr[ebp - 0x30]; mov ecx, 12; mov eax, 0CCCCCCCCh; rep stos dword ptr es : [edi]; // 以上开辟栈帧操做(Debug版本模式) mov eax, [ebp + 0x8]; // ☆ kernel32.dll(MZ) mov dword ptr[ebp - 0x8], eax; mov ebx, [ebp + 0x0c]; // ☆ GetProcAddress Hash值 mov dword ptr[ebp - 0x0c], ebx; // 获取PE头与RVA及ENT mov edi, [eax + 0x3C]; // e_lfanew lea edi, [edi + eax]; // e_lfanew + MZ = PE mov dword ptr[ebp - 0x10], edi; // ☆ 保存PE(VA) // 获取ENT mov edi, dword ptr[edi + 0x78]; // 获取导出表RVA lea edi, dword ptr[edi + eax]; // 导出表VA mov[ebp - 0x14], edi; // ☆ 保存导出表VA // 获取函数名称数量 mov ebx, [edi + 0x18]; mov dword ptr[ebp - 0x18], ebx; // ☆ 保存函数名称数量 // 获取ENT mov ebx, [edi + 0x20]; // 获取ENT(RVA) lea ebx, [eax + ebx]; // 获取ENT(VA) mov dword ptr[ebp - 0x20], ebx; // ☆ 保存ENT(VA) // 遍历ENT 解密哈希值对比字符串 mov edi, dword ptr[ebp - 0x18]; mov ecx, edi; xor esi, esi; mov edi, dword ptr[ebp - 0x8]; jmp _WHILE // 外层大循环 _WHILE : mov edx, dword ptr[ebp + 0x0c]; // HASH push edx; mov edx, dword ptr[ebx + esi * 4]; // 获取第一个函数名称的RVA lea edx, [edi + edx]; // 获取一个函数名称的VA地址 push edx; // ENT表中第一个字符串地址 call _STRCMP; cmp eax, 0; jnz _SUCESS; inc esi; LOOP _WHILE; jmp _ProgramEnd // 对比成功以后获取循环次数(下标)cx保存下标数 _SUCESS : // 获取EOT导出序号表内容 mov ecx, esi; mov ebx, dword ptr[ebp - 0x14]; mov esi, dword ptr[ebx + 0x24]; mov ebx, dword ptr[ebp - 0x8]; lea esi, [esi + ebx]; // 获取EOT的VA xor edx, edx; mov dx, [esi + ecx * 2]; // 注意双字 获取序号 // 获取EAT地址表RVA mov esi, dword ptr[ebp - 0x14]; // Export VA mov esi, [esi + 0x1C]; mov ebx, dword ptr[ebp - 0x8]; lea esi, [esi + ebx]; // 获取EAT的VA mov eax, [esi + edx * 4]; // 返回值eax(GetProcess地址) lea eax, [eax + ebx]; jmp _ProgramEnd; _ProgramEnd: pop edi; pop esi; pop ebx; pop edx; mov esp, ebp; pop ebp; ret 0x8; // 循环对比HASH值 _STRCMP: push ebp; mov ebp, esp; sub esp, 0x04; mov dword ptr[ebp - 0x04], 0x00; push ebx; push ecx; push edx; push esi; // 获取字符串开始计算 mov esi, [ebp + 0x8]; xor ecx, ecx; xor eax, eax; tag_loop: mov al, [esi + ecx]; // 获取字节加密 test al, al; // 0则退出 jz tag_end; mov ebx, [ebp - 0x04]; shl ebx, 0x19; mov edx, [ebp - 0x04]; shr edx, 0x07; or ebx, edx; add ebx, eax; mov[ebp - 0x4], ebx; inc ecx; jmp tag_loop tag_end : mov ebx, [ebp + 0x0C]; // 获取HASH mov edx, [ebp - 0x04]; xor eax, eax; cmp ebx, edx; jne tag_failuer; mov eax, 1; jmp tag_funend; tag_failuer: mov eax, 0; tag_funend: pop esi; pop edx; pop ecx; pop ebx; mov esp, ebp; pop ebp; ret 0x08 start: pushad; push Hash; // Hash加密的函数名称 push dllvalues; // 模块基址.dll call GetHashFunVA; // GetProcess mov FunctionAddress, eax; // ☆ 保存地址 popad; } return FunctionAddress; }