Windows下printf多进程架构及内核态—用户态多级调试分析

阅读量    137150 | 评论 1

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

0、主要内容

全文围绕着微软底层是如何实现printf的这个宗旨,从应用程序开始着手分析,一直到内核层,进行双机调试,顺藤摸瓜,追寻数据的流向,又从内核回到用户态程序,接着又依据内核态调试时获知的信息,对用户态另一个进程进行分析,抓出了一系列的信息,使得这个问题越来越清楚,完完全全将printf的实现过程大白于天下。

涉及到的内容如下:

1、内核对象及内核对象管理;
2、设备驱动程序及驱动程序对象;
3、MDL;
4、用户态程序与内核驱动通信DeviceIoControl;
5、Windbg调试;
6、C运行时库代码分析;

 

1、背景

作为程序员,printf这个函数肯定是不陌生的,刚学C语言那会,第一个程序基本也都是经典的printf(“hello world\n”)吧,一用十几年了,但重来都没有深究过背后的实现原理,只知道他是C语言标准库规定的,用起来很爽。最近几天恰好得闲,于是花了点时间把这个问题给搞清楚了;整个过程非常有意思,本以为简单分析下微软随IDE一起公布的C运行时库代码就能搞清楚的,可越分析越发现仅仅依靠这点源码压根解不开谜团,或者说,C运行时库关于printf的部分仅仅只有一个核心的API,遂搭建双机调试,进行内核分析;

 

2、分析过程之源码部分分析

2.1实例代码如下,很简单;IDE是VS2017;这部分源码较多,大家耐心看完

#include <stdio.h>
#include <Windows.h>

int _tmain(int argc, _TCHAR* argv[])
{
    while(1)
    {
        printf("hello world\n");
        Sleep(500);
    }

    return 0;
}

每隔500ms就打印一下字符串“hello world\n”;下边来跟一下VC运行时库的源码;源码步骤比较繁琐,如果不感兴趣可直接略过,看后边的结论;总结起来就是一句话printf——->WriteFile();

2.2源码分析

经过千山万水,终于看到曙光了,总结起来就是一句话printf——->WriteFile();源码面前,了无密码,现在最关心的是这个WriteFile()写入的文件到底是个什么神仙文件?

 

3、分析部分之内核调试分析——写

根据最后一幅图的os_handle这个数据可知,句柄值为0x0C,现在来看下这个句柄到底是个啥。借助Procexp.exe工具,如下:

光看这名字就不想普通的文件,如果是普通的文件的话,应该是有磁盘路径的,这显然是内核驱动创建的一个设备对象,那这玩意到底是啥呢?这才是今天的重点,且往下看;这时我们需要双机调试了,这玩意在内核里;把这程序拷贝到虚拟机,运行,然后用Windbg查数据;

查看下该对象的具体信息,如下所示:

1: kd> !object 0xFFFFCC8BDCEA4EF0
Object: ffffcc8bdcea4ef0  Type: (ffffcc8bd88cdb20) File
    ObjectHeader: ffffcc8bdcea4ec0 (new version)
    HandleCount: 2  PointerCount: 64696
    Directory Object: 00000000  Name: \Output {ConDrv}
1: kd> dt _OBJECT_HEADER ffffcc8bdcea4ec0
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n64696
   +0x008 HandleCount      : 0n2
   +0x008 NextToFree       : 0x00000000`00000002 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x50 'P'
   +0x019 TraceFlags       : 0 ''
   +0x019 DbgRefTrace      : 0y0
   +0x019 DbgTracePermanent : 0y0
   +0x01a InfoMask         : 0x4c 'L'
   +0x01b Flags            : 0 ''
   +0x01b NewObject        : 0y0
   +0x01b KernelObject     : 0y0
   +0x01b KernelOnlyAccess : 0y0
   +0x01b ExclusiveObject  : 0y0
   +0x01b PermanentObject  : 0y0
   +0x01b DefaultSecurityQuota : 0y0
   +0x01b SingleHandleEntry : 0y0
   +0x01b DeletedInline    : 0y0
   +0x01c Reserved         : 0xffffb68d
   +0x020 ObjectCreateInfo : 0xffffcc8b`db7aed80 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0xffffcc8b`db7aed80 Void
   +0x028 SecurityDescriptor : (null)
   +0x030 Body             : _QUAD

里边的很多字段暂时不用管,后边会专门撰文写Windows内核里对象管理的实现原理,但有一个信息是值得我们关注的,就是这个对象的类型是File,即文件;简单说明下,在Windows内核里,对象都是有类型的,就像应用层一样,每个对象都有其所对应的类类型,进程的对象类型为Process,线程的对象类型为Thread,等等,自然的文件对象的类型即为File了;那下边具体看下这个文件有什么特别的,且看下边的操作:

1: kd> dt _FILE_OBJECT ffffcc8bdcea4ef0
nt!_FILE_OBJECT
   +0x000 Type             : 0n5
   +0x002 Size             : 0n216
   +0x008 DeviceObject     : 0xffffcc8b`dbee7b20 _DEVICE_OBJECT
   +0x010 Vpb              : (null)
   +0x018 FsContext        : 0xffffb68d`59de5b30 Void
   +0x020 FsContext2       : 0xffffcc8b`dacca230 Void
   +0x028 SectionObjectPointer : (null)
   +0x030 PrivateCacheMap  : (null)
   +0x038 FinalStatus      : 0n0
   +0x040 RelatedFileObject : 0xffffcc8b`dd122550 _FILE_OBJECT
   +0x048 LockOperation    : 0 ''
   +0x049 DeletePending    : 0 ''
   +0x04a ReadAccess       : 0 ''
   +0x04b WriteAccess      : 0 ''
   +0x04c DeleteAccess     : 0 ''
   +0x04d SharedRead       : 0 ''
   +0x04e SharedWrite      : 0 ''
   +0x04f SharedDelete     : 0 ''
   +0x050 Flags            : 0x10040002
   +0x058 FileName         : _UNICODE_STRING "\Output"
   +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x070 Waiters          : 0
   +0x074 Busy             : 0
   +0x078 LastLock         : (null)
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT
   +0x0b0 CompletionContext : (null)
   +0x0b8 IrpListLock      : 0
   +0x0c0 IrpList          : _LIST_ENTRY [ 0xffffcc8b`dcea4fb0 - 0xffffcc8b`dcea4fb0 ]
   +0x0d0 FileObjectExtension : (null)

确实挺特殊的,绝大部分字段都没有数据;但与该文件相关联的设备对象值得我们去探究下,如下:

1: kd> dt 0xffffcc8b`dbee7b20 _DEVICE_OBJECT
nt!_DEVICE_OBJECT
   +0x000 Type             : 0n3
   +0x002 Size             : 0x150
   +0x004 ReferenceCount   : 0n10
   +0x008 DriverObject     : 0xffffcc8b`dc4fa200 _DRIVER_OBJECT
   +0x010 NextDevice       : (null)
   +0x018 AttachedDevice   : (null)
   +0x020 CurrentIrp       : (null)
   +0x028 Timer            : (null)
   +0x030 Flags            : 0x50
   +0x034 Characteristics  : 0x20000
   +0x038 Vpb              : (null)
   +0x040 DeviceExtension  : (null)
   +0x048 DeviceType       : 0x50
   +0x04c StackSize        : 2 ''
   +0x050 Queue            : <unnamed-tag>
   +0x098 AlignmentRequirement : 0
   +0x0a0 DeviceQueue      : _KDEVICE_QUEUE
   +0x0c8 Dpc              : _KDPC
   +0x108 ActiveThreadCount : 0
   +0x110 SecurityDescriptor : 0xffffb68d`575f0380 Void
   +0x118 DeviceLock       : _KEVENT
   +0x130 SectorSize       : 0
   +0x132 Spare1           : 0
   +0x138 DeviceObjectExtension : 0xffffcc8b`dbee7c70 _DEVOBJ_EXTENSION
   +0x140 Reserved         : (null)

设备对象在Windows内核里即可以表征一个实实在在的硬件设备,也可以是虚拟出来的一个假的设备,这就是Windows内核分层设计的妙处所在,好了设备仅仅是指代硬件,而该硬件具有哪些功能,则是由其关联的驱动对象所表征的,下边我们再看下其关联的驱动对象:

1: kd> dt 0xffffcc8b`dc4fa200 _DRIVER_OBJECT
nt!_DRIVER_OBJECT
   +0x000 Type             : 0n4
   +0x002 Size             : 0n336
   +0x008 DeviceObject     : 0xffffcc8b`dbee7b20 _DEVICE_OBJECT
   +0x010 Flags            : 0x12
   +0x018 DriverStart      : 0xfffff802`14530000 Void
   +0x020 DriverSize       : 0x12000
   +0x028 DriverSection    : 0xffffcc8b`dc472cf0 Void
   +0x030 DriverExtension  : 0xffffcc8b`dc4fa350 _DRIVER_EXTENSION
   +0x038 DriverName       : _UNICODE_STRING "\Driver\condrv"
   +0x048 HardwareDatabase : 0xfffff802`151f4e38 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
   +0x050 FastIoDispatch   : 0xfffff802`14534020 _FAST_IO_DISPATCH
   +0x058 DriverInit       : 0xfffff802`1453e010     long  +fffff8021453e010
   +0x060 DriverStartIo    : (null)
   +0x068 DriverUnload     : 0xfffff802`1453c8e0     void  +fffff8021453c8e0
   +0x070 MajorFunction    : [28] 0xfffff802`14537e10     long  +fffff80214537e10

该驱动对象的名字叫”\Driver\condrv”,跟之前的设备对象的名字还挺呼应的;对于驱动对象来说,最重要的要说MajorFunction数组里放着的那些个例程了;我们来看下这些历程中比较重要的一个

回调例程的函数原型如下:

typedef NTSTATUS DRIVER_DISPATCH (__in struct _DEVICE_OBJECT *DeviceObject, __inout struct _IRP *Irp);

下一个断点看看,命中之后能获取哪些有用的信息;

1: kd> bp 0xfffff802145382a0
1: kd> g
1: kd> k
# Child-SP          RetAddr           Call Site
00 ffffcb80`4ec07808 fffff802`14a428d9 0xfffff802`145382a0
01 ffffcb80`4ec07810 fffff802`14ed755e nt!IofCallDriver+0x59
02 ffffcb80`4ec07850 fffff802`14ed8ca0 nt!IopSynchronousServiceTail+0x19e
03 ffffcb80`4ec07900 fffff802`14b79553 nt!NtWriteFile+0x8b0
04 ffffcb80`4ec07a10 00000000`6e5e1e5c nt!KiSystemServiceCopyEnd+0x13
05 00000000`00aeead8 00000000`6e5e1b3a 0x6e5e1e5c
06 00000000`00aeeae0 00000023`774de7bc 0x6e5e1b3a
07 00000000`00aeeae8 00000000`6e580023 0x00000023`774de7bc
08 00000000`00aeeaf0 00000000`00000000 0x6e580023

断下来了,也确实看到nt!NtWriteFile了,但这个是内核态的并不是用户态的,我这里pdb路径没有设,设置正确的话,栈是完美的,不过这不影响我们的分析过程;要想直接看nt!NtWriteFile的参数比较麻烦,因为x64架构下的内核是通过寄存器传递参数的,这样需要手动去分析参数,比较麻烦,不过没关系,数据还在,回头看下回调函数的例程原型,第二个参数为IRP*,这个里边有我们想要的东西;来看下:

1: kd> !irp ffffcc8bdc6addc0
Irp is active with 2 stacks 2 is current (= 0xffffcc8bdc6aded8)
Mdl=ffffcc8bdd351330: No System Buffer: Thread ffffcc8bd9c4f2c0:  Irp stack trace.  
     cmd  flg cl Device   File     Completion-Context
[N/A(0), N/A(0)]
            0  0 00000000 00000000 00000000-00000000    
            Args: 00000000 00000000 00000000 00000000
>[IRP_MJ_WRITE(4), N/A(0)]
            0  0 ffffcc8bdbee7b20 ffffcc8bdcea4ef0 00000000-00000000    
           \Driver\condrv
            Args: 0000000d 00000000 00000000 00000000

重点关注其中的Mdl=ffffcc8bdd351330这行,来继续追踪下数据,这里稍微拓展下,MDL的全称是Memory Descriptor List,即内存 描述链表,是内核里常用来维护缓冲区内存用的一种结构,是个单项链表,借此我们正好来看下数据:

1: kd> dt _mdl ffffcc8bdd351330
nt!_MDL
   +0x000 Next             : (null)
   +0x008 Size             : 0n56
   +0x00a MdlFlags         : 0n266
   +0x00c AllocationProcessorNumber : 1
   +0x00e Reserved         : 0
   +0x010 Process          : 0xffffcc8b`dacf9200 _EPROCESS
   +0x018 MappedSystemVa   : 0xffff9401`ce12f940 Void
   +0x020 StartVa          : 0x00000000`00bee000 Void
   +0x028 ByteCount        : 0xd
   +0x02c ByteOffset       : 0x3a4

各个字段的解释如下:

Next: 指向下一个MDL结构,从而构成链表,有时一个IRP会包含多个MDL;
Size: MDL本身的大小,注意包含了定长部分和变长两部分的size;
MdlFlags:属性标记,如所描述的物理页有没有被lock住等;
Process: 顾名思义,指向该包含该虚拟地址的地址空间的对应进程结构;
MappedSystemVa:内核态空间中的对应地址;
StartVa: 用户或者内核地址空间中的虚拟地址,取决于在哪allocate的,该值是页对齐的;
ByteCount:MDL所描述的虚拟地址段的大小,byte为单位;
ByteOffset:起始地址的页内偏移,因为MDL所描述的地址段不一定是页对齐的;
MdlFlags标志取值如下图,而这里的取值是0n266=0x10a;

参照下图可知,内核虚拟地址空间还没有分配,没关系,先看下用户态的数据是啥,先弥补下前边查看nt!NtWriteFile不方便而导致的没能查看数据的不快;

1: kd> db 0x00000000`00bee000+0x3a4 ld
00000000`00bee3a4  68 65 6c 6c 6f 20 77 6f-72 6c 64 0d 0a           hello world..

正好是我们printf输出的字符串,“hello world\n”,到目前为止一切还在掌控中;那内核总会不一直不分配内核空间吧,因为只要进程切换了,CR3就换了,页表就换了,用户态的数据就有可能访问不到了,所以下一步我们就看下内核合适给MDL.MappedSystemVa 字段挂上数据;指向合适的内核内存空间;方法如下:

1: kd> ba r8 ffffcc8bdd351330+18
1: kd> g
Breakpoint 1 hit
nt!MmMapLockedPagesSpecifyCache+0x16a:
fffff802`14a58fea 83e601          and     esi,1
1: kd> dt _mdl ffffcc8bdd351330
nt!_MDL
   +0x000 Next             : (null)
   +0x008 Size             : 0n56
   +0x00a MdlFlags         : 0n267
   +0x00c AllocationProcessorNumber : 1
   +0x00e Reserved         : 0
   +0x010 Process          : 0xffffcc8b`dacf9200 _EPROCESS
   +0x018 MappedSystemVa   : 0xffff9401`cea433a4 Void
   +0x020 StartVa          : 0x00000000`00bee000 Void
   +0x028 ByteCount        : 0xd
   +0x02c ByteOffset       : 0x3a4

数据设置好了,除了MappedSystemVa 字段被安排了合适的值,MdlFlags 字段也发生了改变;即多了项MDL_MAPPED_TO_SYSTEM_VA;赶紧来看下数据对不对:

1: kd> db 0xffff9401`cea433a4
ffff9401`cea433a4  68 65 6c 6c 6f 20 77 6f-72 6c 64 0d 0a ea be 00  hello world.....
ffff9401`cea433b4  c0 ea be 00 50 00 00 00-d4 e3 be 00 cc 30 11 03  ....P........0..

顺便提一下,大家看下下图,最后一级的pfn居然一样,奇不奇怪?一点都不奇怪,本来就是两个虚拟地址映射到同一份物理页:

OK了,到目前为止,我们知道了printf———>WriteFile——->NtWriteFile———>DriverObject.Write例程;下边我们需要知道,谁来读取这个数据呢?

 

4、分析部分之内核调试分析——读

接着上边的,在MappedSystemVa所指向的虚拟内存地址设置一个访问断点,看看谁来处理该数据的,如下:
1: kd> ba r4 0xffff9401`cea433a4
1: kd> g
Breakpoint 2 hit
fffff802`14531424 48ffc9          dec     rcx
1: kd> kb
# RetAddr           : Args to Child                                                           : Call Site
00 fffff802`1453991d : 00000000`00000000 fffff802`14aac8e9 ffffcc8b`00000000 ffffcb80`4ec076f8 : 0xfffff802`14531424
01 00000000`00000000 : fffff802`14aac8e9 ffffcc8b`00000000 ffffcb80`4ec076f8 ffffb68d`6aa157c0 : 0xfffff802`1453991d

栈不完美,没关系,我们来看看当前的进程是哪个。要查看当前的进程是哪个,方法有很多,下边就给出两种方法,看官自取:

方法1:

1: kd> dt _EPROCESS @$proc -yn ImageF
nt!_EPROCESS
   +0x448 ImageFilePointer : 0xffffcc8b`dea4ad10 _FILE_OBJECT
   +0x450 ImageFileName : [15]  "work.exe"

方法2:

1: kd> !pcr
KPCR for Processor 1 at ffff9401cdcc0000:
    Major 1 Minor 1
    NtTib.ExceptionList: ffff9401cdcd0fb0
        NtTib.StackBase: ffff9401cdccf000
       NtTib.StackLimit: 0000000000aeead8
     NtTib.SubSystemTib: ffff9401cdcc0000
          NtTib.Version: 00000000cdcc0180
      NtTib.UserPointer: ffff9401cdcc0870
          NtTib.SelfTib: 0000000000cac000
                SelfPcr: 0000000000000000
                   Prcb: ffff9401cdcc0180
                   Irql: 0000000000000000
                    IRR: 0000000000000000
                    IDR: 0000000000000000
          InterruptMode: 0000000000000000
                    IDT: 0000000000000000
                    GDT: 0000000000000000
                    TSS: 0000000000000000
          CurrentThread: ffffcc8bd9c4f2c0
             NextThread: ffffcc8bdb76d380
             IdleThread: ffff9401cdcccb40
              DpcQueue: Unable to read nt!_KDPC_DATA.DpcListHead.Flink @ ffff9401cdcc2f80

1: kd> !thread ffffcc8bd9c4f2c0
THREAD ffffcc8bd9c4f2c0  Cid 2744.1ea8  Teb: 0000000000cac000 Win32Thread: 0000000000000000 RUNNING on processor 1
IRP List:
    ffffcc8bdc6addc0: (0006,0238) Flags: 00060a00  Mdl: ffffcc8bdd351330
Not impersonating
DeviceMap                 ffffb68d596246e0
Owning Process            ffffcc8bdacf9200       Image:         work.exe
Attached Process          N/A            Image:         N/A
Wait Start TickCount      146715         Ticks: 3 (0:00:00:00.046)
Context Switch Count      1518           IdealProcessor: 1             
UserTime                  00:00:00.046
KernelTime                00:00:00.640
Win32 Start Address work!ILT+110(_wmainCRTStartup) (0x0000000000841073)
Stack Init ffffcb804ec07c10 Current ffffcb804ec069a0
Base ffffcb804ec08000 Limit ffffcb804ec01000 Call 0000000000000000
Priority 8 BasePriority 8 PriorityDecrement 0 IoPriority 2 PagePriority 5
Child-SP          RetAddr           : Args to Child                                                           : Call Site
ffffcb80`4ec07698 fffff802`1453991d : 00000000`00000000 fffff802`14aac8e9 ffffcc8b`00000000 ffffcb80`4ec076f8 : 0xfffff802`14531424
ffffcb80`4ec076a0 00000000`00000000 : fffff802`14aac8e9 ffffcc8b`00000000 ffffcb80`4ec076f8 ffffb68d`6aa157c0 : 0xfffff802`1453991d

嗯,还是work.exe自己,看看是不是复制之类的操作;看汇编下附近的代码:

1: kd> ?rdx+rcx-1
Evaluate expression: -118739493964889 = ffff9401`cea433a7
1: kd> db ffff9401`cea433a7
ffff9401`cea433a7  6c 6f 20 77 6f 72 6c 64-0d 0a ea be 00 c0 ea be  lo world........
1: kd> db rcx
ffff9401`cea44d54  6f 20 77 6f 72 6c 64 0d-0a 00 00 00 9c eb be 00  o world.........

确实是在复制字符串,这个不管,多几次g,断下来之后,看下进程名,调整断点如下:

1: kd> ba r4 0xffff9401`cdf953a4 "dt @$proc _EPROCESS -yn Image"

多执行机制g命令之后,如下图所示,出现了另一个进程也来读取这个数据:

简单看下这个进程当前的线程信息,如下:

可以知道的信息有线程的ID,线程的Teb信息,有了这些,直接用用户态调试器直接调试即可,但已经用到了内核调试器,那就简单看下当前的线程在干啥吧,看下他的用户态栈;大致浏览下信息,看看有没有什么特别的API调用;

好,接下来转战用户态调试器;如果大家对内核调试熟悉的话,完全可以直接用内核态调试器直接调试用户态程序,也没多麻烦;

 

5、分析部分之用户态conhost.exe进程行为分析

先来看下DeviceIoControl()函数原型:

BOOL DeviceIoControl(
  HANDLE       hDevice,
  DWORD        dwIoControlCode,
  LPVOID       lpInBuffer,
  DWORD        nInBufferSize,
  LPVOID       lpOutBuffer,
  DWORD        nOutBufferSize,
  LPDWORD      lpBytesReturned,
  LPOVERLAPPED lpOverlapped
);

参数解释请见https://docs.microsoft.com/zh-cn/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol

x64下,函数参数传递前4个参数是通过cd89寄存器传递的,剩余的通过栈传递,下边来找一下这几个参数:

下边来简单看下传输的缓冲区数据,看不出啥,这个需要去逆向分析通信协议了,不是我们关注的重点,那就让这个函数执行完,我们看看输出的内容吧:

0:000> dd 000000cbe9a7fcd0
000000cb`e9a7fcd0  011c5524 00000000 00000000 00000000
000000cb`e9a7fce0  00000000 00000000 e9a7fd58 000000cb
000000cb`e9a7fcf0  00000004 00000000


原来conhost.exe是通过这个DeviceIoControl()API通过500006这个控制码跟驱动要的数据;
至此整够过程全部分析完毕;

 

6、总结

本文从printf的源码层层深入分析,到驱动的调试逆向分析,再到conhost.exe进程的数据获取过程的详细分析;本文涉及到的知识点比较多;总结起来有以下几点:

1、printf源码的调试跟踪,如何定位观点点;
2、内核对象管理,设备对象,驱动对象及主要的例程;
3、MDL;
4、内核调试;
5、用户态调试;
6、用户态程序通过DeviceIoControl()与内核驱动交互,获取特定数据;
7、printf实现的多进程架构;涉及的内容比较多,希望读者花点时间好
6、用户态程序通过DeviceIoControl()与内核驱动交互,获取特定数据;
7、printf实现的多进程架构;

好整理总结;

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多