可以绕过多少系统提供的用户空间进程初始化?

时间:2016-01-03 18:11:20

标签: winapi assembly reverse-engineering

在大多数基于x86的Unix系统上,您可以构建一个"静态"不加载任何系统提供的DLL( - 等效)的可执行文件,并在正常终止之前运行最少的指令。例如,这适用于x86 / Linux(32位)。从技术上讲,我甚至可能不需要第二条mov指令,因为IIRC ABI保证所有寄存器在程序入口点被清零。

$ cat > test.s
.text
.globl start
start:
    movl $1,%eax  # _exit
    movl $0,%ebx
    int $0x80
$ as -32 test.s -o test.o
$ ld -m elf_i386 -e start test.o -o test

我的问题是,在流程创建和终止之间,在用户空间中执行的最低限度的指令,您可以在Windows上获得多少接近。我听说有传言称,无论PE文件是否引用它们,内核进程创建逻辑都会将ntdll.dll并且可能还kernel32.dll加载到每个进程中,并且这两者都具有可能不可避免的重要启动代码。我还听说过系统呼叫号码不是稳定ABI的一部分的谣言,所以你 通过ntdll呼叫以获得跨版本兼容性,即使你绕过Win32也是如此。我想知道这些谣言在多大程度上是正确的,以及它们的影响可以在多大程度上得到解决。

这是实验中可能的练习,而不是运送给最终用户的产品中的好主意。提出这个问题的一个具体动机是,如果有可能削减强制性的"系统DLL完全脱离循环,然后可以直接测量由于它们的自我初始化而导致的进程启动时间的比例。

我对低级别的Windows编程不是很有经验,所以如果你能像上面那样逐步提供构建" minimal"你提出的可执行文件作为你的答案,我们将不胜感激。

2 个答案:

答案 0 :(得分:4)

我或许可以回答你的部分问题,但我不知道(我怀疑)你可以绕过它们。

  

我也听说过系统电话号码不属于其中的谣言   稳定的ABI,所以你必须通过ntdll调用跨版本   兼容性,即使你绕过Win32

确实如此,每个主要内核版本都附带较新的系统调用号。

系统调用号不是永久性的原因是系统调用表是按名称(而不是数字)生成的。因此,每次插入新的系统调用时,旧的系统调用都会被“推”到更远的位置(如果系统调用被删除,则相反,尽管这种情况非常罕见)。

系统调用表名称(内核端)是KiServiceTableKeServiceDescriptorTableKeServiceDescriptorTableShadow的一部分)。

kd> dps nt!KeServiceDescriptorTable L4
fffff800`1236ba80  fffff800`1215f700 nt!KiServiceTable
fffff800`1236ba88  00000000`00000000
fffff800`1236ba90  00000000`000001b1
fffff800`1236ba98  fffff800`1216048c nt!KiArgumentTable

有0x1B1系统调用(Windows 8.1),系统调用指针位于KiServiceTable

userland系统调用存根看起来像这样(Windows 10):

0:004> u ntdll!ntcreatefile
ntdll!NtCreateFile:
00007fff`1d913ac0 4c8bd1          mov     r10,rcx   ; args
00007fff`1d913ac3 b855000000      mov     eax,55h   ; syscall number
00007fff`1d913ac8 0f05            syscall           ; x64 instruction, perform ring3 -> ring0 transition
00007fff`1d913aca c3              ret
00007fff`1d913acb 0f1f440000      nop     dword ptr [rax+rax]

Windows 8.1 x64中的相同内容:

0:003> u ntdll!ntcreatefile
ntdll!NtCreateFile:
00007ff8`62071720 4c8bd1          mov     r10,rcx
00007ff8`62071723 b854000000      mov     eax,54h
00007ff8`62071728 0f05            syscall
00007ff8`6207172a c3              ret
00007ff8`6207172b 0f1f440000      nop     dword ptr [rax+rax]

正如您所看到的,相同的功能会导致不同的系统调用号码(Windows 10为0x55,Windows 8.1为0x54)

syscall表中的指针(在内核中)现在以一种简单的方式“编码”(它们之前是普通的指针)。我们来看看索引0x54:

kd> ? nt!KiServiceTable+(dwo(nt!KiServiceTable + 0x54 * 4) >> 4)
Evaluate expression: -8795786429460 = fffff800`12463bec

此地址有哪些符号?

kd> ln fffff800`12463bec
Browse module
Set bu breakpoint

(fffff800`12463bec)   nt!NtCreateFile   |  (fffff800`12463c70)   nt!IopCreateFile
Exact matches:
    nt!NtCreateFile (<no parameter info>)

所以ntdll!ntcreatefile导致内核函数nt!NtCreateFile(不是一个大惊喜:)

您可以找到主要Windows系统at this URL的系统调用表。

实际上,Windows XP内核中的leaked source(实际上是WRK)显示了如何生成服务表(在汇编文件中)。

  

我听说过有关内核进程创建逻辑的谣言   将ntdll.dll和可能还有kernel32.dll加载到每个进程中   PE文件是否引用它们,以及这两者   拥有可能不可避免的重要启动代码

这是真的。我将不会完成整个过程,这个过程非常复杂,并且在 Windows Internals 书籍中进行了长时间的讨论。

ntdll被加载,因为用户区域窗口加载器的很大一部分位于那里(如果您有符号信息,请查看以Ldr开头的所有函数。)

kernel32.dll也被加载到进程地址空间内,因为主线程初始化的一部分位于那里。它也是必需的,因为在那里完成了一部分异常处理。

我本可以使用只执行一条指令的可执行文件(即x86 / x64上的RET),但结果与记事本相同。

在入口点放置一个断点:

0:000> bp $exentry
0:000> bl
0 e 00007ff6`275c4030     0001 (0001)  0:**** notepad!WinMainCRTStartup
0:000> g
Breakpoint 0 hit
notepad!WinMainCRTStartup:
00007ff6`275c4030 4883ec28        sub     rsp,28h

条目处的堆栈跟踪:

0:000> kb
 # RetAddr           : Args to Child                                                           : Call Site
00 00007fff`1ce62d92 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : notepad!WinMainCRTStartup
01 00007fff`1d889f64 : 00007fff`1ce62d70 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x22
02 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x34

所以我们有ntdll!RtlUserThreadStart调用KERNEL32!BaseThreadInitThunk来调用可执行文件的入口点。

0:000> u KERNEL32!BaseThreadInitThunk L 10
KERNEL32!BaseThreadInitThunk:
00007fff`1ce62d70 48895c2408      mov     qword ptr [rsp+8],rbx
00007fff`1ce62d75 57              push    rdi
00007fff`1ce62d76 4883ec20        sub     rsp,20h
00007fff`1ce62d7a 498bf8          mov     rdi,r8
00007fff`1ce62d7d 488bda          mov     rbx,rdx
00007fff`1ce62d80 85c9            test    ecx,ecx
00007fff`1ce62d82 7517            jne     KERNEL32!BaseThreadInitThunk+0x2b (00007fff`1ce62d9b)
00007fff`1ce62d84 488bca          mov     rcx,rdx
00007fff`1ce62d87 ff15d3390600    call    qword ptr [KERNEL32!_guard_check_icall_fptr (00007fff`1cec6760)]
00007fff`1ce62d8d 488bcf          mov     rcx,rdi
00007fff`1ce62d90 ffd3            call    rbx  ; call entry point
00007fff`1ce62d92 8bc8            mov     ecx,eax
00007fff`1ce62d94 ff15be2f0600    call    qword ptr [KERNEL32!_imp_RtlExitUserThread (00007fff`1cec5d58)]
00007fff`1ce62d9a cc              int     3

如您所见,从入口点返回调用KERNEL32!_imp_RtlExitUserThread(为主线程调用ExitProcess())。

答案 1 :(得分:2)

最接近初始化本身就是TLS回调,据我所知,here是对事情如何运作的一些解释; TLS回调在应用程序的入口点之前执行,并且它们确实有一些限制(可以通过一些努力来解决)。

至于测量启动时间,你应该避免尝试在自己的应用程序中进行;一个分离的过程最适合那个(调试器可以更可靠的方式完成这个过程)。

关于最小可执行文件,您可以构建仅具有RET的可执行文件(如@Neitsa所述); windows会将程序加载到内存中,但不会执行任何操作,它基本上只会将内容映射到内存中,而且全部都是。

使用FASM,您可以构建一个几乎没有任何内容的exe,如下所示:

include '%fasm%\win32ax.inc'

section 'a' code readable executable

start:
retn
.end start