|
作者:sunwear[E.S.T] shellcoder@163.com# q6 j/ X' ?, [8 _2 F9 L& A; S! v
来源:邪恶八进制 中国 % A/ `! h! G) C' b+ x
此文只能说是一篇笔记,是关于本机API的.本机API是除了Win32 API,NT平台开放了另一个基本接口。本
. F) x# [, u6 P! Z' x# F( y3 ] J机API也被很多人所熟悉,因为内核模式模块位于更低的系统级别,在那个级别上环境子系统是不可见的 2 }2 T7 ]: I! k4 K& Q8 ]) b; n5 A7 K3 K
。尽管如此,并不需要驱动级别去访问这个接口,普通的Win32程序可以在任何时候向下调用本机API。并
! E6 R4 Q, I+ F7 n% Q _! z没有任何技术上的限制,只不过微软不支持这种应用开发方法。
$ P( q u1 H K 7 L9 Q7 h4 L- `3 ^5 Z8 m6 [
User32.dll,kernel32.dll,shell32.dll,gdi32.dll,rpcrt4.dll,comctl32.dll,advapi32.dll,version.d
. k% h" m9 v3 Jll等dll代表了Win32 API的基本提供者。Win32 API中的所有调用最终都转向了ntdll.dll,再由它转发至 ! O# c F5 |; E( v" V& F. p
ntoskrnl.exe。ntdll.dll是本机 API用户模式的终端。真正的接口在ntoskrnl.exe里完成。事实上,内
% O0 H! ` Y9 o/ x, d. \! g核模式的驱动大部分时间调用这个模块,如果它们请求系统服务。Ntdll.dll的主要作用就是让内核函数 / q. |/ I7 U- e8 L6 E+ u/ C) u- Y
的特定子集可以被用户模式下运行的程序调用。Ntdll.dll通过软件中断int 2Eh进入ntoskrnl.exe,就是 1 B4 K5 A7 k$ e+ c |
通过中断门切换CPU特权级。比如kernel32.dll导出的函数DeviceIoControl()实际上调用ntdll.dll中导 8 M7 H" w! ]( A% `
出的NtDeviceIoControlFile(),反汇编一下这个函数可以看到,EAX载入magic数0x38,实际上是系统调
+ C6 ]9 b5 R* N4 G. u用号,然后EDX指向堆栈。目标地址是当前堆栈指针ESP+4,所以EDX指向返回地址后面一个,也就是指向
3 J' _. n' P4 R( |0 d6 u6 i; ?在进入NtDeviceIoControlFile()之前存入堆栈的东西。事实上就是函数的参数。下一个指令是int 2Eh, ; g; m! A7 c, `
转到中断描述符表IDT位置0x2E处的中断处理程序。
0 K9 q. }# e/ K2 v, h反编汇这个函数得到:
& Z* [$ U& a5 c V% O: |mov eax, 38h
7 V8 D' U6 b O, ulea edx, [esp+4]
[- H+ U4 \5 Q' }int 2Eh + E/ g3 `, x1 C
ret 28h % I9 M9 N$ D+ t) E7 a' O6 o/ b0 }# }
当然int 2E接口不仅仅是简单的API调用调度员,他是从用户模式进入内核模式的main gate。 7 x- v+ F: {" g( D
W2k Native API由248个这么处理的函数组成,比NT 4.0多了37个。可以从ntdll.dll的导出列表中很容易
1 f3 L4 K6 P$ E6 c" r认出来:前缀Nt。Ntdll.dll中导出了249个,原因在于NtCurrentTeb()为一个纯用户模式函数,所以不需 " [6 U: s4 q/ z
要传给内核。令人惊奇的是,仅仅Native API的一个子集能够从内核模式调用。而另一方面, 4 y' _6 o1 }9 `. F+ ~4 u$ @( L
ntoskrnl.exe导出了两个Nt*符号,它们不存在于ntdll.dll中: NtBuildNumber, NtGlobalFlag。它们不 / h# d. }/ ]' h* `- R9 ]4 G
指向函数,事实上,是指向ntoskrnl.exe的变量,可以被使用C编译器extern关键字的驱动模块导入。
+ A3 O+ Q$ G7 Y2 _2 \' XNtdll.dll和ntoskrnl.exe中都有两种前缀Nt*,Zw*。事实上ntdll.dll中反汇编结果两者是一样的。而在 0 b5 ?5 w. Y2 a5 O- b9 v
ntoskrnl.exe中,nt前缀指向真正的代码,而zw还是一个int 2Eh的stub。也就是说zw*函数集通过用户模
, F( Y3 U, J4 l3 D2 X; b) ?0 A式到内核模式门传递的,而Nt*符号直接指向模式切换以后的代码。Ntdll.dll中的NtCurrentTeb()没有相
4 K" ?0 W* u' }# [8 h# W对应的zw函数。Ntoskrnl并不导出配对的Nt/zw函数。有些函数只以一种方式出现。
Y2 z6 k# V# U2 d5 U9 B3 ^2Eh中断处理程序把EAX里的值作为查找表中的索引,去找到最终的目标函数。这个表就是系统服务表SST / f' R) e( d# v8 `# G$ Y& o
,C的结构SYSTEM_SERVICE_TABLE的定义如下:清单也包含了结构SERVICE_DESCRIPTOR_TABLE中的定义,为 " R7 E5 o+ h( x7 z4 p$ b" K+ C6 i
SST数组第四个成员,前两个有着特别的用途。 6 E1 ]1 _, k: g' M, }/ n0 z1 z
typedef NTSTATUS (NTAPI *NTPROC) ( ) ; 8 ^: N! e0 T/ J$ a* @" L6 O& {
typedef NTPROC *PNTPROC; " y% k& V6 U# a- m5 ~& r
#define NTPROC_ sizeof (NTPROC)
3 r: m* S; T) Jtypedef struct _SYSTEM_SERVICE_TABLE
# A, e9 ]4 ?* x8 D6 i2 i3 M{ PNTPROC ServiceTable; // 这里是入口指针数组
- _ I5 n5 W+ N/ c+ _& y) DPDWORD CounterTable; // 此处是调用次数计数数组 ; m$ e% ?& e. ~4 d' Q+ ?
DWORD ServiceLimit ; // 服务入口的个数
9 X$ O6 N# N6 Q4 TPBYTE ArgumentTable; // 服务参数字节数的数组 4 @+ C# v2 a8 H! f) s5 {
) SYSTEM_SERVICE_TABLE ,
3 ?$ R1 I. m! J* PSYSTEM_SERVICE_TABLE , ! d3 w/ l2 K# z; B. X- v
* * PPSYSTEM_SERVICE_TABLE ; # C f0 g, P! r
/ / _ _ _ _ _ _ _ _ _ _ _ _ 7 X6 q) n/ o/ J# w# s9 t3 v5 v9 v
typedef struct _SERVICE_DESCRIPTOR_TABLE
* R/ h9 {" i: c5 V2 ?/ G{ SYSTEM_SERVICE_TABLE ntoskrnl ; // ntoskrnl所实现的系统服务,本机的API}
4 Y2 U" C/ n( ?( [5 zSYSTEM_SERVICE_TABLE win32k; // win32k所实现的系统服务 + V3 ^7 ^% K; G. G$ u# A0 m
SYSTEM_SERVICE_TABLE Table3; // 未使用 ) J6 e! G4 B b8 Y- a7 E
SYSTEM_SERVICE_TABLE Table4; // 未使用 ) N! U$ x7 z7 V7 I- X& @
} SERVICE_DESCRIPTOR_TABLE ,
5 J/ k' T6 M8 z* P0 h# F$ _ a* PSERVICE_DESCRIPTOR_TABLE,
: x! p5 w5 v) H7 M* PPSERVICE_DESCRIPTOR_TABLE ;
; ?3 m4 h# A: |9 p2 i' dntoskrnl通过KeServiceDescriptorTable符号,导出了主要SDT的一个指针。内核维护另外的一个SDT,就 # i9 a8 W% W1 ?! k
是KeServiceDescriptorTableShadow。但这个符号没有导出。要想在内核模式组件中存取主要SDT很简单 0 R9 R4 U: c+ |9 U
,只需两行C语言的代码: 7 y6 I6 q9 Y6 {! p. S
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorTable;
" Z+ E8 Z, {& S- t' D4 C) I. U V7 HPSERVICE_DESCRIPTOR_TABLE psdt= KeServiceDescriptorTable;
9 l$ r8 V- f. J3 E* h# P2 zNTPROC为本机 API的方便的占位符,他类似于Win32编程中的PROC。Native API正常的返回应该是一个
$ P# {7 t- ~8 \$ r+ Q+ bNTSTATUS代码,他使用NTAPI调用约定,它和_stdcall一样。ServiceLimit成员有在ServiceTable数组里
" ~% S9 u$ p/ \8 d/ G8 G7 ^. |找到的入口数目。在2000下,默认值是248。ArgumentTable为BYTEs的数组,每一个对应于ServiceTable
% L& u1 c1 E% w2 Z/ |/ I的位置并显示了在调用者堆栈里的参数比特数。这个信息与EDX结合,这是内核从调用者堆栈copy参数到
0 e4 \& [: H, m, n自己的堆栈所需的。CounterTable成员在free buid的2000中并没有使用到,在debug build中,这个成员 ) e2 R- i' e y l
指向代表所有函数使用计数的DWORDS数组,这个信息能用于性能分析。! W/ r4 |8 ] b4 M
可以使用这个命令来显示:dd KeServiceDescriptorTable,调试器把此符号解析为0x8046e0c0。只有 6 c0 |1 N. D5 [% q) _
前四行是最重要的,对应那四个SDT成员。* U% l9 w& g G: L* J% `
运行这个命令:ln 8046e100,显示符号是KeServiceDescriptorTableShadow,说明第五个开始确实为 ; {! a2 ?1 u* |) {' H0 x
内核维护的第二个SDT。主要的区别在于后一个包含了win32k.sys的入口,前一个却没有。在这两个表中 % q( W' Y1 n# m
,Table3与Table4都是空的。Ntoskrnl.exe提供了一个方便的API函数。这个函数的名字为:
4 E/ X+ R; T7 t0 D, u KeAddSystemServiceTable
! T: E8 A* K2 g8 J- m此函数去填充这些位置。
% A; C8 g, T4 I: R/ R2Eh的中断处理标记是KisystemService()。这也是ntoskrnl.exe没有导出的内部的符号,但包含在2k符号 8 b2 W8 y: i- a: A5 Q/ U
文件中。关于KisystemService的操作如下:
! K4 ~& F; t( o Q' A% H5 K" t; t# V1 从当前的线程控制块检索SDT指针 ( k' E- p* U$ j& P
2 决定使用SDT中4个SST的其中一个。通过测试EAX中递送ID的第12和13位来决定。ID在0x0000-0x0fff的
' H( t* V% X0 q: y映射至ntoskrnl表格,ID在
0 S" ~4 G' M1 u! X' _0x1000与0x1ffff的分配给win32k表格。剩下的0x2000-0x2ffff与 6 C& Z6 x! b2 Y- ?7 s' R
0x3000-0x3ffff则是Table3和Table4保留。
6 s) a2 M$ ?$ X- N: M, V3 通过选定SST中的ServiceLimit成员检查EAX的0-11位。如果ID超过了范围,返回错误代码为
% o1 j/ f! p0 j0 Y( DSTATUS_INVALID_SYSTEM_SERVICE。
N6 [* y( w1 n. D. u S9 u q4 检查EAX中的参数堆栈指针与MmUserProbeAddress。这是一个ntoskrnl导出的全局变量。通常等于 / V: {. h) s6 K$ ^! P5 J
0x7FFF0000,如果参数指针不在这个地址之下,返回STATUS_ACCESS_VIOLATION。
2 x1 x; f" k7 T2 D: J5 查找ArgumentTable中的参数堆栈的字节数,从调用者的堆栈copy所有的参数至当前内核模式堆栈。
; _' G# m8 H$ Q2 F' {6 搜索serviceTable中的服务函数指针,并调用这个函数。 * G, O: G$ t3 J1 K4 P4 X+ R; R& v
7 控制转到内部的函数KiserviceExit,在此次服务调用返回之后。 # n3 ?# f" V' e8 y$ M
从对SDT的讨论可以看到与本机API一起还有第二个内核模式接口。这个接口把Win32子系统的图形设备接
& E. W% q/ ~0 t# c+ p2 D% [口和窗口管理器和内核模式组件Win32k连接起来。Win32k接口一样是基于int 2eh。本机API的服务号是从
% n5 T& d2 w. k$ |4 \0x0000到0x0fff,win32k的服务号是从0x1000到0x1fff。(ddW32pServiceTable认定win32k.sys的符号可
, s6 I! f# _2 Z0 }用。)win32k总共包含639个系统服务。 ( {0 L& f" h9 s
$ U- f& D; P# d( ^$ H. [0 J) ~
2Eh的处理过程没有使用全局SDT KeServiceDescriptorTable。
$ r; b* g) u6 p, {( T* Y而是一个与线程相关的指针。显然,线程可以有不同得SDT相关到自身。线程初试化的时
8 s# c; v2 U, `候,KeInitializeThread()把KeServiceDescriptorTable写到线程的控制块。尽管这样,这个默认设置之
3 L7 Y3 |8 |) [8 K后可能被改变为其它值,例如KeServiceDescriptorTableShadow。 * P) p9 D' y/ L5 C9 F/ K
! j! K V( O% _/ L A1 L7 |# ~0 ^* oWindows 2000运行时库
- B5 n, d$ \) Y& W8 C2 B) eNtdll.dll至少导出了不少于1179个符号。其中的249/248是属于Nt*/zw*集合。所以还有682个函数不是通
* c& v; j' R$ l l `! D过int 2eh门中转。很显然,这么多的函数不依靠2k的内核。 ! [( c' @3 q9 T! X# g8 Y6 J
其中一些是和c运行时库几乎一样的函数。其实ntoskrnl也实现了一些类似C运行时库的一些函数。可以
! _" l2 ]* E ]" ?# J通过ddk里的ntdll.lib来链接和使用这些函数。反汇编ntdll.dll与ntoskrnl.exe的C运行时函数能发现 p8 E" ?% \/ [) u3 b! u4 ~; n
,ntdll.dll并不是依赖ntoskrnl.exe。这两个模块各自实现了这些函数。 ! Z, e) F! v8 I" m" R
除了C运行时库外,2000还提供了一个扩展的运行时函数集合。再一次,ntdll.dll与ntoskrnl.exe各自
4 ^( O& R y, o2 Y/ C实现了它们。同样,实现集合有重复,但是并不完全匹配。这个集合的函数都是以Rtl开头的。2000运行 1 q/ @, j( ?& f0 o# ^* ~" x5 s. t+ u
时库包括一些辅助函数用于C运行时候无法完成的任务。例如有些处理安全事务,另外的操纵2000专用的
k$ b; |, ~7 W6 x6 H数据结构,还有些支持内存管理。微软仅仅在DDK中记录了很有用的406个函数中的115个函数。 , ^+ Y8 }5 Z/ _+ d
Ntdll.dll还提供了另外一个函数集合,以__e前缀开头。实际上它们用于浮点数模拟器。
, C( Z3 \% ^ A% d( f& Y+ j6 ^! t- K还有很多的函数集合,所有这些函数的前缀如下:
) ]; X- K% S9 k! k__e(浮点模拟),Cc(Cache管理),Csr(c/s运行时库),Dbg(调试支持),Ex(执行支持),FsRtl(文件系统运行
I, A( H. J! h. l, G8 C/ t& O3 N时),Hal(硬件抽象层),Inbv(系统初试化/vga启动驱动程序bootvid.dll),Init(系统初试
& ?- P& l0 G! V; |6 [, A$ `& F$ ^$ L+ G4 M, n( u
化),Interlocked(线程安全变量操作),Io(IO管理器),Kd(内核调试器支持),Ke(内核例程),Ki(内核中断处 d) [% u3 \1 g# y% n; }
理),Ldr(映象装载器),Lpc(本地过程调用),Lsa(本地安全授权),Mm(内存管理),Nls(国际化语言支持),Nt
- {( }& q# I! x9 }6 q% R(NT本机API),Ob(对象管理器),Pfx(前缀处理),Po(电源管理),Ps(进程支持),READ_REGISTER_(从寄存器 % a9 I% K/ J2 _% K6 e
地址读),Rtl(2k运行时库),Se(安全处理),WRITE_REGISTER_(写寄存器地址),Zw(本机API的替换叫法) . N( u6 x7 ~8 E
,<其它>(辅助函数和C运行时库)。 * C. e( @0 j' G+ R
当编写从用户模式通过ntdll.dll或内核模式通过ntoskrnl.exe和2000内核交互的软件的时候,需要处理 ' V, f2 x+ n8 |; @$ v
很多基本的数据结构,这些结构在Win32世界中很少见到。
) E' |" I* C( g' B* X常用数据结构
- {& l v' B7 L% d2 L% {l 整数 + O0 L. f) @/ p3 }. P
ANSI字符是有符号的,而Unicode WCHAR是无符号的 1 v5 x0 y9 V d9 E o- Z/ C
MASM的TBYTE是80位的浮点数,用于高精度浮点运算单元操作,注意它与Win32的TBYTE(text byte)完全
! k6 b$ }$ t- G. F( `5 \不同。
: n4 \( L4 Y) g/ x4 M1 Q- j, ATABLE 2-3. Equivalent Integral Data Types # H7 v3 K) n' D7 Q; B3 M
BITS MASM FUNDAMENTAL ALIAS #1 ALIAS #2 SIGNED
! o3 E1 _% w5 D K; p8 BYTE unsigned char UCHAR CHAR& e/ z' y* W) g( f; F4 }0 x
16 WORD unsigned short USHORT WCHAR SHORT . Z3 l2 @. K$ o7 r# R' y' w
32 DWORD unsigned long ULONG LONG ! e6 F' _) l2 A: V( {
32 DWORD unsigned int UINT INT
/ @4 Z! I5 O% g# A7 H64 QWORD unsigned _int64 ULONGLONG DWORDLONG LONGLONG
2 ?" l i3 r' V* Z1 K+ p8 i: B80 TBYTE N/A 7 X6 J& P& _; n
typedef union _LARGE_INTEGER 3 R) L+ p5 H) a; \$ \/ |
{ struct{
4 h }4 j6 e! O; rULONG LowPart; 3 ~6 t3 X4 D/ w- k
LONG HighPart;};
( P ]& g) d* k; LLONGLONG QuadPart;
+ j8 j6 p5 _& @7 q0 q}
. }1 [6 p0 \' r& G+ ELARGE_INTEGER , * PULARGE_INTEGER ; ) b, M/ N* s) M. i3 \
typedef union _ULARGE_INTEGER{
+ @, z0 c% `. |# i% F: Fstruct{ 8 }) k: q# I4 ^9 V- R: Q, \( G- J
ULONG LowPart;
, K6 ^8 m9 V( E0 x: o9 L4 ZULONG HighPart;}
/ ~* G; v0 |7 m+ R, ]. EULONGLONG QuadPart; 9 c8 V& H, D$ S; Y$ ^( l2 Q
}ULARGE_INTEGER, *PULARGE_INTEGER; 6 O- Y8 j! C( t- y: ^' F3 I9 s
l 字符 1 [# _9 b: U( f8 N, Q# X; ^/ ^3 Q$ ^3 c
Win32编程中PSTR用户CHAR*,PWSTR用于WCHAR*。取决于是否定义了UNICODE,PTSTR解释为PSTR或者
3 D6 G/ {+ p8 D1 W( `% a' |3 V' |PWSTR。在2k内核模式下,常用的数据类型是UNICODE_STRING,而STRING用来表示ANSI字符串: / Y1 s0 t% x" |& R# q
typedef struct _UNICODE_STRING{
) z6 [# s% O! f0 J @USHORT Length; //当前字节长度,不是字符!!!
, [( g' x0 D# H4 |* u4 hUSHORT MaximumLength; //Buffer的最大字节长度 0 C; N6 _" @* H* \8 j$ y( q
PWSTR Buffer;}UNICODE_STRING , * PUNICODE_STRING ; 6 a# c( S4 \/ f! M+ V4 k. _# @. V# z
typedef struct _STRING{
. Q9 D& f: S# c/ O+ R3 @# a/ dUSHORT Length; & ?" z$ c! C( S' G) w/ m9 M
USHORT MaximumLength; ) y& J, I0 [" J0 S7 ~2 A/ S! {
PCHAR Buffer;}STRING, *PSTRING; 2 O: p' }% Y! z/ [. l" H* k: e
typedef STRING ANSI_STRING, *PANSI_STRING; # Q* t9 \5 a/ U, v, K* l
typedef STRING OEM_STRING, *POEM_STRING;
0 ?% U" Q: j3 {# k1 }: f操纵函数:RtlCreatUnicodeString(),RtlInitUnicodeString(), 4 C$ l6 e; q. |6 f% z
RtlCopyUnicodeString()等等
% l! Q1 p) t+ A- t- bl 结构
: T* B0 ?0 s4 ?" L许多内核API函数需要一个固定大小的OBJECT_ATTRIBUTES结构,比如NtOpenFile()。对象的属性是OBJ_*
! m3 ^9 b, N, v; j/ |值的组合,可以从ntdef.h中查到。 7 F; {. l1 f# B& S4 B3 X
IO_STATUS_BLOCK结构提供了所请求操作结果的信息,很简单,status成员包含一个NTSTATUS代码, 如果 ; g7 \+ D) O0 Y5 Z, Y2 t2 z
操作成功 information成员提供特定请求的信息。 ! g0 R0 F0 V& T8 i) {0 s; \
还有一个结构是LIST_ENTRY,这是一个双向环链表。
" L( \ \% ], K. {4 C; B/ {( a+ Ytypedef struct _OBJECT_ATTRIBUTES
, v0 s6 W+ ^0 o# B6 X{ 8 q) f4 T( e1 v. G/ S0 s. r: ^6 X1 M
ULONG Length; ; y+ {* l5 n2 q, `
HANDLE RootDirectory; 5 n4 ^3 O0 w2 q2 _ a( y! m: b1 Z! T
PUNICODE_STRING ObjectName; / D9 E5 y" l) c V. n
ULONG Attributes; - r9 J: d" k F
PVOID SecurityDescriptor; e- e* i: |& m
PVOID SecurityQualityOfService;
8 K- W( ]/ b, h/ u# u0 O! o, C8 I} OBJECT_ATTRIBDTES, *POBJECT_ ATTRIBUTES;
z$ C, z+ ?! Y+ _8 K% rtypedef struct _IO_STATUS_BLOCK
& S! a1 D3 n' m2 f* X6 e{
3 c! J# T- e( JNTSTATDS Status;
6 G9 q. w) m$ C2 oULONG Information; $ D6 y5 `& ~% ^9 Z
}IO_STATUS_BLOCK , * PIO_STATUS_BLOCK ; : x: ?& W! U5 o4 i3 z0 P9 L1 w
typedef struct _LIST_ENTRY
. K( a+ M3 n9 U9 e4 K! }{ 1 X' V( _0 e" }, o1 P, {
Struct _LIST_ENTRY *Flink;
1 ]/ y' o% G/ b' wStruct _LIST_ENTRY *Blink; 9 C( Y2 p/ R: `+ S2 a: Y
}LIST_ENTRY, *PLIST_ENTRY; & n1 r& j3 Y, F' @7 s
双向链表的典型例子就是进程和线程链。内部变量PsActiveProcessHead是一个LIST_ENTRY结构,在 8 o/ |- w2 `0 O
ntoskrnl.exe的数据段中,指定了系统进程列表的第一个成员。 " |. c+ r0 [" Q) T. w6 \
CLIENT_ID结构由进程和线程ID组成。
9 I5 j. Y: m, Xtypedef struct _CLIENT_ID % Q% N7 K2 C' a' W
{ HANDLE UniqueProcess;
; p( B7 m- b( h! U6 lHANDLE UniqueThread;
! |; c. q1 j0 E: {7 L; Y)CLIENT_ID, *PCLIENT_ID; : M0 i' e7 `6 N0 k1 T
想要从用户模式调用ntdll.dll中的API函数,必须考虑到以下四点:
+ L, x$ ]9 N0 m/ f* U# ^1 W2 d1 SDK头文件没有包括这些函数的原型
2 d4 ?( D: i/ R& h/ r1 i" d: P2 这些函数使用的若干基本数据类型没有包括在SDK文件中
2 u2 Z' A( t6 B) W3 SDK和DDK头文件不兼容,不能在win32的c源文件包含ntddk.h中 7 x0 r4 d" n' i
4 ntdll.lib没有包括在VC的默认导入库列表中。
$ F& L/ \% l2 a* ~, v; D& I第4个很容易解决:#progma comment(linker,“/defaultlib:ntdll.lib”)
/ t- e4 j& K; p& u# y- K) r5 X缺失的定义比较难解决,最简单的方法是写一个自定义的头文件,刚刚包含需要调用ntdll.dll中函数的
$ V1 O& k/ g0 O$ J+ d定义。幸运的是,已经在光盘的w2k_def.h文件中做了这个工作。因为这个头文件将用于用户模式和内核 ) I- Z5 M; k, `
模式程序,所以必须在用户模式代码中,#include<w2k_def.h>之前#define _USER_MODE_,使得DDK中出
# G* r1 D% {$ h6 D9 i9 U# _现而SDK中没有的定义可用。
; I0 N2 \$ [) \, `) R' A9 H4 q: x5 K. R# g4 s" R* q1 a, W
本文部分翻译于一篇电子书<win api about>.也感谢朋友GameHunter这位英语极好的朋友帮忙.与Free的 c Y2 k( D# V4 w6 Q% Z
指导& b- q5 P1 [9 q" C# a6 V1 Q2 |
' `1 I4 i% \) l- P+ C% K t
+ o3 ^/ L. D5 S: X9 v; _ |