231解剖室:PE File Format - Part 1 头部

上一篇贴文讲了PE的架构,这篇会从PE的头部下刀
深入了解DOS Header、DOS Stub
并且写一个小parser来读取Rich Header的资料

DOS Header (aka MS-DOS Header)

--介绍--
为一段长度为64 bytes的区段,位在PE档案的开头,为了让现在的执行档可以向下相容MS-DOS而存在。如果没有此片段,则无法在MS-DOS上执行。

我们可以从winnt.h找到IMAGE_DOS_HEADER的定义,藉此了解他的架构
http://img2.58codes.com/2024/20156936cxS5sQIBzE.png

MS-DOS的loader会根据此header来把执行档写入记忆体
以目前大部分的Windows系统来说,只会用到这个header里面的两个变数:

e_magic : DOS Header的signature(magic number),用来辨识此为MS-DOS执行档,ASCII的值为MZe_lfanew : DOS Header中的最后一个变数,offset为0x3c,可以透过这个变数得知NT Headers的位置
// 透过DOS Header找到NT Headers的位置IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)fbuf; // fbuf == pointer to data read from fileIMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((size_t)dosHeader + dosHeader->e_lfanew);

PE Bear的DOS Hdr pane
http://img2.58codes.com/2024/20156936LbNs4RcC8E.png

DOS Stub

--介绍--
http://img2.58codes.com/2024/20156936R5RIt3mS6q.png
从Microsoft的官方文件我们可以知道,DOS Stub是MS-DOS系统的执行档,只要是合法的MS-DOS程式都可以被塞到这个区块。而在没有特别指定的情况下,功能为印出"This program cannot be run in MS-DOS mode."这串错误讯息。

--小实验--

实际用DOSBox模拟看看在DOS系统底下执行PE执行档

操作步骤:

在C槽建一个新的资料夹 testnotepad执行档複製到test资料夹里面打开DOSBox,并把资料夹安装到虚拟环境里面的C槽 MOUNT C C:\test进入刚安装的C槽并执行notepad

http://img2.58codes.com/2024/20156936GzjxnhmVh5.png

执行结果应证了文件所述,但我还是很好奇他到底是怎么运行的,于是决定来拆解他

--逆一下--

把DOS Stub分离出来会比较好做分析,底下是分离的步骤

我们先用PE Bear複製该程式片段
在读完档案后,点到DOS Stub区域,选取到Rich Header的offset之前,并把padding的部分删掉
http://img2.58codes.com/2024/20156936tiHZ1Ms30E.png

选取之后按右键複製并贴到HxD,把档案存成dos_stub.exe
http://img2.58codes.com/2024/20156936hD4QgWTPoy.png

分离好之后就可以开始分析的环节了
我决定用radare2来操作,因为他很酷

在wsl环境下载完之后,就可以输入r2 dos_stub.exe来开启档案。用pd印出组合语之后,会发现显示的指令有点怪怪的,这是因为当时的处理器为16位元,与目前大家常用的64位元处理器指令集不一样
这边我们可以透过e asm.bits=16修正成正确的指令。
http://img2.58codes.com/2024/20156936efrLvTqtnC.png

好多了,但还是有地方怪怪的,程式不小心把字串解读成指令了。
http://img2.58codes.com/2024/20156936BXy1YpvQqO.png

修正的步骤也很简单,只要按V进入hex mode,并按c再按住shift透过hjkl或上下左右键进行连续选取,把字串的部分全部框起来。(藉由刚刚的小实验,我们可以知道字串的部分是从This开始一路到最后面)
http://img2.58codes.com/2024/20156936OblxnJGL8b.png

d显示选项,可以看到有非常多的设定方式,这边我们想要把选取的片段设成data,所以再按一次d
http://img2.58codes.com/2024/20156936JvYHB2m0jX.png

设定完成后按q返回,并再试一次pd印出组合语
http://img2.58codes.com/2024/20156936V6NeN1cVVv.png

根据之前的逆向经验,简单把程式分成三个部分

第一段是把data segment的值设定成code segment的值,可以看成ds = cs。

0000:0000    0e        push cs0000:0000    1f        pop ds

第二段与第三段都出现int 0x21的instruction

0000:0002    ba0e00    mov dx, 0xe0000:0005    b409      mov ah, 90000:0007    cd21      int 0x21
0000:0009    b8014c    mov ax, 0x4c010000:000c    cd21      int 0x21

根据DOS API的wiki,可以查到int 0x21是interrupt vector
透过设定ah的值来决定系统要採取什么行动,下图是ah数值对应到的指令表
http://img2.58codes.com/2024/20156936Gcf01KXurg.png

所以我们可以知道
第二段的功能就是把在0000:000e上面的资料("This program ...")印出来
第三段的功能就是以return value = 1的状态下终止程序

若想再深入了解DOS Stub,并尝试更改Stub的行为,可以看这里

Rich Header

--介绍--

介于DOS Stub与NT Headers之间的片段,用Visual Studio工具集把程式编译成执行档而生成的资讯,不是PE档案格式的一部分。2018年的Olympic Destroyer病毒就是透过更改此片段来使分析过程变得困难。详细资讯可以看这篇解析

Rich Header是由一整个chunk的加密讯息、一个singature以及一个4 bytes的XOR key所组成的。被加密的chunk由一个signature以及三个DWORD(4 bytes)大小的0作为开头,紧接着的是一连串的DWORD pair,每个DWORD pair可以解出三组数据,分别代表使用的编译工具(Product ID)、build ID、use count

--小实验--

写一个小parser当作练习,操作步骤:

透过DOS Header找到NT Headers的位置假设DOS Header跟DOS Stub的长度都固定 => Rich header从0x80开始检查Rich Header是否存在找到Rich signature,得到key的值用key把chunk分别解出来处理大小端,并把资讯印出来
// 透过DOS Header找到NT Headers的位置IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)fbuf;IMAGE_NT_HEADERS *ntHeaders = (IMAGE_NT_HEADERS *)((size_t)dosHeader + dosHeader->e_lfanew);
// 假设DOS Header跟DOS Stub的长度都固定 => Rich header从0x80开始// 检查Rich Header是否存在if (dosHeader->e_lfanew == 0x80) {        printf("Rich Header does not exist\n");        return;}
// 找到Rich signature,得到key的值memcpy(richbuf, fbuf + 0x80, richLen);tok = strstr(richbuf, "Rich");richOffset = tok - richbuf;if (tok == NULL) {        printf("Broken binary\n");        return;}memcpy(key, tok + 4, 4);
// 用key把chunk分别解出来for (int i = 0; i < richOffset; i++) {        output[i % 8] = richbuf[i] ^ key[i % 4];        if (i % 8 == 7) printHex(output);}
// 处理大小端,并把资讯印出来void printHex(char *output){        unsigned char *ptr;        unsigned char ldword[4], udword[4];        char uformatted[16], lformatted[16];        for (int i = 0; i < 4; i++) {                ptr = output + i;                ldword[4-i-1] = *ptr;                udword[4-i-1] = *(ptr + 4);        }        for (int i = 0; i < 4; i++) {                sprintf(lformatted + i * 2, "%02x", ldword[i]);                sprintf(uformatted + i * 2, "%02x", udword[i]);        }        printf("%s  %s - ", lformatted, uformatted);        if (strcmp(lformatted, "536e6144") == 0) {                printf("%-8s", "DanS");        } else {                printf("%d.%d.%d",                ldword[2] * 256 + ldword[3],                ldword[0] * 256 + ldword[1],                udword[2] * 256 + udword[3]);        }        printf("\n");        return;}

编译后去parse看看notepad.exe的rich header
http://img2.58codes.com/2024/20156936oVIAvEVsYE.png

用PE Bear也可以得到一样的结果
http://img2.58codes.com/2024/20156936zWtZ5Onyh2.png

richParser.c : source
(successfully built on Windows11 with gcc 11.2.0)

总结

我们提到了DOS Header的架构,并了解到这个Header对当前的Windows系统来说不是特别重要。
看完DOS Stub的官方文件之后我们决定开模拟器实际执行一次,接着用radare2把它拆了。
最后我们写了一个parser去把Rich Header这个神秘区段的资料捞出来,并用PE Bear验证。

下一篇文章,我们将会开始处理与现在Windows系统比较相关的NT Headers

References

0xRick's dive into the PE file format
How to end assembly correctly?
What's the purpose of PUSH CS / POP DS before a REP MOVSW?
winnt.h
radare2 meets com101
How To Run Dos Program in Windows 10 64 Bit using DOSBox Tutorial
Exploring the MS-DOS Stub


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章