从病毒开始聊聊那些windows下大杂烩

猪年送安康,祝大家新一年健康、快乐。愿大家都做一个勤奋努力、真诚奉献的人,幸运会永远的眷顾你们。

引子:
 某一天饶有兴趣在卡饭上浏览着帖子,故事的相遇就那么简单。当时一条评论勾起我的好奇心,那么好逆向开始。
 根据我的习惯,拿到样本我会线上恶意代码分析,直接拉到virustotal之类的网站上,看看是否已经被大多数杀毒软件所能识别,看一些有价值的数据,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片一:基本信息
 当看到这个页面时候,看到最后的分析日期是18年11月,又看了一下导出表的函数信息,是一款老病毒。根据各大厂商对这个病毒行为特性、分析定位为特洛伊、伪装等,定位不一很正常......,其实兴趣降低了一大半,并不是新鲜品种,但不能这样侮辱一个病毒!接着习惯性拉入到IDA中,当我看到熟悉的汇编之后,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二:GetProcAddress实现
 当点进去其中的一个函数,看到了fs寄存器,且一大堆比较复杂的操作,看到熟悉的汇编指令以后,心中已有定数,这是一个自己实现的GetProcAddress函数。

理论篇汇编篇
保护模式,定时器,PE杂谈手动实现GetProcAddress函数及Hash加密字符比对


一、理论篇
 先来看病毒样本中的一段代码,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片三:CreateTimerQueueTimer

 还记着以前分析熊猫烧香时候的定时器,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片四: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? 这里属于保护模式得内容,但是这里还是想与大家一起分享其中的原理,那么先说说段寄存器,为了方便理解做了一个简陋的图,如下所示:
从病毒开始聊聊那些windows下大杂烩
                  图片五:段寄存器

其实段寄存器共96位,只有其中的16位是可见的,剩余部分隐藏,可见的部分就是我们能查询到的立即数,也叫做选择子。隐藏部分只可以被CPU操作,不可以使用指令进行操作。
 GDT全局描述符表,系统中按照不同的属性、类型进行描述,所以这些描述符统一存储到内存中,并且形成了一个数组,这就是GDT。全局描述符的索引保存在了可见部分16位的选择子中,这就是GDT与段选择子的关联。如何从选择子中知道索引呢?如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片六:选择子

高13位是索引号,也就是下标。TI = 0 代表GDT,TI = 1代表LDT。RPL是当前请求特权级别,权限检查会用到,这里不对权限检测做详细介绍。
 清楚了上面的知识后,我们分析一下内核态fs = 30,16位选择子内容,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片七:解析fs寄存器

 通过上述分解,我们知道了fs在GDT中的第六项(0开始),接着获取gdtr,并且获取段描述符的属性状态,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片八:gdtr寄存器
 段描述符如何来分解?段描述符都有那些属性呢?如下图所示:

从病毒开始聊聊那些windows下大杂烩

                  图片九:通用描述符

介绍一些主要属性:

LD/BPSDPLTYPEG
64位代码段默认操作大小段有效值描述符类型描述符特权级别段类型粒度


 我们按照上图分解,取Base Address,按照想对应的规则10101100 01001000 10000100 01000000进行地址拼接,其实这个就获取到了KPCR的结构。
 fs寄存器其实拥有那么的数据量,本质是是从结构数据中获取,便于操作。推荐一下bochs这款x86硬件平台的开源模拟器,学习保护模式,除了书中获取相关知识以外,还可以多多阅读源码,才能更深层的学习理解。

 回到主题,我们既然知道fs在内核态指向的是什么了,我们观察一下fs:[00000124h]是什么?结构体相关内容以前介绍过,这里不罗嗦,如下图所示:
从病毒开始聊聊那些windows下大杂烩

                  图片十:_KPRC
 fs寄存器内核态指向的是_KPRC,fs:[0x124]指向CurrentThread(_EPROCESS),有了这些基础以后,我们继续分析NtSetTimer得调用过程。
NtSetTimer汇编代码:(因为排版 所以就上图了)
从病毒开始聊聊那些windows下大杂烩
                  图片十一:NtSetTimer解析1
 如上图所示,先是获取_ETHREAD,然后获取了ETHREAD+0x13a(Previous Mode),如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十二:

 什么是Previous Mode?,简单来说调用Nt或Zw版本时,系统调用机制将调用线程捕获到内核模式,判定参数是否来源于用户模式标志。

 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其中得两个状态值
 1、UserMode 状态码是1
 2、KernelMode 状态码是0

定时函数分析:
 所以上图中与0进行判断,判断当前是否内核态,是则跳转0x8402fdd。我们先来看看如果是内核态,是怎样一条执行路线,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十三:定时器ID判定
 第二个参数必须大于等于0,否则会抛出异常,继续看,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十四:内核态汇编解析
 OD中我们跟中一下看是否真的追加了第五个参数,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十五:NtUserSetTimer

 如果为0则跳转,跳转位置如下图所示:
从病毒开始聊聊那些windows下大杂烩
从病毒开始聊聊那些windows下大杂烩
从病毒开始聊聊那些windows下大杂烩
                  图片十六:ExpSetTimer
 我们会发现,SetTimer->NtUserSetTimer->Wow64得函数(如果32位运行在64位)-->KiFastSystemCall->ExSetTimer-->ObReferenceObjectByHandle-->..........
 所以SetTimer在内核态得过曾还是比较复杂得,大家可以通过函数栈来观察到底如何运作得,这告诉我们一个道理,谁HOOK得函数越底层,谁就有可能做更多得事情。
 如果Previous Mode = UserMode呢?如何执行?如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十七:用户态汇编分析
 在做了一些判断赋值及参数保存操作以后,又跳回了与内核态执行得流程,所以说不论怎样最终还会调用那些函数。
 关于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中我们动态观察一下,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片十八:CreateTimerQueueTimer
 函数内部调用了RtlCreateTimer,我们继续动态跟踪,如下所示:
从病毒开始聊聊那些windows下大杂烩
从病毒开始聊聊那些windows下大杂烩
 内部调用了大量的函数,其中包括TpSetTimer也在其中,基本确定内部是调用TpSetTimer来实现该函数功能,在windbg中简答了分析一下,内部调用了TppTimerpSet,且使用了Slim读写锁机制,因为触碰到了盲区,感觉不太准确,也找不到相关的参考所以有兴趣的朋友可以深入分析一下,这里就不讲解了。
从病毒开始聊聊那些windows下大杂烩
                  图片十九:TppTimerpSet
 这里以上是给大家提供一些函数分析的思路罢了,有时间的话写一篇相关的话题一起讨论一下。

PE杂谈 :
 关于PE知识虽然看起来杂乱,但还是比较有序的。PE涉猎的范围较广,PE文件是指一种格式,如可执行文件、动态链接库、驱动等等,都属于PE格式的文件。
 想深入学习的朋友,推荐一本书籍《Windows PE权威指南》,里面内容是win32汇编撰写而成。
 我们这里只对用到的基本知识和导出表做介绍,PE结构体大概分为几个部分,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十: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头内部是如何?如下所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十一:NT结构
如上所示,NT分为三部分,介绍如下:

SignatureFileHeaderOptionalHeader
标记,判断是否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;
MachineNumberOfSectionsTimeDateStampNumberOfSymbols
文件运行平台区段的数量文件创建时间符号个数
SizeOfOptionalHeaderPointerToSymbolTableCharacteristics
扩展头大小符号表偏移PE文件属性

补充:
 1、Machine:0x014c代表i386,平时intel32为平台,0x0200表示Intel 64为平台。
 2、NumberOfSymbols:这个很重要了,你遍历节表先要获取数量,这个就是。
 3、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
0x2000DLL文件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;

挑重点介绍一下:

MagicAddressOfEntryPointBaseOfData
标志一个文件什么类型程序入口点RVA起始数据的相对虚拟地址(RVA)
ImageBaseSizeOfImageSizeOfHeaders
默认加载基址0x400000文件加载到内存后大小(对齐后)所有头部大小
NumberOfRvaAndSizesDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]SizeofStackReserve
数据目录个数(一般是0x10)数据目录表栈可增长大小

补充:
 1、文件中的数据是0x200对齐的(FileAlinment),内存中是以0x1000对齐的(SectionAlignment),对齐什么意思?打个比方,假如从0开始,数据只占用了0x88字节,那么下一段数据会在0x200开始,中间填充0。
 2、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

CharacteristicsTimeDateStampMajorVersionNumberOfFunctions
保留值, 为0时间主版本号函数数量
MinorVersionNameBaseNumberOfNames
次版本号PE名称序号基数函数名称数量
AddressOfFunctionsAddressOfNamesAddressOfNameOrdinals
函数地址表RVA函数名称表RVA函数序号表RVA

补充:
 导出表一般会被安排到.edata中,一般也都合并到.rdata中。上述中有三个字段分别是AddressOfFunctions,AddressOfNames和AddressOfNameOrdinals,对应着三张表,上面三个字段保存了相对虚拟地址,且有关联性,下面来看一下三个表的关联性,如下所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十二: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加密字符串进行与名称表进行对比,理论知识先告一段落。


二、汇编篇:
 通过理论篇的阅读,熟悉了如何使用C/C++(其他语言思路不变)来获取且遍历导出表,那么如图二,当分析一段恶意代码或者正向代码,我们发现这些汇编指令如何去做?IDA中转换成C语言?其实我很少使用IDA中的转换,应为看汇编与看c差距并不是特别大,特别对于算法,想要还原规则及代码,汇编最为真实可靠。当然如果说有大量工作需求,没有太多时间去研究,只是对部分规则,逻辑进行分析形成报告,那么就另说了......
 上面介绍了保护模式相关内容及fs寄存器,分析了内核态的fs:[0x124],那么用户态fs:[0x30]呢?,如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十三:TEB

从病毒开始聊聊那些windows下大杂烩
                  图片二十四:PEB
 什么是TEB什么是PEB呢?在以前的博客中介绍过一些相关的内容,这里在简单的说一说。
TEB(Thread Environment Block),线程环境块,也就是说每一个线程都会有TEB,用于保存系统与线程之间的数据,便于操作控制,通过理论篇述保护模式知识可以自己分析一下,用户态取fs寄存器的段描述符的BaseAddress拼接后地址为TEB地址,以前的NT类系统上地址是固定的,每4KB是一个TEB,通过分解的段描述符,内存中是向下扩展
PEB(Process Environment Block),进程环境块,保存进程相关的信息,同样每个进程都是由自己的进程信息的。
获取PEB有那些途径?
1、fs寄存器偏移+0x30 PEB的地址
2、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三个都是双线链表有什么不同呢?

InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList
模块加载顺序模块在内存中的顺序模块初始化装载顺序

LDR_DATA_TABLE_ENTRY是怎样一个双向链表呢?如下所示:

从病毒开始聊聊那些windows下大杂烩
                  图片二十五:关联

LDR_DATA_TABLE_ENTRY结构体,如下所示:

从病毒开始聊聊那些windows下大杂烩

                  图片二十六:LDR_DATA_TABLE_ENTRY

代码中会用到以下属性,简单理解如下,其实一个驱动的加载过程这个结构体很重要:

DLLBaseFullDllNameBaseDllName
模块基址文件路径模块名称

汇编如何获取呢?如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十七:获取DLLBase
补充:上面一段汇编代码,我们通过fs获取了PEB,通过PEB偏移+0x0C获取_PEB_LDR_DATA,加上偏移+0x1c是InInitializationOrderModuleList为双向链表进行的遍历。
 接着获取了字符串,然后通过Hash比对,注意模块名称存储是宽字符,比对成功获取DLLBase基地址,我们可以遍历获取想要的模块基址如krnel32.dll等。
PE获取:
 PE如何用c++获取导出表且遍历,理论篇已给出完整代码。汇编如何实现呢?对于标准的PE来
说,相对于基址偏移是一定的如下:

0x3c0x78
PE标头导出目录表的相对虚拟地址(RVA)

如下图所示:
从病毒开始聊聊那些windows下大杂烩
                  图片二十八:获取Export Table

 因为是汇编来实现操作,关键的步骤都写到了注释当中,下面贴上完整的汇编代码,实现函数如下:

puGetModulepuGetProcAddress
获取模块基址,参数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;
}

转载于:https://blog.51cto.com/13352079/2348907

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值