相关叨叨:DLL注入技术与网络安全

对于网络安全的课题,攻与防是同等地位的重要,可攻可受(?)是每一个学习网络安全的~冤种~技术人所应同时具备的能力。
而DLL注入技术,一般来讲是向一个正在运行的进程插入/注入代码的过程,可以被正常软件用来添加/扩展其他程序,调试或逆向工程的功能性;该技术也常被恶意软件以多种方式利用。这意味着从安全角度来说,了解DLL注入的工作原理是十分必要的。
DLL注入原理与流程(图源网)

实验目标与内容

利用DLL注入,实现扫雷一件通关辅助:

  1. 找到地雷数组所在内存位置;
  2. 数组中的各个值代表的含义;
  3. 实现辅助

实验拟题来自于 中国海洋大学 网络攻防先导实践 lab03:简单的DLL注入实践 -by 曲海鹏 等

实验依赖工具

Windows 消息传递机制

所谓的Windows消息传递机制就类似于生活中的物流公司。当寄件人(例如鼠标、键盘)将包裹(消息)交给物流公司(Windows系统)时,物流公司(Windows系统)会进行整理并且派发(整理及派发主要由消息循环完成),交给相应的快递员(窗口过程)来处理。快递员(窗口过程)拿到包裹(消息)后则有多种方式来处理,如立马交给收件人,等一天交给收件人,或转交给其他快递派发,这就需要在窗口过程中用switch/case来区分。
Windows消息传递机制示意
WIndows消息传递的回调与响应函数
对于通过DLL进行的消息劫持与注入,则需要利用Windows消息传递机制来完成,下述为Windows消息事件的介绍及获取/模拟:

Windows消息来源相关知识部分应用

消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体,改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。

  1. Windows中,消息使用统一的结构体(MSG)来存放信息,其中message表明消息的具体的类型,
    </br>而wParam,lParam是其最灵活的两个变量,为不同的消息类型时,存放数据的含义也不一样。
    time表示产生消息的时间,pt表示产生消息时鼠标的位置。
    Windows中消息MSG声明如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    typedef struct tagMsg
    {
    HWND hwnd; // 接受该消息的窗口句柄,告诉操作系统,应该把消息发生给哪个应用 哪个窗口
    UINT message; // 消息常量标识符,也就是我们通常所说的消息号
    WPARAM wParam; // 32位消息的特定附加信息,确切含义依赖于消息值
    LPARAM lParam; // 32位消息的特定附加信息,确切含义依赖于消息值
    DWORD time; // 消息创建时的时间
    POINT pt; // 消息创建时的鼠标/光标在屏幕坐标系中的位置
    }MSG;
  2. 消息类型
    消息类型
    (1) 窗口消息:即与窗口的内部运作有关的消息,如创建窗口,绘制窗口,销毁窗口等。
    可以是一般的窗口,也可以是MainFrame,Dialog,控件等。
    如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL等.</br>
    (2) 当用户从菜单选中一个命令项目、按下一个快捷键或者点击工具栏上的一个按钮,都将发送WM_COMMAND命令消息。
    LOWORD(wParam)表示菜单项,工具栏按钮或控件的ID;如果是控件, HIWORD(wParam)表示控件消息类型。</br>

    1
    2
    #define LOWORD(l) ((WORD)(l))
    #define HIWORD(l) ((WORD)(((DWORD)(l) >> 16) & 0xFFFF))

    (3) 随着控件的种类越来越多,越来越复杂(如列表控件、树控件等),仅仅将wParam,lParam将视为一个32位无符号整数,已经装不下太多信息了。
    为了给父窗口发送更多的信息,微软定义了一个新的WM_NOTIFY消息来扩展WM_COMMAND消息。
    WM_NOTIFY消息仍然使用MSG消息结构,只是此时wParam为控件ID,lParam为一个NMHDR指针,
    不同的控件可以按照规则对NMHDR进行扩充,因此WM_NOTIFY消息传送的信息量可以相当的大。
    注:Window 9x 版及以后的新控件通告消息不再通过WM_COMMAND 传送,而是通过WM_NOTIFY 传送,
    但是老控件的通告消息, 比如CBN_SELCHANGE 还是通过WM_COMMAND 消息发送。</br>
    (4) windwos也允许程序员定义自己的消息,使用SendMessage或PostMessage来发送消息。</br>

  3. 消息队列(Message Queues)
    Windows中有两种类型的消息队列
    (1) 系统消息队列(System Message Queue)
    这是一个系统唯一的Queue,设备驱动(mouse, keyboard)会把操作输入转化成消息存在系统队列中,然后系统会把此消息放到目标窗口所在的线程的消息队列(thread-specific message queue)中等待处理.</br>
    (2) 线程消息队列(Thread-specific Message Queue)
    每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗口过程(WndProc)处理.
    注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。</br>

  4. 队列消息(Queued Messages)和非队列消息(Non-Queued Messages)
    (1) 队列消息(Queued Messages)
    消息会先保存在消息队列中,消息循环会从此队列中取出消息并分发到各窗口处理
    如:WM_PAINT,WM_TIMER,WM_CREATE,WM_QUIT,以及鼠标,键盘消息等。
    其中,WM_PAINT,WM_TIMER只有在队列中没有其他消息的时候才会被处理, WM_PAINT消息还会被合并以提高效率。其他所有消息以先进先出(FIFO)的方式被处理。</br>
    (2) 非队列消息(NonQueued Messages)
    消息会绕过系统消息队列和线程消息队列直接发送到窗口过程被处理 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR, WM_WINDOWPOSCHANGED
    注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是非队列消息, 被直接送到窗口过程处理.</br>

  5. 窗体函数(WindowProc)
    应用程序消息循环(messager loop)
    Windows 应用程序创建的每个窗口都在系统核心注册一个相应的窗口函数,窗口函数程序代码形式上是一个巨大的switch 语句,用以处理由消息循环发送到该窗口的消息,窗口函数由Windows 采用消息驱动的形式直接调用,而不是由应用程序显示调用的,窗口函数处理完消息后又将控制权返回给Windows。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //而窗体负责响应消息的函数称为“窗体过程(Window Procedure)”,窗体过程是一个函数,每个窗体一个,它大致拥有以下的“模样”(C++代码):
    LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
    {
    //……
    switch (uMsg) //依据消息标识符进行分类处理
    {
    case WM_CREATE:
    // 初始化窗体.
    return 0;
    case WM_PAINT:
    // 绘制窗体
    return 0;
    //
    //处理其他消息
    //
    default:
    //如果窗体没有定义处理此种消息的代码,则转去调用系统默认的消息处理函数
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    }
    //可以看到,“窗体过程”不过就是一个多分支语句罢了,在这个语句中,窗体对不同类型的消息进行处理。

以上内容来自(整理)window 消息传递机制
~事实上,上述内容我也看的懵懂,大概当个了解~

Spy++

Spy++ (SPYXX.EXE) 是一个基于 Win32 的实用工具,它提供系统的进程、线程、窗口和窗口消息的图形视图。使用 Spy++ 可以执行下列操作: 显示系统对象(包括进程、线程和窗口)之间关系的图形树。 搜索指定的窗口、线程、进程或消息。 查看选定的窗口、线程、进程或消息的属性。
介个就是spyxx.exe
spyxx界面

后续在本实验的应用上,我拿这个用来在创建模拟点击事件时的定位窗口坐标。

Cheat Engine

Cheat Engine是一款专注于游戏的修改器。它可以用来扫描游戏中的内存,并允许修改它们。它还附带了调试器、反汇编器、汇编器、变速器、作弊器生成、Direct3D操作工具、系统检查工具等。
这个是Cheat Engine
CE界面

针对游戏的修改器/辅助器/或你叫它什么都好,总之要想达到作弊的效果,最重要的当然是要分析破译游戏内存数据所代表的含义,并能精准定位所要实现修改的内存存放地址,因此在后续的操作中,Cheat Engine扮演了十分重要的角色。

Xenos

《Xenos》是2007年池泽辰也执导的悬疑片,由海东键、一戸奈美、堀田ゆい夏等主演。
Xenos

...当然,不可能是这个"Xenos"

我们所要介绍的Xenos,是用来进行DLL注入的软件,长这样:
这个才是注入器Xenos
Xenos界面

这个工具,则是帮助我们将写好的DLL类型文件对程序进行注入。

以上,则是本次实验/本类型任务的基本搭建环境和操作工具,接下来正式进入对本实验的分析与实践。

实验分析:让俺看看扫雷背后都有啥

摆烂了将近一个月终于开始继续

WinMine_XP.exe 游戏规则分析

扫雷游戏界面

  1. 坐上叫雷数(剩余旗🚩数)显示;
  2. 右上角计时器显示:初始点击雷区按钮触发计时,点击充值按钮触发计时器重置;
  3. 雷区按钮:
    • 左击:① 显示该按钮下内容:数字(1、2…):以该按钮坐标周围3*3个单位坐标按钮下存在雷的个数;② 雷(被引爆,游戏结束);
    • 右击:① 单机:插旗🚩(当且仅当在所有雷都进行插旗排雷后通关),占用旗数;② 双击:问好,不占用旗数,用于个人怀疑记录;
  4. game设置选项:
    Game设置界面
    可设置棋盘规格大小;其中Custom选项可进行自定义,且Height<=24,Width<=30.
    注:在开始扫雷时左击的第一个按钮,即使是雷也会由程序自动将该地址的雷转移到其他地址,保证玩家不会上来就挂.

实现思路分析

获取棋盘雷区数据存储地址,通过模拟点击事件对雷区所有按钮进行遍历点击,其中对内存存储数值含义为雷的进行模拟右击插旗,其他进行左击排查;通过计时器存储数值判断棋盘数据是否重置。

通过Spy++获取WinMine_XP.exe窗口句柄:
句柄
其中
IpClassName: Minesweeper
IpWindowsName: Minesweeper

通过Cheat Engine获取窗口WinMine_XP.exe窗口内存数据,并查找到棋盘内存点进行数据分析:


分析棋盘数据(以9*9棋盘为例)

  • 棋盘规格数据:
    · 宽度数据存储地址:Width=0x0100553;
    · 长度数据存储地址:Height=0x0100553;
  • 雷区起始点地址:0x01005361;
  • 计时器数据存储地址:0x0100579C;
  • 地雷布局的内存位置:0x0100534;
  • 雷区按钮数据:
    · 10:棋盘边界;
    · 0F:无雷且未点击;
    · 8F:有雷且未点击;
    · 0E:无雷插旗;
    · 8E:有雷插旗;
    · CC:点击引爆;

通过spy++获取雷区按钮坐标相关信息


按钮坐标(1,1)中心对应串钩坐标约为(20,63),且相邻两个按钮之间的窗口坐标差值约为16个单位长度。

编程实现

回调函数

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{

PBYTE t = (PBYTE)0x0100579C;
//char szChar[20];

//对不同窗口信息传送进行对应操作指令
switch (uMsg)
{
//输入任意字符实现一键通关
case WM_CHAR:


if (*t == 0)
{
//内存地址
PBYTE m = (PBYTE)0x01005361;
PBYTE w = (PBYTE)0x01005334;
PBYTE h = (PBYTE)0x01005338;
int x = 20, y = 63, n = 1, q = 1; //坐标
//雷区按钮遍历
while (x <= 20 + *w * 16 && y <= 63 + (*h - 1) * 16)
{
if (q > *h)
y = 63;
if (n > *w)
{
x = 20;
y += 16;
n = 1;
m += 32 - *w;
q++;
}
if (*m != 143) //非雷则左击
{
SendMessage(hwnd, WM_LBUTTONDOWN, 0, MAKELPARAM(x, y));
SendMessage(hwnd, WM_LBUTTONUP, 0, MAKELPARAM(x, y));
}

else //雷则右击
{
SendMessage(hwnd, WM_RBUTTONDOWN, 0, MAKELPARAM(x, y));
SendMessage(hwnd, WM_RBUTTONUP, 0, MAKELPARAM(x, y));
}
//完成该按钮操作后移动至下一按钮坐标处
x += 16;
n++;
m++;
}
x = 20;
y = 63;
SendMessage(hwnd, WM_LBUTTONDOWN, 0, MAKELPARAM(x, y));
SendMessage(hwnd, WM_LBUTTONUP, 0, MAKELPARAM(x, y));
}
default:
break;
}
return CallWindowProc((WNDPROC)OPROC, hwnd, uMsg, wParam, lParam);
}

  1. 数据地址定义:
    · PBYTE t: 计时器数据存储地址;
    · PBYTE m: 雷区按钮起始点地址;
    · PBYTE w: 雷区宽度数据存储地址;
    · PBYTE h: 雷区长度数据存储地址;
  2. 坐标定义:
    · x,y: 窗口坐标;
    · n,q: 按钮坐标;
  3. Switch(uMsg): 对不同窗口信息传送进行对应操作指令(在此选择通过从键盘输入任意字符实现一键通关指令)
  4. while(x <= 20 + w 16 && y <= 63 + (h - 1) * 16): 对雷区按钮进行遍历;
    · if 此按钮坐标(n,q)下数据m!=143(8F)时,对该按钮坐标(x,y)模拟鼠标左击事件;
    · else 对该按钮窗口坐标(x,y)模拟鼠标右击事件
    · 完成该按钮操作后进行坐标移动
  5. *t==0时(棋盘重置)重复上述操作;

笔记:

  • 同列相邻行间地址差推算:address+32-width;
  • 棋盘重置可通过 * p的值作为条件,棋盘重置后需要将地址指针及坐标悉数重置;
  • 若本次布雷,在(1,1)即为雷时,由于程序对内存数据的访问限制会使第一次的模拟点击跳过,需在遍历后补充一次对(1,1)按钮的左击。

注入函数

void injected()

1
2
3
4
5
6
7
8
9
10
11
void injected()
{
MessageBoxA(nullptr, "injected", "title", MB_OK);
HWND OW = FindWindowA("Minesweeper", "Minesweeper");
if (!OW)
{
MessageBoxA(nullptr, "find window failed", "Failed", MB_OK);
return;
}
OPROC = SetWindowLong(OW, GWL_WNDPROC, (LONG)WindowProc);
}

入口点函数

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
injected();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

实验结果

注入测试

通过Xenos注入:

接收到注入函数中设定的注入成功的MessageBox;

问题及解决

  1. 模拟鼠标点击事件:
    SendMessage()函数;
    参考:如何使用Sendmessage模拟某一按钮的点击事件 - 百度文库
  2. 由于扫雷程序对第一次左击不触雷的设定,致使在测试中出现第一个按钮未被执行点击的情况:在遍历后补充对第一个按钮的无差别点击。

篇末BB

事实上这个外挂辅助的实现有许多不同中的思路,我不能确保自己选择的是最巧妙的但是我所唯一能操作实现的。
在大量辅助工具的帮助下对目标程序的内存分析十分简单且便捷。
个人认为本次实验的困难包括但不限于对内存数据的分析与利用,虽然能照本宣科地使用Spy++/CE/Xenos等,但我并未能利用好PeiD工具,或许因为人错过了更为便捷的信息获取与分析途径。
总而言之,虽然“独立”完成了扫雷外挂的编程实现是极具满足感的,但或以更多的是在对辅助工具的认识与使用上,于是更期待之后能够更熟练运用这些工具,做出更可拷/可刑的工作。