上一篇贴文讲了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
的定义,藉此了解他的架构
MS-DOS的loader会根据此header来把执行档写入记忆体
以目前大部分的Windows系统来说,只会用到这个header里面的两个变数:
MZ
e_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
DOS Stub
--介绍--
从Microsoft的官方文件我们可以知道,DOS Stub是MS-DOS系统的执行档,只要是合法的MS-DOS程式都可以被塞到这个区块。而在没有特别指定的情况下,功能为印出"This program cannot be run in MS-DOS mode."这串错误讯息。
--小实验--
实际用DOSBox模拟看看在DOS系统底下执行PE执行档
操作步骤:
在C槽建一个新的资料夹test
把notepad
执行档複製到test
资料夹里面打开DOSBox,并把资料夹安装到虚拟环境里面的C槽 MOUNT C C:\test
进入刚安装的C槽并执行notepad
执行结果应证了文件所述,但我还是很好奇他到底是怎么运行的,于是决定来拆解他
--逆一下--
把DOS Stub分离出来会比较好做分析,底下是分离的步骤
我们先用PE Bear複製该程式片段
在读完档案后,点到DOS Stub区域,选取到Rich Header的offset之前,并把padding的部分删掉
选取之后按右键複製并贴到HxD,把档案存成dos_stub.exe
分离好之后就可以开始分析的环节了
我决定用radare2来操作,因为他很酷
在wsl环境下载完之后,就可以输入r2 dos_stub.exe
来开启档案。用pd
印出组合语之后,会发现显示的指令有点怪怪的,这是因为当时的处理器为16位元,与目前大家常用的64位元处理器指令集不一样
这边我们可以透过e asm.bits=16
修正成正确的指令。
好多了,但还是有地方怪怪的,程式不小心把字串解读成指令了。
修正的步骤也很简单,只要按V
进入hex mode,并按c
再按住shift
透过hjkl或上下左右键进行连续选取,把字串的部分全部框起来。(藉由刚刚的小实验,我们可以知道字串的部分是从This开始一路到最后面)
按d
显示选项,可以看到有非常多的设定方式,这边我们想要把选取的片段设成data,所以再按一次d
设定完成后按q
返回,并再试一次pd
印出组合语
根据之前的逆向经验,简单把程式分成三个部分
第一段是把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数值对应到的指令表
所以我们可以知道
第二段的功能就是把在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
用PE Bear也可以得到一样的结果
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