进程和线程

336次阅读  |  发布于3年以前

看了张银奎老师的书,记录一下。有很多比较官方的概念我直接抄了下来,所以不要喷我抄书,爱所有反卷局的大宝贝们!

进程和线程 处于运行状态的程序又称为进程,每个进程运行在自己的空间中,空间相对独立,受操作系统保护,在每个进程空间中,一般都会有一个或者多个线程在运行。

1

进程资源

在操作系统的规则中,资源一般都是针对于进程来分配的,必须要先有一个进程,才能对其分配资源。 在Windows操作系统中,每个进程都拥有如下的资源:1、一个虚拟的地址空间,又被称为进程空间

2、全局唯一的进程ID,简称PID ( Client ID )

3、一个可执行映像,也是该进程的可执行文件在内存中的表示

4、一个或者多个线程

5、一个位于内核空间中的名为EPROCESS的数据结构,用来记录该进程的关键信息,包括进程的创建时间,映像文件名称等等

6、一个位于内核空间中的对象句柄表,用来记录和索引该进程所创建/打开的内核对象,操作系统根据该表格将用户模式下的句柄翻译为指向内核对象的指针

7、一个用于描述内存目录表起始位置的基地址,简称页目录基地址 ( DirBase ),当CPU切换到该任务/进程的时候,会将该地址加载到页表基地址寄存器CR3,这样当前进程的虚拟地址才会被翻译为正确的物理地址

8、一个位于用户空间中的进程环境块PEB

9、一个访问令牌,用于表示该进程的用户,安全组以及优先级

!process 0 0 执行该命令可以列出系统内所有的进程,第一个参数用来指定要显示的进程ID ( 因为叫Client ID 所以这里就是Cid ),0代表了所有进程,第二个参数用来指定要显示的进程属性,0代表只显示最基本的进程属性。 可以在命令后加上程序文件的名字来过滤,这样命令执行后,只显示wermgr进程的属性。

进程的SessionId代表了该进程所在的Windows会话 ( session ) 的ID号, 当有多个用户同时登陆的时候,Windows会给每个登录用户建立一个会话,每个会话都有自己的WorkStation和Desktop,这样大家就可以在不同的会话中共用一个Windows系统,对于XP来说,只有一个用户登录的时候,用户启动的程序和系统服务都运行在session 0,当切换到另一个用户账号的时候,系统会建立Session 1,以此类推。 测试了一下,确实奥!如果我们net usr add后进行Switch User的时候我们的SessionId会进行++, 从Windows Vista开始,只允许系统服务运行在Session 0,系统启动后便会自动创建,当用户登录的时候会创建另一个会话 一般叫做 Session 1,所以用户登录到系统的时候一般会看至少两个Csrss在运行,系统启动早期创建的几个特殊进程不属于任何会话,所以他们的SessionId为空,比如system Registr 等等。

Cid为进程ID,进程ID是表示进程的一个整数,很多用户态的函数用它作为标识进程的参数,在内核空间的代码中,主要使用EPROCESS指针来标识进程。 Parent Cid是父进程的进程ID,既创建该进程的那个进程的进程ID。

DirBase 代表了该进程顶级页表的位置,也就是CPU切换到该进程的时候,CR3寄存器的内容,也就是当时看火哥的那个讲的说页表目录CR3寄存器内容指向的就是第一个表的位置,该寄存器是将虚拟地址转换成物理地址必须要的参数数值,顶级的页表叫页目录表,所以这个字段的名字叫做页目录基地址,DirBase字段的位定义与当前使用的分页模式有关,在32位的分页模式的时候,DirBase低12位总是0,高20位是该进程的页目录的页帧编号 ( PFN ),比如DirBase是上面的0xA2C83000,那么PFN为0xA2C83,那么就会有一个小疑问PFN是什么?这里我们就先把他当作一个index,是一个数组,等到时候变强了我再来看!

在IA32 分页模式下,CR3低12位会因为CR4的PCIDE位(17位)而不同,PCIDE( Process-Context Identifiers Enable )。 当PCIDE开启为1的时候,CPU会缓存多个进程的页表信息,低12位变成进程上下文的ID号,为了防止CPU熔断和幽灵的漏洞。NT内核中引入了KVA影子的安全补丁,这个补丁会使用CPU的PCID功能,但是并不知道CPU Meltdown和Spectry是啥!得去查查。


CPU Meltdown和Spectry漏洞:

回来啦!这两个漏洞比较类似,大概的意思就是:我问二木哥:我昨天啊~看见了你和一个女生在一起逛街,你们是不是?虽然我昨天压根儿什么都没看到,但是我说完,他要是愣了一会(愣神比较久),回了一句:我凭什么要告诉你?虽然LSP没有承认,但是显然我已经知道了答案。那么这次的漏洞也是同理,利用了CPU的两个特殊的机制:其中Meltdown利用了CPU的乱序执行,Spectre利用了CPU的预测执行。

什么是CPU乱序执行和预测执行呢?

预测执行:在整个OS系统中,假设某个恶意软件去问操作系统:刚才那小伙!登录的密码第一个数字是1吗?操作系统回复:不关你的事,你没有权限知道。虽然这个过程本身是没有问题的,但是问题出在:操作系统答复了,但是他心里想了一遍这个答案,尤其是当对方问到一个正确的数字时,他回答的稍微犹豫了几毫秒,这个事情被恶意软件注意到了,就可以通过反复的提问,最后猜到了密码是什么。

乱序执行:假设二木哥 有a b c三个老婆,其中a老婆在娘家没有探访的权限,但是b和c在自己家里随时可以访问,此时执行下面这个条件表达式!x= a?b:c 由于a老婆不能去,如果二木哥去了!就会被揍!但实际上,二木哥会在检查a是否可访问的同时,预先就往下执行了,等到结果回来,已经根据a的结果完成了b或者c访问,反正不能亏!只是还没有赋给x而已,那么经过加载的b或者c会放缓存里。虽然报错了,但如果再次访问就会比较快,于是再次访问b和c,根据返回的时间快慢,就可以猜到a的内容!假设我想知道a有没有买化妆品,如果执行b成功了,就可以知道a确实买了化妆品。


知道了上面的瞎扯淡原理后,我们来好好看看是为啥!

Meltdown
; rcx = kernel address
; rbx = probe_array
mov al, byte [rcx]
shl rax, 0xc
mov rbx, qword [rbx + rax]

rcx存放了用户层不可以访问的内核地址,rbx存放了探测的数组。 当我们要访问rcx的时候,就会触发异常,该指令及之后的指令对寄存器的修改都会被丢弃,处理器重新回到能正常执行的指令中,但由于处理器采用乱序执行方式,在等待处理器完成该指令执行的同时(权限检查结束之前),后面两条指令已经被执行了(尽管最终会被丢弃)。

将指令3读取到的数据乘以4096(4KB)也就是一页,因为对可访问的内存进行了访问后会将内存页放入到缓存中去,根据这个速度就可以进行判定,因为缓存的速度会很快。

那么作为攻击者去对缓存进行测信道攻击就会知道哪个内存页访问过了,从而推断出被访问的内核内存数据。


NT内核中也引入了叫做KVA的补丁,会开启PCID的功能,查看一下KVA的模式为1,cr4的寄存器位17为1。

ObjectTable的含义为该进程的内核对象和句柄表格,Windows使用该表格将句柄翻译成内核对象的指针。 HandleCount表示ObjectTable所含的表项的数量。

2

进程空间

介绍了32位的进程空间,64位的,就还是内核空间是共享的,但是要为系统中的所有进程服务,所以不允许被某个进程任意访问和破坏,所以内核空间的特权级别高于用户空间,又介绍了64位的,又说了怎么看EPROCESS结构 dt _EPROCESS xxxxxxxx 用 !process EPROCESS的结构地址 来显示进程的关键信息,还介绍了PEB是进程环境块,里面有进程大多数用户模式的信息,.process xxxxxxx 设置当前进程后 dt _PEB xxxxxxx。

访问模式

重点来了! Windows定义了两种访问模式 ( access mode )

应用程序的代码运行在用户模式之下,操作系统的代码运行在内核模式之下,对于x86处理器来说并没有任何寄存器来表明我们当前处于何种模式,优先级只是代码或者数据所在的内存段或者页的一个属性。

虽然不可以直接访问R0层的,但是用户程序可以通过调用系统服务来间接的访问内核空中的数据或间接调用执行内核空间代码,当我们调用系统服务的时候,会从用户模式切换到内核模式,调用结束的时候返回用户模式,这就是模式切换。

那么在线程KTHREAD结构中,定义了UserTime和KernelTime的两个字段,用来记录线程在用户模式和内核模式运行的时间(时钟中断次数为单位),模式切换通过软中断或者专门的fast system call指令实现。

使用INT 2E切换到内核模式:

我们可以很简明的看到我们该调用首先呢转到Kernel32的ReadFile函数,ReadFile函数对参数进行简单的检查后调用Ntdll中的NtReadFile函数。

kernel32: Ntdll:

我是个好奇的人,所以直接去看了win2k3的源码把这个KiSystemService给逆了。

_KiSystemService        proc

        ENTER_SYSCALL   kss_a, kss_t    ; set up trap frame and save state

?FpoValue = 0

;
; (eax) = Service number
; (edx) = Callers stack pointer
; (esi) = Current thread address
;
; All other registers have been saved and are free.
;
; Check if the service number within valid range
;

_KiSystemServiceRepeat:
        mov     edi, eax                ; copy system service number
        ;edi = eax 等于了系统调用号
        shr     edi, SERVICE_TABLE_SHIFT ; isolate service table number
        and     edi, SERVICE_TABLE_MASK ;
        ; 这里用来确定是去找系统服务表还是驱动的表
        mov     ecx, edi                ; save service table number
        add     edi, [esi]+ThServiceTable ; compute service descriptor address
        ; 指向系统服务表
        mov     ebx, eax                ; save system service number
        ; ebx位系统调用号
        and     eax, SERVICE_NUMBER_MASK ; isolate service table offset
        ; eax为系统服务下标
;
; If the specified system service number is not within range, then attempt
; to convert the thread to a GUI thread and retry the service dispatch.
;

        cmp     eax, [edi]+SdLimit      ; check if valid service
        jae     Kss_ErrorHandler        ; if ae, try to convert to GUI thread
        ; 检查系统调用的下标是否超过了表的大小

;
; If the service is a GUI service and the GDI user batch queue is not empty,
; then call the appropriate service to flush the user batch.
;

        cmp     ecx, SERVICE_TABLE_TEST ; test if GUI service
        ; 跳转GUI API服务
        jne     short Kss40             ; if ne, not GUI service
        mov     ecx, PCR[PcTeb]         ; get current thread TEB address
        ; ecx指向KPCR
        xor     ebx, ebx                ; get number of batched GDI calls

KiSystemServiceAccessTeb:
        or      ebx, [ecx]+TbGdiBatchCount ; may cause an inpage exception

        jz      short Kss40             ; if z, no batched calls
        push    edx                     ; save address of user arguments
        push    eax                     ; save service number
        call    [_KeGdiFlushUserBatch]  ; flush GDI user batch
        pop     eax                     ; restore service number
        pop     edx                     ; restore address of user arguments

;
; The arguments are passed on the stack. Therefore they always need to get
; copied since additional space has been allocated on the stack for the
; machine state frame.  Note that we don't check for the zero argument case -
; copy is always done regardless of the number of arguments because the
; zero argument case is very rare.
;

Kss40:  inc     dword ptr PCR[PcPrcbData+PbSystemCalls] ; system calls
        ; 直接跳转到这里进行++系统调用数


FPOFRAME ?FpoValue, 0

        mov     esi, edx                ; (esi)->User arguments
        ; 3环的指针
        mov     ebx, [edi]+SdNumber     ; get argument table address
        ; 系统服务表
        xor     ecx, ecx
        mov     cl, byte ptr [ebx+eax]  ; (ecx) = argument size
        mov     edi, [edi]+SdBase       ; get service table address
        mov     ebx, [edi+eax*4]        ; (ebx)-> service routine
        ; 指向的函数
        sub     esp, ecx                ; allocate space for arguments
        shr     ecx, 2                  ; (ecx) = number of argument DWORDs
        mov     edi, esp                ; (es:edi)->location to receive 1st arg
        cmp     esi, _MmUserProbeAddress ; check if user address
        jae     kss80                   ; if ae, then not user address

KiSystemServiceCopyArguments:
        rep     movsd                   ; copy the arguments to top of stack.
                                        ; Since we usually copy more than 3
                                        ; arguments.  rep movsd is faster than
                                        ; mov instructions.
       ; 复制参数

;
; Make actual call to system service
;
kssdoit:
        CAPSTARTX <_KiSystemService,ebx>
        call    ebx                     ; call system service
        ; call 系统服务
        CAPENDX <_KiSystemService>

kss60:

两张表一张是找内核函数,另一个是驱动函数,那么我们看的eax 里存一个值,系统调用号或者服务号,这个东西的低12位就是函数参数表和函数地址表的下标,而第13位如果是0,表示找系统服务表,如果是1,那么找驱动函数的的表。

总结来说就是KiSystemService会根据服务ID从SSDT中找调用的服务地址和参数描述,然后将参数从用户态复制到线程的内核态,然后调用真正的函数执行操作,操作结束后返回给KiSystemService,该函数会把结果返回到用户态,在进行系统调用的时候,CPU需要从内存中加载门描述符和段描述符才可以得到KiSystemServer的地址,然后进行权限检查,检查源代码位置和目标代码位置的代码段的权限。

快速系统调用

系统调用是非常繁琐的事情,那么肯定想要减少这些开销的啦!

奔腾 II 处理器引入的SYSENTER/SYSEXIT 指令正是按照这一思路设计的,AMD K7 引入的 SYSCALL/SYSRETURN指令 都是因为这个速度的目的来设计的,对比INT 2E来说,使用这些指令可以加快系统调用的速度,因此利用这些指令来进行系统调用的方式叫做快速系统调用。

因为Windows 2000 或者之前的Windows的操作系统不支持快速系统调用,他们只能使用INT 2E的方式来进行系统调用,这时候我们需要去小路哥哥那里白嫖一个windows XP回来!

这让人兴奋的界面,直接来了把扫雷。

稳了!继续学习!

在Windows XP和Windows Server 2003或更高的版本在启动的过程中会通过CPUID 指令检测CPU是否支持快速系统调用指令(EDX寄存器的SEP标志位),EDX的11位代表了SEP标志位,来代表是否支持快速系统调用,如果系统不支持就还会使用INT 2E的方式。

进行快速系统调用需要做的准备:

在GDT表中建立4个段描述符,用来描述供SYSENTER指令进入内核模式时使用的代码段(CS)和栈段(SS),以及SYSEXIT指令从内核模式返回用户模式时使用的代码段和栈段,在GDT中他们的排列方式应该严格按照上述的方式进行排列。

设置专门用于系统调用的MSR寄存器,SYSENTER_EIP_MSR用于指定新的程序指针,是SYSENTER指令要跳转的目标例程地址。Windows系统会将其设置为KiFastCallEntry的地址,因为该例程是Windows内核中专门用于受理快速系统调用的,SYSENTER_CS_MSR用来指定新的代码段,也就是KiFastCallEntry所在的代码段,SYSENTER_ESP_MSR用于指定真的栈指针(ESP)。 | MSR Name | MSR Add | Useful | | ---------------- | ------- | -------------------- | | SYSENTER_CS_MSR | 174 | 目标代码段的CS选择子 | | SYSENTER_ESP_MSR | 175 | 目标ESP | | SYSENTER_EIP_MSR | 176 | 目标EIP |

该地址用于Kernel debug时,通过rdmsr/wrmsr指令来读/写这3个寄存器。 174的位置我们应该不陌生了,因为0环的时候切换寄存器看到的cs经常是08,那么我们看一下EIP的位置: 可以看到是nt!KiFastCallEntry的地址。

会将一小段名为SystemCallStub的代码复制到SharedUserData的内存区,内存区会被映射到每个Win32的进程空间中,这样每次进行系统调用的时候,NTDll.DLL中的stub函数就会调用这段SystemCallstub代码,该代码根据系统硬件的不同而不同,对于IA-32就会使用SYSENTER,对于AMD就会使用SYSCALL指令。 打开windbg开始,看一下ntdll中的ReadFile函数!这里碰到了点坑,就是符号老加载不上,如果想去看ntdll的东西,需要切换一下进程。 可以很清楚的看到 SharedUserData!SystemCallstub 的地址给了edx寄存器,Call的是edx中的地址。 将当前的esp的寄存器放入到edx寄存器当中 ( 个人认为这里应该是要去获取r3层的参数所以到时候可以通过edx寄存器来取参数,因为esp需要修改 ),因为sysenter要调用KiFastCallEntry,所以去看看!

8053e540 b923000000      mov     ecx,23h
8053e545 6a30            push    30h
8053e547 0fa1            pop     fs
8053e549 8ed9            mov     ds,cx
8053e54b 8ec1            mov     es,cx

在 KPCR(Processor Cotnrol Region)区域的 +0x40 位置是 TSS 指针(指向一个 KTSS 结构),KPCR 结构的地址在0xffdff000: 0x80042000 _KTSS为TSS的指针,所以KTSS的结构为: 0级的Esp值,这指向一个 KTRAP_FRAME 结构 V86Es 成员。

8053e54d 8b0d40f0dfff    mov     ecx,dword ptr ds:[0FFDFF040h]
8053e553 8b6104          mov     esp,dword ptr [ecx+4]
8053e556 6a23            push    23h

这个!ffdff040就是KPCR TSS指针所指向的KTSS的指针,所以他将这个值给了ecx,再通过ecx+4获得ESP0的地址,因为当前的esp所在的是KTRAP_FRAME的V86Es,所以push 23修改了V86Es上层成员HardwareSegSs。

KTRAP_FRAME 结构

在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里,因为我们已经获取到了ESP0的地址,KTRAP_FRAME的基址为 Esp0 - 0x7c。

在 KiFastCallEntry() 中将 context 信息保存在一个被称为 KTRAP_FRAME 的结构里,在前面我们看到 KTRAP_FRAME 结构的地址被赋予 esp 寄存器,因此:KTRAP_FRAME 结构就是 KiFastCallEntry() 函数的 stack 区域。

开始逆!

kd> u nt!Kifastcallentry L20
nt!KiFastCallEntry:
8053e540 b923000000      mov     ecx,23h   
8053e545 6a30            push    30h
8053e547 0fa1            pop     fs            ;fs = 0x30
8053e549 8ed9            mov     ds,cx        ;ds = 0x23
8053e54b 8ec1            mov     es,cx        ;es = 0x23 都是段选择子有关的修改相应的权限的
8053e54d 8b0d40f0dfff    mov     ecx,dword ptr ds:[0FFDFF040h]    ;ecx = _KTSS ptr
8053e553 8b6104          mov     esp,dword ptr [ecx+4]    ;esp = ESP0 -> KTRAP_FRAME的0x7c位置的V86Es
8053e556 6a23            push    23h    ;_KTRAP_FRAME.HardwareSegSs = 0x23
8053e558 52              push    edx    ;_KTRAP_FRAME.HardwareEsp = edx (edx为三环的esp)
8053e559 9c              pushfd            ;_KTRAP_FRAME.EFlags = EFlags
8053e55a 6a02            push    2       
8053e55c 83c208          add     edx,8    ;因为edx为三环的esp,所以这个步骤是要去取三环的参数
8053e55f 9d              popfd            ;0环 EFlags = 0x2
8053e560 804c240102      or      byte ptr [esp+1],2 ;EFlags的 IF位置1
8053e565 6a1b            push    1Bh    ;_KTRAP_FRAME.SegCs = 0x1B
8053e567 ff350403dfff    push    dword ptr ds:[0FFDF0304h] ;_KTRAP_FRAME.Eip为 kd> u 7c92e4f4  ntdll!KiFastSystemCallRet
8053e56d 6a00            push    0        ;_KTRAP_FRAME.ErrCode = 0
8053e56f 55              push    ebp    ;_KTRAP_FRAME.EBP = EBP
8053e570 53              push    ebx    ;_KTRAP_FRAME.EBX = EBX
8053e571 56              push    esi    ;_KTRAP_FRAME.ESI = ESI
8053e572 57              push    edi    ;_KTRAP_FRAME.EDI = EDI
8053e573 8b1d1cf0dfff    mov     ebx,dword ptr ds:[0FFDFF01Ch] ;ebx = _kpcr 0ffdff000
8053e579 6a3b            push    3Bh    ;_KTRAP_FRAME.SegFs = 0x3B
8053e57b 8bb324010000    mov     esi,dword ptr [ebx+124h]    ;esi = kpcr_kprcb_CurrentThread 
8053e581 ff33            push    dword ptr [ebx]    ;_KTRAP_FRAME.ExceptionList = ExceptionList
8053e583 c703ffffffff    mov     dword ptr [ebx],0FFFFFFFFh ;ExceptionList = -1
8053e589 8b6e18          mov     ebp,dword ptr [esi+18h] ;ebp = kpcr_kprcb_CurrentThread_InitialStack
8053e58c 6a01            push    1    ;_KTRAP_FRAME.PreviousPreviousMode = 1 代表三环过来的
8053e58e 83ec48          sub     esp,48h    ;esp = ntdll!_KTRAP_FRAME_ptr
8053e591 81ed9c020000    sub     ebp,29Ch    ;
8053e597 c6864001000001  mov     byte ptr [esi+140h],1    ;kpcr_kprcb_CurrentThread_PreviousMode = 1 表示从3环调用来
8053e59e 3bec            cmp     ebp,esp    ;比较当前的esp和ebp的位置
8053e5a0 759a            jne     nt!KiFastCallEntry2+0x47 (8053e53c)    ;如果不相同则异常,正常都在_KTRAP_FRAME_ptr
8053e5a2 83652c00        and     dword ptr [ebp+2Ch],0    ;_KTRAP_FRAME.Dr7 = 0
8053e5a6 f6462cff        test    byte ptr [esi+2Ch],0FFh    ;DebugActive
8053e5aa 89ae34010000    mov     dword ptr [esi+134h],ebp    ;TrapFrame = ebp(_KTRAP_FRAME_ptr)
8053e5b0 0f854afeffff    jne     nt!Dr_FastCallDrSave (8053e400)    ;检测是否被调试的状态
8053e5b6 8b5d60          mov     ebx,dword ptr [ebp+60h]    ;ebx = _KTRAP_FRAME.Ebp
8053e5b9 8b7d68          mov     edi,dword ptr [ebp+68h]    ;edi = _KTRAP_FRAME.Eip
8053e5bc 89550c          mov     dword ptr [ebp+0Ch],edx    ;_KTRAP_FRAME.DbgArgPointer = edx 保存3环的参数指针
8053e5bf c74508000ddbba  mov     dword ptr [ebp+8],0BADB0D00h    ;_KTRAP_FRAME.DbgArgMark = 0BADB0D00h
8053e5c6 895d00          mov     dword ptr [ebp],ebx    ;_KTRAP_FRAME.DbgEbp = _KTRAP_FRAME.Ebp
8053e5c9 897d04          mov     dword ptr [ebp+4],edi    ;_KTRAP_FRAME.DbgEip = _KTRAP_FRAME.Eip
8053e5cc fb              sti

结构THRED的E0偏移为ServiceTable

KeServiceDescriptorTableShadow

8053e5cd 8bf8            mov     edi,eax    ;系统服务例程号
8053e5cf c1ef08          shr     edi,8        ;右移八位
8053e5d2 83e730          and     edi,30h    ; index
8053e5d5 8bcf            mov     ecx,edi    ; ecx = index
8053e5d7 03bee0000000    add     edi,dword ptr [esi+0E0h]    ;esi+0E0h = ServiceTable_ptr 代表是去找index=1还是=0的结构的项
8053e5dd 8bd8            mov     ebx,eax    ;系统服务例程号
8053e5df 25ff0f0000      and     eax,0FFFh    ;取后24位
8053e5e4 3b4708          cmp     eax,dword ptr [edi+8]    ;edi + 8的位置为MaxServiceNumber:最大的系统服务例程号
8053e5e7 0f8345fdffff    jae     nt!KiBBTUnexpectedRange (8053e332)
8053e5ed 83f910          cmp     ecx,10h    ;比较index是否为1
8053e5f0 751a            jne     nt!KiFastCallEntry+0xcc (8053e60c)
8053e5f2 8b0d18f0dfff    mov     ecx,dword ptr ds:[0FFDFF018h]
8053e5f8 33db            xor     ebx,ebx    ;ebx = 0
8053e5fa 0b99700f0000    or      ebx,dword ptr [ecx+0F70h]    ;调试的时候发现这里都是0
8053e600 740a            je      nt!KiFastCallEntry+0xcc (8053e60c)
8053e602 52              push    edx
8053e603 50              push    eax
8053e604 ff15e43f5580    call    dword ptr [nt!KeGdiFlushUserBatch (80553fe4)]
8053e60a 58              pop     eax
8053e60b 5a              pop     edx
8053e60c ff0538f6dfff    inc     dword ptr ds:[0FFDFF638h]    ;系统调用的次数+1
8053e612 8bf2            mov     esi,edx    ;三环的edx给了当前的esi
8053e614 8b5f0c          mov     ebx,dword ptr [edi+0Ch]    ;ebx指向了参数表的
8053e617 33c9            xor     ecx,ecx    ;ecx = 0
8053e619 8a0c18          mov     cl,byte ptr [eax+ebx]    ;读取参数的个数ArgumentSizeTable:提供每个例程所需要的参数大小,这个值将要用来从 caller 里复制多少个参数
8053e61c 8b3f            mov     edi,dword ptr [edi]    ;获取ServiceRoutineTable(提供真正的系统服务例程的地址)的地址
8053e61e 8b1c87          mov     ebx,dword ptr [edi+eax*4]    ;读取服务例程地址
8053e621 2be1            sub     esp,ecx    ;在当前栈上开辟空间容纳参数
8053e623 c1e902          shr     ecx,2    ;ecx/4 获取参数个数
8053e626 8bfc            mov     edi,esp    ;edi指向栈
8053e628 3b35d4995580    cmp     esi,dword ptr [nt!MmUserProbeAddress (805599d4)]    ;看是否属于用户空间
8053e62e 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (8053e7dc)
8053e634 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]    ;复制参数到当前栈上
8053e636 ffd3            call    ebx    ;调用最终的服务例程

表示在windbg里面跟了一边 清晰多了!但是windbg太不好看了!决定换回windbg preview?

画一手流程图:

ecx用来指定SYSEXIT返回用户模式时的目标地址,当使用INT 2E进行系统调用的时候,由于INT N的指令会i东将中断发生时的CS和EIP寄存器压入栈中,当中断处理例程通过执行iretd返回时,iretd指令会让栈中保存的cs和EIP返回合适的位置,但是因为SYSENTER指令不会向栈中压入要返回的位置,所以sysexit指令需要通过其他机制知道需要返回的位置。

书上给了一个例子,来加深这个系统调用的一个理解,因为栈上记录了函数相互调用时的参数和返回地址等信息,栈回溯是从栈上去找到这些信息,然后显示出来的过程。

可以看到第一列是序号,第二列是每个函数的栈帧基地址,第三列是返回地址,第四列是执行的位置。

逆向调用

之前我们知道了用户模式进入内核模式的两种方式,通过这两种形式,用户模式的代码可以调用位于内核模式的系统服务,如果我们想让内核模式的代码主动调用用户模式代码的时候,那么这种调用叫做逆向调用。 逆向调用的过程: 内核代码使用KiCallUserMode发起调用,接下来的执行的过程和系统返回的KiSystemCallExit类似,只是进入用户模式的时候执行的是NTDLL.dll中的KiUserCallbackDispatcher,而后该函数会调用内核希望调用的用户态函数,用户模式的工作完成后,执行返回动作的函数会执行INT 2B的指令,从而触发0x2B的异常。 可以知道对应的异常的函数是nt!KiCallbackReturn。

实例分析

如果要切换.process 一定要加上/i /p 这样cr3才会换回来,上下文才会过来。 可以看到,栈回溯是 nt!KeUserModeCallback -> KiCallUserMode -> KiUserCallbackDispatcher。 可以看到整个过程有之前说到的nt!KiFastCallEntry,通过服务号去找对应的函数,执行完后将消息组织好去调用KeUserModeCallback函数,从而进行逆向调用,用户模式状态下的过程没有体现出来,但是可以看到XyCallbackReturn函数用于返回内核模式,调用int 2B nt!KiCallbackReturn 返回到内核模式。

3

线程

线程是进程中的生命,一个进程内有一个或者多个线程,进程创建初期或者进程退出和销毁的过程中,进程内可能没有任何线程。

ETHREAD

NT内核使用ETHREAD结构来描述线程,在内核代码中,大多时候使用ETHREAD结构的地址来索引线程,执行.thread命令,就可以显示出当前线程的ETHREAD结构地址。 查看一下KTHREAD的结构:

包含了线程的各种的属性,ETHREAD开头的512字节是一个KTHREAD结构,也称作为线程控制块( TCB ),里面主要是供内核调度线程时使用的。 Header字段为Dispatcher_Header类型,是NT内核的线程调度器的别名,代表了分发CPU时间片的意思。

因为字段太多了,所以调试的时候谜一般不直接观察,使用扩展命令!thread,从而用这个扩展命令来更好的显示线程属性。

右上角的RUNNING on processor 0代表了这个线程正运行在0号CPU上,来源于KTHREAD结构的state字段。

为枚举类型KTHREAD_STATE,共有9个值从而代表了线程的状态。

可以看到当前的线程,在Running。

NT内核给每个CPU定义了一个名为处理控制块的PRCB的庞大结构,这个结构中有一个DispatcherReadyListHead的数组,包括了32个元素,代表了线程的32个优先级,每个元素是一个LIST_ENTRY的结构,从而起到链表头的作用,用来挂接对应优先级的就绪线程。

各个线程状态之间的切换关系: 上面所说的那个链表的事情找到了答案,我用的是xp的系统32位的,那时候的这个结构在TEB中的WaitListEntry中。

如果线程处于等待状态下,那么!thread命令会显示出等待的原因,在KTHREAD结构中有一个叫做WaitReason的字段。

从而记录线程等待的原因,长度为1字节,是枚举的类型,名为KWAIT_REASON,在公开的PDB符号文件中,包含了这个枚举类型的定义。

TEB

NT内核定义了线程环境快TEB来描述线程的用户空间的信息,包括了用户态栈,异常处理,错误码,线程局部存储等等。

windbg调试应用程序的时候,可以使用 !teb 来显示当前线程的teb 结构的位置,我用的是本机win10 挂载的 notepad。

NT内核会使用CPU的硬件机制来快速定位当前线程的TEB,所以内核在创建线程的时候,会分配专门的内存也来用作TEB,将地址记录在KTHREAD中,所以TEB的地址总是按照页对齐的。

同时也可以用dt _TEB的命令查看TEB的信息。

4

WOW进程

现在大多数的windows系统是64位的,运行在支持64位CPU的地方,但是为了兼容32位的应用程序,64位windows系统可以运行32位的应用程序,这样运行在64位内核上的32位进程有一个专门的名字,叫做WoW64(Windows 32 on Windows 64)进程。

架构:

由于32位的代码是不能与64位内核交互的,所以中间转换层为了解决这个问题设计,转接层本身为64位的模块,他给32位的应用程序营造了一个32位的环境,主要负责指针长度的转换和解决API兼容等问题。

工作过程

可以使用.effmach命令再两种代码间进行切换。

.effmach amd64切换到64位的模式下,

可以看到上面的wow64和wow64win都是转接层的核心的模块。

切换到32位下,可以看到再WoW的进程中,有两个ntdll的模块,一个是64位的,一个是32位的,WINDBG会在后加载进程的32位版本的模块名加上基地址。

在WoW的进程中,有很多的东西是双份的,每个进程有两个PEB,每个线程有两个TEB,有两个栈。

WINDBG的wow64exts扩展模块专门是为调试WoW进程从而设计的,info命令可以显示WoW进程的双份的资产。

Guest代表了32位的代码 Native指向的为64位的代码。

执行过程

可以看到需要call Wow64SystemServiceCall。 jmp Wow64Transition (KiFastSystemCall)

段选择子33 指向的是64位的段,这个就是从32位兼容模式到64位模式的方法。

我去查看了0x33段选择子 base是0 所以直接跳转到了0x773A7009的位置,我这里做了一个测试拿win7做的。 现在去逆以下wow64cpu!CpupReturnFromSimulatedCode 这里我个人推荐!直接去调notepad可以一点点看这个过程。

之前的cs的状态是23的状态,因为经过跳转的过程变成33。

可以看到Long进行了改变,interl手册的段描述符中的高21位置是L位代表了是32位还是64位。

可以看到gdtr表中他们的不同。

可以看到当前的esp装的返回地址,那么+4放下推的话就是call之前的参数-

00000000`754f2723 458985bc000000  mov     dword ptr [r13+0BCh],r8d    ;可以看到r13是64位环境的栈底,保存了32位的返回地址
00000000`754f272a 4189a5c8000000  mov     dword ptr [r13+0C8h],esp    ;保存了32位的时候的栈帧

00000000`754f2723 458985bc000000  mov     dword ptr [r13+0BCh],r8d    ;可以看到r13是64位环境的栈底,保存了32位的返回地址
00000000`754f272a 4189a5c8000000  mov     dword ptr [r13+0C8h],esp    ;保存了32位的时候的栈帧

但是这里有个疑问!0x1480是个什么,直接去看一下TEB的结构!

发现叫做TlsSlots,但是TlsSlots是什么东东!待我去学习一下!

http://www.nynaeve.net/?p=181

是个线程槽了,这里存储了64位线程的一些关键环境以及32位线程的关键数据。


00000000`754f2731 498ba42480140000   mov     rsp,qword ptr [r12+1480h]    ;r12为teb的地址,这里取出了64位的rsp
00000000`754f2739 4983a4248014000000 and   qword ptr [r12+1480h],0         ;清空缓存

edx存放的是参数的地址:

00000000`754f2742 448bda          mov     r11d,edx        ;r11d存放的是参数的地址

可以看到r15是一个表rcx相当于跳转的服务号。

00000000`754f2745 41ff24cf        jmp     qword ptr [r15+rcx*8]        ;跳转

跳转后的环境:

syscall会跳到:

可以看出来WoW进程中的32版本的NTDLL.DLL 在执行系统调用的时候,会调用特殊的WoW64SystemServiceCall函数,切换到64位的WoW转换层。

注册表重定向

64位的windows操作系统会对WoW进程的注册表访问实施重定向,如果程序中访问的路径为HKEY_LOCAL_MACHINE\software,那么就会被重定向到HKEY_LOCAL_MACHINE\software\Wow6432Node,所以i使用注册表编辑器的时候,如果要查看WoW进程的设置,那么也要去查看Wow6432Node表键下的,有一部分的表键是供两类程序共享的,有一类则是重定向的。

test代码:

#include <windows.h>
#include <stdio.h>

int main()
{
    //找到系统的启动项
    char* Register = "Software\\Microsoft\\Windows\\CurrentVersion\\Run";
    char* Myapp = "C:\\Users\\Administrator\\Desktop\\ctfmon.exe";
    HKEY hKey;

    //打开注册表启动项
    if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, Register, 0, KEY_ALL_ACCESS, &hKey) == ERROR_SUCCESS)
    {
        RegSetValueExA(hKey, "Mytest", 0, REG_SZ, (BYTE*)Myapp, strlen(Myapp));
        //关闭注册表
        RegCloseKey(hKey);
        printf("succeed!\n");
    }
    else
    {
        printf("Failed!");
        return -1;

    }
    return 0;
}

注册表反射

考虑到COM组件,有32位的也有64位的,为了让用户再一个版本中所做的设置在另一个版本中也有效,所以windows实现了一种叫做注册表反射的机制,对于某些COM组件有关的表键,来自一边的修改会自动更新到另一边。

5

创建进程

我们打开一个新的程序,windows会使用一套标准的流程来创建一个新进程:

1、在父进程的用户空间中打开要执行的映像文件,确定其名称,类型和系统对它的设置选项

2、进入父进程的内核空间,为新进程创建EPROCESS结构,进程地址空间,KPROCESS结构和PEB

3、创建初始线程,但是创建的时候指定了挂起标志,并不会立刻开始运行

4、通知子系统服务程序,对于windows程序,通知windows子系统服务进程CSRSS

5、初始线程开始在内核空间执行

6、通过APC的方式,在新进程自己的空间中执行初始化动作,主要是通过NTDLL.DLL中的加载器,加载进程所依赖的DLL文件

Windows Internals上面详细的介绍了!但是我去翻了没太看懂,有点蒙。 到时候写Windows Internals笔记的时候 会好好研究!写出来的东西让更多人明白一下!

6

最小进程和Pico进程

对于正常的NT进程,NT内核会自动创建一些设施并且将他们映射到进程的用户空间当中,如PEB,TEB等等,因为考虑到NTDLL.DLL的特殊,NT内核也会自动将NTDLL.DLL映射到普通进程用户模式空间中,对于某些特殊的情况,这些的动作可能是多余而带有副作用的,所以设计了最小进程和Pico进程。

这里,最小进程和Pico进程推荐自己看书奥!这里没有好的dump文件比较难搞,日后有时间补上。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8