一:背景

1.讲故事

有朋友咨询个问题,他每次在调试 WinDbg 的时候,进程初始化断点之前都会有一些 dll 加载到进程中,比如下面这样:


Microsoft (R) Windows Debugger Version 10.0.25200.1003 X86
Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: D:\net6\ConsoleApp1\Debug\ConsoleApplication3.exe ************* Path validation summary **************
Response Time (ms) Location
Deferred srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
Symbol search path is: srv*c:\mysymbols*https://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 0041f000 ConsoleApplication3.exe
ModLoad: 774b0000 77653000 ntdll.dll
ModLoad: 753a0000 75490000 C:\Windows\SysWOW64\KERNEL32.DLL
ModLoad: 75900000 75b14000 C:\Windows\SysWOW64\KERNELBASE.dll
ModLoad: 79bc0000 79d36000 C:\Windows\SysWOW64\ucrtbased.dll
ModLoad: 79ba0000 79bbe000 C:\Windows\SysWOW64\VCRUNTIME140D.dll
(44c.4b0c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=afe00000 edx=00000000 esi=774c1ff4 edi=774c25bc
eip=77561a42 esp=0019fa20 ebp=0019fa4c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2b:
77561a42 cc int 3

问是否可以用 WinDbg 解读下内部运作原理,哈哈,其实要了解运作原理,一定要熟知 PE 头,那这篇就安排上。

二:理解 PE 头结构

1. 测试代码

为了方便讲述,先上一段测试代码,这里故意加载 combase.dll 是为了提取 PE 中的某些数据结构,代码如下:


#include <iostream>
#include <Windows.h> int main(int argc, char* argv[]) { LoadLibrary(L"combase.dll"); getchar();
}

其实你仔细想一想也能知道,既然能做到初始化加载,必然在 PE 头上藏了什么东西,这些东西让 Windows 加载器可以顺利加载诸如 ntdll.dll, KERNEL32.dll 等等,接下来一起观察下。

2. 可视化观察 PE 头

要想可视化观察 PE 头,工具有很多,这里使用 PPEE 工具,截图如下:

从图中可以看到,其实初始化加载什么,由可选头中的 DIRECTORY_ENTRY_IMPORT 数据目录项决定,哪这里包含了哪些初始化 dll 呢? 可以选中右边的 DIRECTORY_ENTRY_IMPORT 项即可,如下图所示:

肯定有朋友说,WinDbg 上显示的是 5 个,你这里才 3 个,还有 2 个为什么没有? 很简单,多余的 ntdll.dllKERNELBASE.dll 必然是依赖项哈。

3. 用 WinDbg 深入探究

玩 WinDbg 都喜欢刨根问底,拿可视化 PPEE 肯定忽悠不过去,那好吧,我们用 C 中的结构体去解剖它。

  1. DOS Header 节

这一块信息在源码中是用 ntdll!_IMAGE_DOS_HEADER 结构来承载的,可以用 dt 输出,起始点就是我们的 ConsoleApplication3.dll 在进程的首位置,即: 0x400000


0:000> dt 0x400000 _IMAGE_DOS_HEADER
ConsoleApplication3!_IMAGE_DOS_HEADER
+0x000 e_magic : 0x5a4d
+0x002 e_cblp : 0x90
+0x004 e_cp : 3
...
+0x024 e_oemid : 0
+0x026 e_oeminfo : 0
+0x028 e_res2 : [10] 0
+0x03c e_lfanew : 0n232
  1. NT Header 节

接下来就是 NT Header 节,它在源码中是由 _IMAGE_NT_HEADERS 结构来承载的,起始位置的偏移已经保存在上面的 e_lfanew 字段中,即 0n232


0:000> dt 0x400000+0n232 _IMAGE_NT_HEADERS
ConsoleApplication3!_IMAGE_NT_HEADERS
+0x000 Signature : 0x4550
+0x004 FileHeader : _IMAGE_FILE_HEADER
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER
  1. _IMAGE_DATA_DIRECTORY

在 PPEE 的第一张截图中,我们查看的是 Data Directorys 数组中的第二项 DIRECTORY_ENTRY_IMPORT 内容,它里面定义了我们需要初始化导入的 dll,我们可以用 dt r3 展开一下,然后一直点点点就好了,简化后如下:


0:000> dt -r3 0x400000+0n232 _IMAGE_NT_HEADERS
ConsoleApplication3!_IMAGE_NT_HEADERS
+0x000 Signature : 0x4550
+0x004 FileHeader : _IMAGE_FILE_HEADER
+0x000 Machine : 0x14c
...
+0x012 Characteristics : 0x103
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER
+0x000 Magic : 0x10b
+0x002 MajorLinkerVersion : 0xe ''
...
+0x05c NumberOfRvaAndSizes : 0x10
+0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress : 0
+0x004 Size : 0
0:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160))
(*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY (*)[16])0x400160)) [Type: _IMAGE_DATA_DIRECTORY [16]]
[0] [Type: _IMAGE_DATA_DIRECTORY]
[1] [Type: _IMAGE_DATA_DIRECTORY]
[2] [Type: _IMAGE_DATA_DIRECTORY]
...
[15] [Type: _IMAGE_DATA_DIRECTORY] 0:000> dx -r1 (*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168))
(*((ConsoleApplication3!_IMAGE_DATA_DIRECTORY *)0x400168)) [Type: _IMAGE_DATA_DIRECTORY]
[+0x000] VirtualAddress : 0x1b1cc [Type: unsigned long]
[+0x004] Size : 0x50 [Type: unsigned long]

从输出的 VirtualAddress=0x1b1cc 中可以看到,我们 PPEE 截图二中的 DIRECTORY_ENTRY_IMPORT 真实内容是在偏移 0x1b1cc 处,它是一个 combase!_IMAGE_IMPORT_DESCRIPTOR 结构体,输出如下:


0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
+0x000 Characteristics : 0x1b21c
+0x000 OriginalFirstThunk : 0x1b21c
+0x004 TimeDateStamp : 0
+0x008 ForwarderChain : 0
+0x00c Name : 0x1b40c
+0x010 FirstThunk : 0x1b000

到这里就很关键了,涉及到如下几点信息:

  • 加载的 dll 名字是什么?

可以从 Name 字段提取,参考如下代码:


0:005> da 0x400000+0x1b40c
0041b40c "KERNEL32.dll"
  • 加载的 方法名 是什么?

这需要提取 OriginalFirstThunk 字段,这里是一个 _IMAGE_IMPORT_BY_NAME类型的指针数组,代码如下:


0:005> dp 0x400000+0x1b21c
0041b21c 0001b3e8 0001b3fc 0001b954 0001b942
0041b22c 0001b934 0001b924 0001b912 0001b906
0041b23c 0001b8fa 0001b8ea 0001b8d6 0001b8ba
... 0:005> dt 0x400000+0x0001b3e8 combase!_IMAGE_IMPORT_BY_NAME
+0x000 Hint : 0x382
+0x002 Name : [1] "I" 0:005> da 0x400000+0x0001b3e8+0x2
0041b3ea "IsDebuggerPresent"

结合上面的输出,我们知道 IsDebuggerPresent() 是属于 KERNEL32.dll 下的,有了这两点信息,Windows 加载器就可以用 LoadLibraryGetProcAddress 方法将其加载到进程中了,转化为 C 代码大概是这样的。


typedef BOOL(CALLBACK* DeubbgerFunc)(); int main(int argc, char* argv[])
{
HMODULE hModule = LoadLibrary(L"KERNEL32.dll"); DeubbgerFunc func = (DeubbgerFunc)GetProcAddress(hModule, "IsDebuggerPresent"); BOOL b= func();
}
  • func 函数地址会保存吗?

当然会保存了,会放在 _IMAGE_IMPORT_DESCRIPTOR 结构下的 FirstThunk 字段中,这是一个函数地址的指针数组,可以用 dds 观察。


0:005> dt 0x400000+0x1b1cc combase!_IMAGE_IMPORT_DESCRIPTOR
+0x000 Characteristics : 0x1b21c
+0x000 OriginalFirstThunk : 0x1b21c
+0x004 TimeDateStamp : 0
+0x008 ForwarderChain : 0
+0x00c Name : 0x1b40c
+0x010 FirstThunk : 0x1b000 0:005> dds 0x400000+0x1b000
0041b000 753c20d0 KERNEL32!IsDebuggerPresentStub
0041b004 753c16c0 KERNEL32!LoadLibraryWStub
0041b008 753c2e80 KERNEL32!GetCurrentProcess
0041b00c 753bf550 KERNEL32!GetProcAddressStub
0041b05c 753b9910 KERNEL32!TerminateProcessStub
...

还有一点要注意,如果你在代码中使用 IsDebuggerPresent() 方法的话,它会从 0041b000 位置上取函数地址,参考如下汇编代码:

三:总结

对初学者来说,搞懂这些还是有一定困难的,我在网上找了一份很好的参考图,大家可以对照着这张图理解,在此感谢作者。

  • INT 是 Windows 需要加载的函数名列表。
  • IAT 是存放 GetProcAddress 返回函数地址的列表。

WinDBG详解进程初始化dll是如何加载的的更多相关文章

  1. JDBC详解系列(二)之加载驱动

    ---[来自我的CSDN博客](http://blog.csdn.net/weixin_37139197/article/details/78838091)---   在JDBC详解系列(一)之流程中 ...

  2. 详解web.xml中元素的加载顺序

    一.背景 最近在项目中遇到了启动时出现加载service注解注入失败的问题,后来经过不懈努力发现了是因为web.xml配置文件中的元素加载顺序导致的,那么就抽空研究了以下tomcat在启动时web.x ...

  3. java环境变量详解---找不到或无法加载主类

    默认安装在C:\ProgramFiles\Java\jdk1.7.0目录下环境变量配置为PATH=.;%JAVA_HOME%\binCLASSPATH=.;%JAVA_HOME%\lib\dt.jar ...

  4. Asp.Net 中Grid详解两种方法使用LigerUI加载数据库数据填充数据分页

    1.关于LigerUI: LigerUI 是基于jQuery 的UI框架,其核心设计目标是快速开发.使用简单.功能强大.轻量级.易扩展.简单而又强大,致力于快速打造Web前端界面解决方案,可以应用于. ...

  5. 【转】详解web.xml中元素的加载顺序

    顺序为: context-param --> listeners --> filters --> servlets(如DispatcherServlet等) 详见<https: ...

  6. phpExcel常用方法详解【附有php导出excel加超级链接】

    phpExcel常用方法详解[附有php导出excel加超级链接] 发表于4年前(-- :) 阅读() | 评论() 0人收藏此文章, 我要收藏 赞0 http://www.codeplex.com/ ...

  7. 从整体上理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换

    学号后三位<168> 原创作品转载请注明出处https://github.com/mengning/linuxkernel/ 1.分析fork函数对应的内核处理过程sys_clone,理解 ...

  8. Delphi静态加载DLL和动态加载DLL示例

    下面以Delphi调用触摸屏动态库xtkutility.dll为例子,说明如何静态加载DLL和动态加载DLL. 直接上代码. 1.静态加载示例 unit Unit1; interface uses W ...

  9. (六)《Java编程思想》——初始化及类的加载顺序

    package chapter7; /** * 初始化及类的加载顺序:顺序如下 * 1.基类的static变量 * 2.导出类的static变量 * 3.基类的变量 * 4.基类的构造函数 * 5.导 ...

随机推荐

  1. MySQL源码分析之SQL函数执行

    1.MySQL中执行一条SQL的总体流程 2.SQL函数执行过程 1.MySQL中执行一条SQL的总体流程 一条包含函数的SQL语句,在mysql中会经过: 客户端发送,服务器连接,语法解析,语句执行 ...

  2. Python之创建数据库及功能示例样本

    创建数据库实例 import pymysql db= pymysql.connect(host="localhost",user="root",password ...

  3. 【设计模式】Java设计模式 - 单例模式

    [设计模式]Java设计模式 - 单例模式 不断学习才是王道 继续踏上学习之路,学之分享笔记 总有一天我也能像各位大佬一样 分享学习心得,欢迎指正,大家一起学习成长! 原创作品,更多关注我CSDN: ...

  4. PostgreSQL 绑定变量窥探

    今天我们要探讨的是 custom执行计划和通用执行计划.这一技术在 Oracle中被称为绑定变量窥视.但 Kingbase中并没有这样的定义,更严格地说,Kingbase叫做custom执行计划和通用 ...

  5. KDB_Database_Link 使用介绍

    kdb_database_link 是 KingbaseES 为了兼容oracle 语法而开发的跨数据库访问扩展,可用于访问KingbaseES, Postgresql , Oracle .以下分别介 ...

  6. Thrift RPC改进—更加准确的超时管理

    前言: 之前我们组内部使用Thrift搭建了一个小型的RPC框架,具体的实现细节可以参考我之前的一篇技术文章:https://www.cnblogs.com/kaiblog/p/9507642.htm ...

  7. 引擎之旅 前传:C++代码规范

    自己以前写代码时,一个项目一个风格.单人开发的工作使得我并没有注意到代码规范性和可读性的问题.每当项目结束后,看到自己杂乱无章的代码,完全没有二次开发和重构的欲望. 写代码就应该像写诗一样优雅. by ...

  8. flink-cdc同步mysql数据到hbase

    本文首发于我的个人博客网站 等待下一个秋-Flink 什么是CDC? CDC是(Change Data Capture 变更数据获取)的简称.核心思想是,监测并捕获数据库的变动(包括数据 或 数据表的 ...

  9. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(22)-如何使用Fiddler生成Jmeter脚本-下篇

    1.简介 今天这篇文章其实和上一篇差不多也是利用一个fiddler的插件进行Jmeter脚本的导出,开始宏哥想要合在一起写一篇文章,可是结果实践的时候,两个插件还是有区别的,因此为了不绕晕小伙伴或者童 ...

  10. acme.sh官方中文说明文档

    转载自:https://github.com/acmesh-official/acme.sh/wiki/说明 acme.sh 实现了 acme 协议, 可以从 letsencrypt 生成免费的证书. ...