关键词:APC,蓝屏,QueueUserAPC,ZwQueueApcThread,漏洞

曾在看雪上看到一篇 【】Ring3下把Windows XP SP2 弄蓝了…… 的帖子。
出于好奇,改了段QueueUserAPC的Delphi的代码(见该贴的5楼),结果SP3下也蓝屏了:




也许是太简单了,苦等不见高手回复。正好在学漏洞分析这门课,就拿这个问题来练手了 。
最后分析发现,其实这个蓝屏原因很简单QueueUserAPC的调用上范了初级错误,APC插csrss后执行出错,导致csrss被OS中止掉了。 


虽然简单,不过重要的是分析过程,所以在这里记录一下。分析中难免有错,高手飘过...

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

由于出问题的API是QueueUserAPC,于是尝试着用OllyDbg跟入检查原因。可以看到,QueueUserAPC实际是通过调用ntdll!ZwQueueApcThread实现的。不过ZwQueueApcThread没办法进一步跟进去:

代码:
7C92E23D >  B8 B4000000     MOV EAX,0B4             ; ZwQueueApcThread
7C92E242    BA 0003FE7F     MOV EDX,7FFE0300
7C92E247    FF12            CALL DWORD PTR DS:[EDX] ; ntdll.KiFastSystemCall
7C92E249    C2 1400         RETN 14
SS:
0012FF30   7C834174  返回到 kernel32.7C834174 来自 ntdll.ZwQueueApcThread

7C92EB8B >  8BD4            MOV EDX,ESP            ; ntdll.KiFastSystemCall
7C92EB8D    0F34            SYSENTER
SYSENTER之后进入Ring0,OD是拿它没办法的。
好在实际测试发现,并不是SYSENTER进入后就马上蓝屏,也就是说出问题的地方并不是ntdll.KiFastSystemCall中。


由于不会SoftICE,所以没法进一步跟踪,看来只能从DUMP文件入手。
在“启动和故障恢复”选项卡中,选择核心内存转储方式(图2),然后运行引起蓝屏的程序,让Windows输出蓝屏时的内存DUMP:



这样,蓝屏后就会将高端内存输出到C:\WINDOWS\MEMORY.DMP中。用WinDBG打开DMP文件,!analyze -v进行分析(详细分析方法见二楼),发现该蓝屏的产生是系统关键进程CSRSS被意外终止所导致的。据此可以初步断定,由于ZwQueueApcThread插入APC队列使csrss.exe被终止,而不是最早插入APC的SYSTEM和SMSS.EXE这两个进程。

看来不了解APC的原理是无法继续分析了,我们先来看看什么是APC。APC即异步过程调用,是(Asynchronous Procedure Call)的缩写。这里直接引用《谈谈对APC的一点理解》中的原话:
引用:
1)  APCs允许用户程序和系统元件在一个进程的地址空间内某个线程的上下文中执行代码。
2)  I/O管理器使用APCs来完成一个线程发起的异步的I/O操作。
3)  使用APC可以得到或者设置一个线程的上下文和挂起线程的执行。

在NT中,有两种类型的APCs:用户模式和内核模式。……用户模式的APCs需要目标线程处在Alertable等待状态才能被成功的调度执行。对于用户模式下,可以调用SleepEx, WaitForSingleObjectEx等API使目标线程处于Alertable等待状态,从而让用户模式APCs执行。当一个用户模式APC被投递到一个线程,调用上面的等待函数,如果返回等待状态STATUS_USER_APC,在返回用户模式时,内核转去控制APC例程,当APC例程完成后,再继续线程的执行。
简单来说,APC通过 Alertable I/O 提供更有效的异步通知形式,当IO请求完成后,一旦线程进入可告警状态(Alertable),我们在APC队列中设置的回调函数将会执行,此时我们将获得目标线程的控制权直至返回。

APC在Windows系统中应用相当广泛,实际上TerminateThread就是利用APC实现的。使用TerminateThread可以在任何时候结束指定线程。查阅微软支持库Q254956(http://support.microsoft.com/kb/254956/en-us/)可知,调用TerminateThread会在目标进程中产生一个APC队列,然后强制所有线程进入等待状态,用于执行APC队列中的ExitThread函数,从而结束目标进程。这是一种典型的APC应用,由于使用APC来执行退出进程操作,这就导致了被TerminateThread结束的线程不能回到自身的清理函数中,这也是APC的一个特性。[/url]
APC方式执行的回调函数,实际上是中断目标进程转入指定回调程序执行。这样的好处在于不会有新的线程产生,而且可以控制目标线程的执行流程。返回目标进程领空也比较简单,由于APC调用前已经把返回地址压入堆栈中,只需适时的执行一条ret指令便可以返回,而无需像挂钩子函数或者远程线程中,执行繁琐的清理操作。

这么一来就清楚了。一旦我们劫持csrss.exe中的某个线程,让他去执行一些非法操作,于是csrss.exe就壮烈牺牲了。


有人说了,将CallBack地址赋值LoadLibraryA也会引起蓝屏?首先我要说的是,表1中那段APC代码实际是错的,光调用一个QueueUserAPC函数就错了两个参数,也正是因为写得有问题才引起的目标程序访问违规:
引用:
完整代码点这里
pStrDll := PAnsiChar(dllName);
QueueUserAPC(@LoadLibraryA, hThread, DWORD(pStrDll));
由Delphi的调用规范可知,参数1(@LoadLibraryA)是取LoadLibraryA的地址。然而,这里取得的并非LoadLibraryA的真正地址,而是一个JMP指令(不清楚的可以用OD跟踪一下程序中的API调用)。正确方法应该是通过GetProcAddress取得LoadLibraryA函数入口,这样才能继续。当然,参数3(DWORD(pStrDll))是另一个错误所在,将pStrDll这个DLL名字指针强制转成DWORD型是没有错误,问题在于这个pStrDll所指内容是在本地进程中的,要是在其他进程里访问这个地址,内容肯定不一样。
是不是很熟悉,QueueUserAPC这个API调用其实和CreateRemoteThread很像,只不过其根本不同在于CreateRemoteThread要创建新的线程用于执行我们的目标代码,而QueueUserAPC不用。


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

另外说说网上一些关于APC注入的误区。APC注入参数1不一定非要是LoadLibraryA,其他API也是可行的(网上教程都用LoadLibrary是因为载入DLL实现起来比较简单而且通用)。APC的可以帮助我们执行指定位置的任意代码,只要把QueueUserAPC参数1的指针指向要执行的ShellCode,然后等待系统处理这个用户模式APC就行了。

当然,搬网上某些APC注入教程的原话,“有时你等得花儿都谢了APC还没有执行”。确实如此,只有当目标线程调用了SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等API时,用户模式APC才能执行,这也是大多数线程插入APC后没有反应的原因。就目前来看,公认的方法是通过暴力注入explorer.exe的所有线程来达到目标效果。实际explorer中,通常都会有这样两个线程等着你去插入:
代码:
ntdll!RtlQueueWorkItem+0x2b5
ntdll!RtlDowncaseUnicodeString+0x75
这两个进程可以通过Sysinternals的Process Explorer的线程列表进行查看,他们会不断的进入Alertable等待状态,所以可以顺利执行ShellCode。

  • 标 题:WinDbg Crash Dump Analysis
  • 作 者:木桩
  • 时 间:2009-01-13 17:18

这是注入CSRSS并且LoadLibraryA一个显示对话框的DLL引起的蓝屏截图,相关代码见此楼附件:



同样的使用WinDbg分析,可以得到如下信息:

引用:
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

Use !analyze -v to get detailed debugging information.

BugCheck 50, {f0006c74, 0, bf838c27, 0}

Probably caused by : memory_corruption

Followup: memory_corruption
---------
kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced.  This cannot be protected by try-except,
it must be protected by a Probe.  Typically the address is just plain bad or it
is pointing at freed memory.
Arguments:
Arg1: f0006c74, memory referenced.
Arg2: 00000000, value 0 = read operation, 1 = write operation.
Arg3: bf838c27, If non-zero, the instruction address which referenced the bad memory address.
Arg4: 00000000, (reserved)

Debugging Details:
------------------
READ_ADDRESS:  f0006c74 

FAULTING_IP: 
win32k!NtUserGetDCEx+2c
bf838c27 8b7108          mov     esi,dword ptr [ecx+8]

MM_INTERNAL_CODE:  0
DEBUG_FLR_IMAGE_TIMESTAMP:  0

FAULTING_MODULE: bf800000 win32k

DEFAULT_BUCKET_ID:  CODE_CORRUPTION

BUGCHECK_STR:  0x50

PROCESS_NAME:  csrss.exe

TRAP_FRAME:  f9c7ccd8 -- (.trap 0xfffffffff9c7ccd8)
ErrCode = 00000000
eax=e14254c0 ebx=bf838c46 ecx=f0006c6c edx=0055ee64 esi=0055ee70 edi=f9c7cd64
eip=bf838c27 esp=f9c7cd4c ebp=f9c7cd50 iopl=0     vif nv up ei pl zr na pe nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00090246
win32k!NtUserGetDCEx+0x2c:
0008:bf838c27 8b7108          mov     esi,dword ptr [ecx+8] ds:0023:f0006c74=????????
Resetting default scope

LAST_CONTROL_TRANSFER:  from 805256fb to 805349ae

STACK_TEXT:  
f9c7cc74 805256fb 00000050 f0006c74 00000000 nt!KeBugCheckEx+0x1b
f9c7ccc0 804e2ff1 00000000 f0006c74 00000000 nt!MmAccessFault+0x6f5
f9c7ccc0 bf838c27 00000000 f0006c74 00000000 nt!KiTrap0E+0xcc
f9c7cd50 804e006b 00000000 00000000 00000003 win32k!NtUserGetDCEx+0x2c
f9c7cd50 7c92eb94 00000000 00000000 00000003 nt!KiFastCallEntry+0xf8
0055ee58 77d1f229 77d3b22a 00000000 00000000 ntdll!KiFastSystemCallRet
0055f110 77d3b12b 0055f26c 0055f340 0174fb14 USER32!NtUserGetDCEx+0xc
0055f260 77d65fdf 0055f26c 00000028 00000000 USER32!MessageBoxWorker+0x2ba
0055f2b8 77d50574 00000000 0174a1ac 0174a1a8 USER32!MessageBoxTimeoutW+0x7a
0055f2d8 77d6615b 00000000 0174a1ac 0174a1a8 USER32!MessageBoxExW+0x1b
0055f2f4 0174a19f 00000000 0174a1ac 0174a1a8 USER32!MessageBoxW+0x45
WARNING: Stack unwind information not available. Following frames may be wrong.
0055f350 7c9211a7 01740000 00000001 00000000 32+0xa19f
0055f370 7c93cbab 0174b158 01740000 00000001 ntdll!LdrpCallInitRoutine+0x14
0055f478 7c936178 00000000 c0150008 00000000 ntdll!LdrpRunInitializeRoutines+0x344
0055f724 7c9362da 00000000 00163910 0055fa18 ntdll!LdrpLoadDll+0x3e5
0055f9cc 7c801bb9 00163910 0055fa18 0055f9f8 ntdll!LdrLoadDll+0x230
0055fa34 7c801d6e 7ffdcc00 00000000 00000000 KERNEL32!LoadLibraryExW+0x18e
0055fa48 7c801da4 016e0000 00000000 00000000 KERNEL32!LoadLibraryExA+0x1f
0055fa64 7c8341cc 016e0000 0055fac0 00000000 KERNEL32!LoadLibraryA+0x94

0055faac 7c92eac7 7c801d77 016e0000 00000000 KERNEL32!BaseDispatchAPC+0x50
0055fff4 00000000 00000000 000000c8 0000012a ntdll!KiUserApcDispatcher+0x7


STACK_COMMAND:  kb

MODULE_NAME: memory_corruption

IMAGE_NAME:  memory_corruption

FOLLOWUP_NAME:  memory_corruption

MEMORY_CORRUPTOR:  LARGE

FAILURE_BUCKET_ID:  MEMORY_CORRUPTION_LARGE

BUCKET_ID:  MEMORY_CORRUPTION_LARGE

Followup: memory_corruption
---------

kd> lmvm win32k
start    end        module name
bf800000 bf9c0200   win32k     (pdb symbols)          D:\Program Files\Symbols\win32k.pdb\D48935A134F249CDA985C45051FBF0462\win32k.pdb
    Loaded symbol image file: win32k.sys
    Image path: \SystemRoot\System32\win32k.sys
    Image name: win32k.sys
    Timestamp:        Wed Aug 04 14:17:30 2004 (41107F7A)
    CheckSum:         001C663B
    ImageSize:        001C0200
    File version:     5.1.2600.2180
    Product version:  5.1.2600.2180
    File flags:       0 (Mask 3F)
    File OS:          40004 NT Win32
    File type:        3.7 Driver
    File date:        00000000.00000000
    Translations:     0804.04b0
    CompanyName:      Microsoft Corporation
    ProductName:      Microsoft(R) Windows(R) Operating System
    InternalName:     win32k.sys
    OriginalFilename: win32k.sys
    ProductVersion:   5.1.2600.2180
    FileVersion:      5.1.2600.2180 (xpsp_sp2_rtm.040803-2158)
    FileDescription:  Multi-User Win32 Driver
    LegalCopyright:   (C) Microsoft Corporation. All rights reserved.
可见这个BSOD是由于内存访问违规引起的。由于win32k.sys中的这条指令,读取了0xf0006c74的内容,而0xf0006c74不再内存中,于是引起了蓝屏。
0008:bf838c27 8b7108   mov esi,dword ptr [ecx+8]      ; ds:0023:f0006c74=????????

回想这个蓝屏发生的原因,由于LoadLibrary了一个cmdShell到csrss.exe的第二个线程中,结果就出现了这个蓝屏。查看蓝屏前的堆栈,可以看到这样三行:
0055fa34 7c801d6e 7ffdcc00 00000000 00000000 KERNEL32!LoadLibraryExW+0x18e
0055fa48 7c801da4 016e0000 00000000 00000000 KERNEL32!LoadLibraryExA+0x1f
0055fa64 7c8341cc 016e0000 0055fac0 00000000 KERNEL32!LoadLibraryA+0x94
这说明了LoadLibraryA确实被执行了。也就是说如果我们写一段ShellCode到CSRSS中,就可以利用CSRSS的权限作我们想做的事情。至于注入CSRSS所需要的权限,例如管理员、SeDebugPrivilege等,参见其他注入教程中关于OpenThread和OpenProcess的部分。

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

附件 APC_src.rar 说明
\APC注入_D2009\AnyAPC_Boom.rar              里面是APC注入的主程序,用法见源码目录下的readme.txt
\注射用DLL\多线程cmdshell\s32.dll           改成32.dll丢到Windows安装目录中,注入后监听3820端口,可以Telnet试

相关的程序源码都在里面,自己看吧。另外,建议先扫一遍毒再运行
上传的附件 APC_src.rar[解压密码:pediy]