用常用注册表API构造目标指令:
与设计目标有关的汇编代码如下:
别小看这三行代码,如果通过手工修改,将这些代码人为地添加到目标 PE 中,难度还是相当大的。
模拟指令代码:(前面invoke指令分解目录中介绍步骤就是压栈,段内调用,无条件跳转)
细化三个invoke指令:
下面,对模拟的指令字节码进行细化,具体包括:
(1 ) 68xxXxXXXxXx push addr @hKey ;@只是一个汇编语言的标识符而已
如果数据在 .data 段中,而传递的是地址,比如 addr @hKey,则 push 的指令码为 68h,后面紧跟着 8 位的地址,该地址指示了数据所在位置的VA,即 @hKey 所在的 VA。
关于这个值的计算方法如下:
口获得程序默认装载地址 = 0x00400000
口获得数据段 .data 的起始 RVA = 0x03000
假设数据段的安排如下所示:(我还不知道这些sz1、sz2、sz3是哪里来的)
原始的内存字节码:
插入数据段后的内存字节码:
根据数据段各变量的位置,从上述所列字节码中找到@hKey所在的位置为0x00403069(就是红框框起的地方),所以第一条push指令字节码是 68 69 30 40 00。
(2) 68xxxXXXXxx push addr sz1l
采用和第 1 条指令相同的分析方法,获取注册表分支所在位置,最后得出第二条压栈指令。第二条指令字节码是 68 0B 30 40 00。
(3) 68xxxxXXXx push HKEY_LOCAL_MACHINE
因为 HKEY LOCAL _ MACHINE 是一个双字常量,所以压栈的指令字节码为68h,其后是该常量的值。打开 D:\masm32\includevwindows.inc 文件,找到该常量的定义语句:HKEY_LOCRL MACHINE equ 80000002h 得到第三条指令字节码是 68 02 00 00 80。
(4) E8xxxxxxxx call RegCreateKeyA
由于这是一个段内调用,所以 call 的指令字节码为E8,而其后跟随的则是距离真正跳转指令的偏移。那么,真正跳转的指令距离这个指令到底有多远呢? 可以通过以下方法计算出来。
首先,确定最初 HelloWorld.exe 的指令长度为 24h (通过前面知识获知指令在.text段处,也就是文件偏移400h,内存偏移0x402000处):
PS:为什么要计算原始跳转指令长度呢?我猜是新加入的指令要放在原始指令之前,但是jmp无条件跳转指令要在所有指令之后,也就是原始指令变成新加入的指令---原始指令---所有的JMP跳转指令。
其次,它要跳过自身后面的所有指令,如下所示:(最左边的是行数,不是指令长度)
这些指令加起来的长度为 26h,两者相加即为 call 到 jmp 的偏移量 4Ah : 所以,第四条指令的字节码是 E8 4A 00 00 00。
(5) 6A27 PUSH 27H
如果往栈里推人的双字可以控制在一个字节内,那么需要使用 6A 指令。后面直接跟一个字节的数值。
第五条指令的字节码是 6A 27。
使用以上分析方法,不难得出6一 9 条指令的字节码依次为:
第六条指令字节码是 68 42 30 40 00
第七条指令字节码是 6A 01
第八条指令字节码是 6A 00
第九条指令字节码是 68 39 30 40 00
(6) FF35 xxxxxxxx push @hKey
当要将数据段指定位置的双字值压人栈时,需要使用指令 FF35,其跟着指定位置的 VA。其实该指令可以解释为: push dword ptr ds:[xxxxxxxx] 。(ptr重写数据类型,这里意思是取ds段基址的[xxxxxxx]地址偏移处的值,而且取dword双子类型)
所以第十条指令字节码是 FF 35 69 30 40 00。
(7) E8 xxxxxxxx call RegSetValueExA
同第四条指令,该跳转的地址紧跟在第四条指令跳转的后面。现在计算操作数,长度由以下部分组成:
1) 该指令下的其他指令如下,总计 0Bh 个字节(6+5=11=0B)。
2) 原 Helloworld.exe 指令,总计 24h 个字节 。
3) 第四条指令的跳转指令 FF25 XXXXXXXX,总计 6h 个字节。
以上长度加起来以后的结果是 33h。所以第十一条指令字节码为 E8 35 00 00 00。
(8) FF35 xxxxxxxx Push @hKey
第十二条指令字节码是 FF 35 69 30 40 00。
(9) E8xxxxxxxx Call RegClose
同第四条指令,该跳转地址紧跟在第十一条指令后,其长度的计算包括以下几个部分:
口其后再无添加代码 (0h)
口原 Helloworld.exe 指令 (24h)
口第四条指令的跳转指令 FF 25 XX XX XX XX (6h)
口 第十一条指令的跳转指令 FF 25 XX XX XX XX (6h)
以上长度加起来以后的结果是 30h。第十三条指令字节为 E8 30 00 00 00。
(10) 跳转指令
跳转指令为 FF25,后面跟着要跳转到的 VA 地址。从前面对导入表的分析看,这三个函数的地址是 IAT 的前三个,也就是最后一个非零 IMAGE_IMPORT_DESCRIPTOR 结构中字段 FirstThunk 指向的位置。
其地址依次为 0402000、00402004、00402008,(IAT是jmp 操作数的集合,是数据目录项的第13个,是双字数组------>参照导入函数地址表(IAT目录))
所以,最后三个跳转指令依次为:
综上所述,新增加代码的字节码如下:
原始helloworld.exe代码再次放出来:
PE 头部变化:
附上PE头的图片做参考:
由于插入代码而使得 PE 头部发生变化之处见下表:
IMAGE_SECTION_HEADER(.data).VirtualSize:(节区的大小)
新增加的常量都定义在源代码的数据段(.data),主要包括以下内容:
共 98 个字节,所以 .data 节的尺寸要增加 62h。
IMAGE_DATA_DIRECTORYI[IAT] .isize:(IAT大小)
由于新增加了一个IMAGE _ IMPORT _DESCRIPTOR(因为三个函数都是一个动态链接库的),所以在 IAT 中要加入新增加的三个入口函数地址 ,同时增加一个结尾的0,共四个双字,16 个字节,故IMAGE_DATA_DIRECORY[IAT] .isize 要多加 10h。
IMAGE_DATA_DIRECTORYI[IT].VirtualAddress:(导入表项起始VA)
由于导入表的起始地址紧跟在 IAT 后,IAT 多增加的字节数也是导入表起始地址的后移数量,所以该字段要后移 16 个字节,即 IMAGE _ DATA_DIRECORY[IT].VirtualAddress 要多加 10h。
IMAGE_SECTION_HEADER(.rdata).VirtualSize:(导入表节区的大小)
1) 增加一个动态链接库,就增加一个 IMAGE_IMPORT_DESCRIPTOR 结构,大小为14h。
2) OriginalFirstThunk 部分,3 个新增注册表操作函数名指针和一个 0 结尾的指针共 10h。
3) “编号/名称”部分包含以下定义:
共计 61 个字节,即十六进制的3Dh。
4) FirstThunk 部分,3 个新增注册表函数名指针和一个 0 结尾的指针共 10h。
综上所述,IMAGE_SECTION_HEADER(.rdata).VirtualSize 的值需要多加 71h。
IMAGE_DATA_DIRECORY[IT].isize:(导入表项区域大小)
多了一个动态链接库,就多出一个 IMAGE_IMPORT _DESCRIPTOR 结构,该结构大小为14h。即 IMAGE _DATA_DIRECORY[IT] .isize 多加 14h。
IMAGE_SECTION_HEADER(.text).VirtualSize:
代码大小前面已分析过,增加了 4Ch 大小。
开始手工重组:
完成了指令字节码构造,明确了 PE 头部需要更改的字段,接下来就是手工重组部分。由于新的代码和数据与原来的代码和数据加在一起没有超出文件对齐要求的粒度,所以DOS头、PE 头的大部分字段不需要修改。下面就逐项列出手工重组过程中需要修改的地方和修改的具体方法。
数据段修改:
首先添加程序中用到的数据。定位到 HelloWorld.exe 文件的 800h 处,采用覆盖的方式修改成以下数据:
这些数据总共为 98 个字节,即十六进制的 62H。
然后修改与数据段有关的字段。定位 IMAGE _ SECTION_ HEADER(.data).VirtualSize,并将原来的值 0000000Bh 更改为 0000006Dh:
由于目前只修改了数据段内容,并没有破坏 PE 文件的结构,所以到现在为止修改后的HelloWorld.exe 还是可以正常运行的。
代码段修改:
首先看前面细化三个Invoke指令处理的结果来作为后面添加的代码:
原代码段位于 0400h 处,其字节码如下:
对代码段的修改需要将新增加的指令字节码加入到该段中,最终变成以下内容:
加入代码以后,原来 HelloWorld.exe 框起来的部分内容也发生了变化,变化对应下图的黑色箭头指向。发生变化的部分是调用函数的入口地址。如果不健忘,这个位置的数据应该指向 IAT。当导入表发生变化以后,新产生的 IA 被安排在 IAT 的头部,所以其他的调用地址必须往后调整。
在程序中,一共增加了三个函数 (这三个函数均来自同一个动态链接库),所以按照导入表的要求,在 IAT 表头需要增加三个函数的入口地址(12 个字节) 和一个全0 (4 个字节) 的地址。这样,原来的入口函数地址都要往后调整 10h 个字节,所以在 HelloWorld.exe 的原代码中两个地址均增加了 10h。
修改完代码段以后要注意对齐,以保证下面的 0600h 处起始为 .rdata 的数据。
现在来修改与代码段有关的字段。定位 IMAGE_SECTION_HEADER(.text).VirtualSize,并将原来的值 00000024h 更改为 00000070h:
由于代码发生了变更,而代码中涉及的函数调用地址等还没有完全构造好,所以这时候一定不要去测试,运行 HelloWorld.exe 程序是不会成功的。
.rdata段修改:
由于涉及对导入表的重组,所以对这个段的修改是最复杂的,回顾一下双桥结构:
修改前,先看一下导入表数据的原始字节码:
修改前先划分出要修改的部分和占用的大小:
(1) IAT,即 FirstThunk 部分 (4*4+2*4+2*4=20h)
(2) 导入表项部分 (20*3+20=50h )
(3) INT,即 OriginalFirstThunk 部分 (4*4+2*4+2*4=20h )
(4) 函数编号、函数名以及动态链接库名部分
以上列出了导入表的四个组成部分,只要知道了代码中调用函数的个数以及链接库的个数,INT、导入表项、IAT 三个部分的大小就是固定的了。事实上,导人表的重组只需要先将第 4 部分也就是函数编号、函数名和动态链接库这一部分根据编码设置好,剩下的其他部分的数据就可以按照数据结构自行构造了。
开始修改.rdata段:
首先修改好第 4 部分的函数编号、函数名以及动态链接库名区域:
然后根据源代码把第 4 部分编排好,然后构造一张动态链接库及调用函数的地址表,内容见下表:
注意:
无论是原来 HelloWorld 的字节码还是新加入的字节码,涉及的 VA 都需要重新计算。
根据上表VA的内容,完成导入表其他三个部分的字节码构造。先来看第IAT部分和第导入表项部分字节码的构造:
第 1 部分:4*4+2*4+2*4=20h (桥2指向的IAT):
指向链接库导入所有函数且以双字的0结尾
第 2 部分:20*3+20=50h(导入表项):
红框的桥1和相隔5个字节的蓝框桥2,这里共三个链接库,所以三个IMAGE_IMPORT_DESCRIPTOR 加1个同样大小的全0结构,所以范围从620h~660h。
第 3 部分:4*4+2*4+2*4=20h(桥1指向的INT):
指向链接库导入所有函数且以双字的0结尾,未加载前和桥2的IAT一样
最后修改与数据目录表项和节表项相关字段:
定位 IMAGE_SECTION_HEADER(rdata).VirtualSize,并将原来的值 00000092h 更改为00000103h。(导入表节区的大小)
定位 IMAGE_DATA_DIRECORY[IT] .VirtualAddress,并将原来的值 00002010h 更改为 00002020h。(导入表项起始地址)
定位 IMAGE _DATA_DIRECORY[IT],isize,并将原来的值 0000003ch 更改为 00000050h。(导入表项大小)
定位 IMAGE_DATA_DIRECORY[IAT],isize,并将原来的值 00000010h 更改为 00000020h。(IAT大小)
注意:经过以上修改,可能会导致节“.rdata”的数据没有按照内存对齐粒度对齐,请注意查看。
错误排障:
运行后发现只是注册表被更改,而后面的弹出对话框没有出来。
回到程序字节码部分,看看调用函数 MessageBox 和函数 ExitProcess 时使用的地址,由于我们手工生成了IAT,在生成的时候并没有考虑到这两个函数的调用顺序。所以,只需要在字节码中,将代码段涉及这两个函数的指令字节码中的 jmp 的地址调换一下即可。
第一部分中IAT的图,610h处是MessageBoxA函数,618h是ExitProcess函数:
事实上,IAT 总是按照函数名或函数编号的升序进行排列的,所以在导入表重组时一定要考虑这个问题,不然就会出现程序运行错误。
重新运行一次,可以正常弹框,下图是被修改的注册表启动项:(我本机上不知道为什么实现不了,在windows7系统上反而有360拦截记录可查)
程序实现:
刚才的工作现在让链接器来实现:下面的程序代码实现添加注册表启动项的功能代码:
编译链接生成最终的可执行程序,其运行效果和手工修改以后的 NewHelloWorld.exe 是完全一样的。
使用PEComp 工具把刚才改造的NewHelloworld.exe 与 HelloWorld2.exe 进行对比:
会发现两个程序实现的功能虽然一样,但还是有很多不一样的地方,比如位于节“.rdata”中的导入表数据部分。节“rdata”的 IMAGE_SECTION_HEADER2.VirtualSize是不一样的,链接器在定义函数 RegSetValueExA 的名字时后面多加了一个“\0”。凡是函数名字符串的字符个数为偶数,则后面跟两个“\0”,如果函数名后面为奇数,则后面跟 1 个
“\0"。“\0”的个数是为了保持 2 个字节对齐而设置的。
最后总结思考:
思考: 关于IAT 的连贯性:
IAT 必须是连续的吗? 答案是否定的。
从对代码段的分析来看,只要跳转指令 FF25 跳转到正确的位置,导入表的 FirstThunk 字段指针指向正确的位置,则 IAT 可以不连续。
(1) 代码段的跳转指令
将其中对有关函数调用的地址从 004020XX 更改为 004021xx,如下所示:;
附上上面的IAT图,修改的是青色框部分:
(2)导入表部分:
为了使改动最小,将原来位于文件偏移 600h 的新增加的 IAT 移动到700h 处,并将导入表的数据结构的最后一个非0 的 FirstThunk (桥2)指针,修改为 00002110,即下面加框的部分。
附上上面原来的桥1桥2图,修改的是最后一个蓝框:
结果:
文件偏移量 600h 处的 16 个字节和偏移量 710h 处的 16 个字节共同组成了 IAT,该程序依旧可以被装载并正确运行。
那么如果更改了IAT 的大小情况会怎样呢? 数据目录中 IAT 的大小部分可以任意更改,起始位置 RVA 也可以更改,但不能将位置改动到有其他属性的节中,不然会出现如图 4-15 所示的错误。
思考: 关于导入表的位置:
导入表通常在常量节(.rdata)里,但这并不是定理。我们可以把它放到代码节里,或其他的任何可读属性的节里。
比如,将 HelloWorld2.exe中的导入表数据放和人代码段的空闲位置。即文件偏移0x000004B0 处,如下所示:
导入表原来位置的数据替换为 0 或不用都可以。
根据FOA 和了RVA 的转换关系,得出导入表新位置在内存中的RVA 值为 000010BOh:
附上上面原来修改PE头的值得地方:
修改数据目录表中导入表项的字段IMAGE_SECTION_HEADER(rdata).VirtualSize(导入表节区的大小)为新的RVA 值,如下所示:
导入表转移到代码段后程序依旧可以很好地运行: