Aggregator
2024年医疗保健行业网络安全现状
2024年医疗保健行业网络安全现状
CVE-2025-0355 | NEC WX4200D5 missing authentication
【总结】逻辑运算在Z3中运用+CTF习题
国际赛IrisCTF在前几天举办,遇到了一道有意思的题目,特来总结。
题目
附件如下:babyrevjohnson.tar
解题过程
关键main函数分析如下:
int __fastcall main(int argc, const char **argv, const char**envp)
{
int v4; // [rsp+4h] [rbp-7Ch]
int v5; // [rsp+4h] [rbp-7Ch]
int v6; // [rsp+8h] [rbp-78h]
int v7; // [rsp+Ch] [rbp-74h]
char input[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v9; // [rsp+78h] [rbp-8h]
v9 = __readfsqword(0x28u);
puts("Welcome to the Johnson's family!");
puts("You have gotten to know each person decently well, so let's see
if you remember all of the facts.");
puts("(Remember that each of the members like different things from
each other.)");
v4 = 0;
while ( v4 <= 3 ) // 在提供的颜色中,选择4种
{
printf("Please choose %s's favorite color: ", (&names)[v4]);//
4个人
__isoc99_scanf("%99s", input);
if ( !strcmp(input, colors) )
{
v6 = 1; // red
goto LABEL_11;
}
if ( !strcmp(input, s2) )
{
v6 = 2; // blue
goto LABEL_11;
}
if ( !strcmp(input, off_4050) )
{
v6 = 3; // green
goto LABEL_11;
}
if ( !strcmp(input, off_4058) )
{
v6 = 4; // yellow
LABEL_11:
if ( v6 == chosenColors[0] || v6 == dword_4094 || v6 ==
dword_4098 || v6 == dword_409C )// 选择4个颜色,然后顺序不能一样
puts("That option was already chosen!");
else
chosenColors[v4++] = v6; // 存储选择的颜色(已经转换成了数字)
}
else
{
puts("Invalid color!");
}
}
v5 = 0;
while ( v5 <= 3 )
{
printf("Please choose %s's favorite food: ", (&names)[v5]);//
4个人最喜欢的食物
__isoc99_scanf("%99s", input);
if ( !strcmp(input, foods) )
{
v7 = 1; // pizza
goto LABEL_28;
}
if ( !strcmp(input, off_4068) )
{
v7 = 2; // pasta
goto LABEL_28;
}
if ( !strcmp(input, off_4070) )
{
v7 = 3; // steak
goto LABEL_28;
}
if ( !strcmp(input, off_4078) )
{
v7 = 4; // chicken
LABEL_28:
if ( v7 == chosenFoods[0] || v7 == dword_40A4 || v7 == dword_40A8
|| v7 == dword_40AC )
puts("That option was already chosen!");
else
chosenFoods[v5++] = v7;
}
else
{
puts("Invalid food!");
}
}
check(); // 开始check,检测我们输入的颜色和食物是否正确
return 0;
}
-----------------------------------------------------------------------
将check提取出来,我们方便分析
其实到这里已经可以得到结果了,国外的题目确实很讲究趣味性,用颜色和食物作为导向,引导一步一步分析
笔者使用静态分析的方法,一步一步跟踪
C++
int check(){
bool v0; // dl
_BOOL4 v1; // eax
_BOOL4 v2; // edx
v0 = dword_40A8 != 2 && dword_40AC != 2;
v1 = v0 && dword_4094 != 1;
v2 = chosenColors[0] != 3 && dword_4094 != 3;
if ( !v2 || !v1 || chosenFoods[0] != 4 || dword_40AC == 3 ||
dword_4098 == 4 || dword_409C != 2 )
return puts("Incorrect.");
puts("Correct!");
return system("cat flag.txt"); // 执行cat flag的命令
}
-----------------------------------------------------------------------
对应的输入值地址如下:
我们将颜色color数组用x系列表示,将食物用food数组y系列表示
化简如下:
C++v0 = y3 != 2 && y4 != 2;
v1 = v0 && x2 != 1;
v2 = x1 != 3 && x2 != 3;
if ( !v2 || !v1 || y1 != 4 || y4 == 3 || x3 == 4 || x4 != 2
)
{
//错误
}
else
{
//成功
}
-----------------------------------------------------------------------
思路1:简单粗暴的爆破,但不是学习的目的,因此并不采用
思路2:锻炼写脚本能力,使用z3解题可以锻炼写脚本的能力,因此采用
Python
from z3 import *# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
得到结果
Python
成功:x1 = 4
x2 = 0
x3 = 5
x4 = 2
y1 = 4
y2 = None
y3 = 3
y4 = 0
-----------------------------------------------------------------------
其实有经验的师傅发现了,这是有多解的,因为没有为约束变量添加范围约束
改进之后的代码如下:
Python
from z3 import *# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4
>= 1, x4 <= 4,
y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
solver.add(range_constraint)
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------
得到结果:
-----------------------------------------------------------------------
Python
成功:
x1 = 1
x2 = 4
x3 = 1
x4 = 2
y1 = 4
y2 = 1
y3 = 3
y4 = 4
-----------------------------------------------------------------------
发现x1和x3重复了,因此还要添加值不重复约束
Pythonfrom z3 import *
# 创建变量
x1, x2, x3, x4 = Ints('x1 x2 x3 x4')
y1, y2, y3, y4 = Ints('y1 y2 y3 y4')
# 创建约束条件
v0 = And(y3 != 2, y4 != 2)
v1 = And(v0, x2 != 1)
v2 = And(x1 != 3, x2 != 3)
#值范围约束
range_constraint = And(x1 >= 1, x1 <= 4, x2 >= 1, x2 <= 4, x3 >= 1, x3 <= 4, x4
>= 1, x4 <= 4,
y1 >= 1, y1 <= 4, y2 >= 1, y2 <= 4, y3 >= 1, y3 <= 4, y4 >= 1, y4 <= 4)
#非重复值约束
distinct_x=Distinct(x1,x2,x3,x4)
distinct_y=Distinct(y1,y2,y3,y4)
# 创建条件语句
cond = Or(Not(v2), Not(v1), y1 != 4, y4 == 3, x3 == 4, x4 != 2)
cond1 = Not(cond)
#正常来说,cond的值要为false的,但是z3的add添加的条件必须为1才行,因此要进行取反操作
# 创建求解器
solver = Solver()
# 添加约束条件和条件语句到求解器
solver.add(cond1)#这里添加的条件必须为true,所以最后使用了 not 进行取反操作
solver.add(range_constraint)
solver.add(distinct_y)
solver.add(distinct_x)
# 求解
if solver.check() == sat:
# 如果有解,则获取解
model = solver.model()
# 打印解
print("成功:")
print("x1 =", model[x1])
print("x2 =", model[x2])
print("x3 =", model[x3])
print("x4 =", model[x4])
print("y1 =", model[y1])
print("y2 =", model[y2])
print("y3 =", model[y3])
print("y4 =", model[y4])
else:
print("无解")
---------------------------------------------------------------------------------------
最终得到正确的结果
Python 成功: x1 = 1 x2 = 4 x3 = 3 x4 = 2 y1 = 4 y2 = 2 y3 = 3
y4 = 1x1-x4= 1 4 3 2
y1-y4= 4 2 3 1
按照这样的顺序输入即可:
得到了flag
irisctf{m0r3_th4n_0n3_l0g1c_puzzl3_h3r3}
总结
题目并不是很难,没有复杂的ollvm混淆也没有复杂的加密。但是却一步一步引导我们去学习和总结。z3解题的过程中,会有很多误解,然后经过自己的思考总结,发现了漏掉的东西,再进行补充,最终写出正确的脚本。
国外的题还是很值得学习的,不单单为了出题而出题。这就是逻辑运算在z3的运用以及如何增加约束,让z3求解出我们需要的key。
【总结】逻辑运算在Z3中运用+CTF习题
浅谈热补丁的钩取方式
热补丁的钩取方式是为了解决内联钩取在多线程情况下会出错的情况,使用热补丁的钩取可以避免重复读写指令造成问题。
内联钩取潜在问题正常情况下,在每次跳转到自定义函数时需要将原始的指令(mov edi,edi)写回CreateProcessW函数内,为了后续正确调用CreateProcesW函数,在调用完毕之后,又需要进行挂钩的处理,即将mov指令修改为jmp指令。
但是在多线程的情况下就可能会出现下列问题,在进行mov指令篡改时可能会发生线程的切换,因为篡改指令的操作不是原子操作。那么在线程2时可能调用了CreateProcessW函数时可能跳转指令还没写完成,例如下图的jmp 0x12xx,而不是原本的jmp 0x1234就导致了执行出错。
为了解决此问题采用了热补丁钩取。
热补丁钩取热补丁是指在不中断系统运行进行应用。即不中断程序运行也能够修改系统库或程序中的执行逻辑。
这里以CreateProcessW为例子
在windbg中使用以下指令在CreateProcessW函数中打下断点
.reload /fbp CreateProcessW
可以看到CreateProcessW函数入口点是mov edi,edi指令,而在该指令上方有一段没用用到的空间,在windbg中使用int 3指令填充了。
而mov edi,edi指令本身没有实际意义,这就是微软在系统库预留的空间,用于打上热补丁。因为这个指令无论被修改成什么都不会影响程序的执行。
接着可以发现这跳指令的长度为2字节,因此可以使用任意的2字节长的指令替换mov edi,edi。
那么这里就需要寻找可以完成跳转的指令,并且仅占用2字节完成对mov指令的替换。
在汇编中存在着短跳转指令可以完成跳转并且仅占用2字节,用以下例子来观察一下短跳转的指令。
int main(){
// 使用标签作为跳转目标
__asm {
jmp short label;
};
// 标签处定义跳转目标
label:
// 这里是跳转目标后的代码
return 0;
}
可以看到在跳转到标签label上时,采用的跳转指令机器码是EB开头的,而不是E9,并且指令长度也只有2字节。
那么00是跳转的偏移值,根据该例子分析一下跳转偏移的计算
跳转偏移 = 目标地址 - 当前地址 - 当前指令的长度00 = 00731005 - 00731003 - 2
可以看到计算偏移的公式与jmp指令一致,只是跳转的指令的长度为5字节,而短跳转的指令长度为2字节,因此jmp指令也被称之为长跳转。
那么怎么配合短跳转进行一个钩取操作,如下图。我们可以借助短跳转使得指令执行到上述填充的区域,然后再使用jmp指令完成钩取的操作。这里需要注意的是空闲区域的空间大小需要大于5个字节,不然无法容纳jmp指令。
最终修改后钩取的效果如下图,在自定义函数中不在需要钩取与脱钩的操作,因为我们修改的指令不会影响正常的CreateProcessW函数执行。那么在既然不存在写操作,那么在多线程中也不会因为条件竞争导致还没写完就切换线程的情况。
那么代码实现部分如下,这里需要注意长跳转的指令0xE9,短跳转的指令为0xEB,这里先把偏移计算好了0xF9,因此写好了,但是这个偏移值不是唯一值,只要找到的地址存在大于5字节的空闲区域都是可以的。紧接着就是修改函数内部的指令,将初始的指令修改为短跳转,然后再空闲区中填充长跳转即可。
...//长跳转指令
BYTE pBuf[5] = { 0xE9, 0 };
//短跳转指令 + 偏移值
BYTE pShortJmp[2] = { 0xEB, 0xF9};
//获取模块地址
HMODULE hModule = GetModuleHandleA(szDllName);
//获取函数地址
FARPROC pfnOld = GetProcAddress(hModule, szFuncName);
//选中长跳转指令填充的地址,这里选择恰好能容纳jmp指令的位置
DWORD target = (DWORD)pfnOld - 5;
//计算跳转的偏移
DWORD dwAddress = (DWORD)pfnNew - target - 5;
//修改区域的权限
VirtualProtect((LPVOID)target, 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);
//将偏移填充到指令中
memcpy(&pBuf[1], &dwAddress, 4);
//将长跳转指令填充
memcpy((LPVOID)target, pBuf, 5);
//保存原始的两个字节
memcpy(pOldBytes, pfnOld, 2);
//将短跳转指令填充
memcpy(pfnOld, pShortJmp, 2);
VirtualProtect((LPVOID)target, 7, dwOldProtect, &dwOldProtect);
...
在自定义函数中,只需要直接调用CreatePorcessW + 2的指令就可以完成原始CreateProcessW函数,不再需要挂钩脱钩的处理。
...//调用CreateProcessW + 2
BOOL ret = ((LPFN_CreateProcessW)((DWORD)pfnOld + 2))(
applicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation
);
...
完整代码:
https://github.com/h0pe-ay/HookTechnology/tree/main/Hook-HotPatch
总结优点:避免多线程出错
缺点:不一定有热补丁的条件,就是不一定存在有垃圾指令
如64位程序的CreateProcessW函数的第一条指令是mov r11,rsp,但是后续的指令都需要用到r11寄存器的值,因此该指令不是无用指令。就不能上述热补丁的方法。
浅谈热补丁的钩取方式
CVE-2025-0354 | NEC WX4200D5 Web Management Interface cross site scripting
CVE-2025-0356 | NEC WX1500HP/WX3600HP os command injection
Cactus
浅谈进程隐藏技术
在之前几篇文章已经学习了解了几种钩取的方法
● 浅谈调试模式钩取
● 浅谈热补丁
● 浅谈内联钩取原理与实现
● 导入地址表钩取技术
这篇文章就利用钩取方式完成进程隐藏的效果。
进程遍历方法在实现进程隐藏时,首先需要明确遍历进程的方法。
CreateToolhelp32SnapshotCreateToolhelp32Snapshot函数用于创建进程的镜像,当第二个参数为0时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。
#include <iostream>#include <Windows.h>
#include <TlHelp32.h>
int main()
{
//设置编码,便于后面能够输出中文
setlocale(LC_ALL, "zh_CN.UTF-8");
//创建进程镜像,参数0代表创建所有进程的镜像
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
std::cout << "Create Error" << std::endl;
exit(-1);
}
/*
* typedef struct tagPROCESSENTRY32 {
* DWORD dwSize; 进程信息结构体大小,首次调用之前必须初始化
* DWORD cntUsage; 引用进程的次数,引用次数为0时,则进程结束
* DWORD th32ProcessID; 进程的ID
* ULONG_PTR th32DefaultHeapID; 进程默认堆的标识符,除工具使用对我们没用
* DWORD th32ModuleID; 进程模块的标识符
* DWORD cntThreads; 进程启动的执行线程数
* DWORD th32ParentProcessID; 父进程ID
* LONG pcPriClassBase; 进程线程的基本优先级
* DWORD dwFlags; 保留
* TCHAR szExeFile[MAX_PATH]; 进程的路径
* } PROCESSENTRY32;
* typedef PROCESSENTRY32 *PPROCESSENTRY32;
*/
PROCESSENTRY32 pi;
pi.dwSize = sizeof(PROCESSENTRY32);
//取出第一个进程
BOOL bRet = Process32First(hSnapshot, &pi);
while (bRet)
{
wprintf(L"进程路径:%s\t进程号:%d\n", pi.szExeFile, pi.th32ProcessID);
//取出下一个进程
bRet = Process32Next(hSnapshot, &pi);
}
}
EnumProcesses
EnumProcesses用于将所有进程号的收集。
#include <iostream>#include <Windows.h>
#include <Psapi.h>
int main()
{
setlocale(LC_ALL, "zh_CN.UTF-8");
DWORD processes[1024], dwResult, size;
unsigned int i;
//收集所有进程的进程号
if (!EnumProcesses(processes, sizeof(processes), &dwResult))
{
std::cout << "Enum Error" << std::endl;
}
//进程数量
size = dwResult / sizeof(DWORD);
for (i = 0; i < size; i++)
{
//判断进程号是否为0
if (processes[i] != 0)
{
//用于存储进程路径
TCHAR szProcessName[MAX_PATH] = { 0 };
//使用查询权限打开进程
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ,
FALSE,
processes[i]);
if (hProcess != NULL)
{
HMODULE hMod;
DWORD dwNeeded;
//收集该进程的所有模块句柄,第一个句柄则为文件路径
if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
&dwNeeded))
{
//根据句柄获取文件路径
GetModuleBaseName(hProcess, hMod, szProcessName,
sizeof(szProcessName) / sizeof(TCHAR));
}
wprintf(L"进程路径:%s\t进程号:%d\n", szProcessName, processes[i]);
}
}
}
} ZwQuerySystemInfomation
ZwQuerySystemInfomation函数是CreateToolhelp32Snapshot函数与EnumProcesses函数底层调用的函数,也用于遍历进程信息。代码参考https://cloud.tencent.com/developer/article/1454933
#include <iostream>#include <Windows.h>
#include <ntstatus.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
//定义函数指针
typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
);
int main()
{
//设置编码
setlocale(LC_ALL, "zh_CN.UTF-8");
//获取模块地址
HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll");
if (ntdll_dll == NULL) {
std::cout << "Get Module Error" << std::endl;
exit(-1);
}
NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL;
//获取函数地址
ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(ntdll_dll, "ZwQuerySystemInformation");
if (ZwQuerySystemInformation != NULL)
{
SYSTEM_BASIC_INFORMATION sbi = { 0 };
//查询系统基本信息
NTSTATUS status = ZwQuerySystemInformation(SystemBasicInformation, (PVOID)&sbi, sizeof(sbi), NULL);
if (status == STATUS_SUCCESS)
{
wprintf(L"处理器个数:%d\r\n", sbi.NumberOfProcessors);
}
else
{
wprintf(L"ZwQuerySystemInfomation Error\n");
}
DWORD dwNeedSize = 0;
BYTE* pBuffer = NULL;
wprintf(L"\t----所有进程信息----\t\n");
PSYSTEM_PROCESS_INFORMATION psp = NULL;
//查询进程数量
status = ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwNeedSize);
if (status == STATUS_INFO_LENGTH_MISMATCH)
{
pBuffer = new BYTE[dwNeedSize];
//查询进程信息
status = ZwQuerySystemInformation(SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL);
if (status == STATUS_SUCCESS)
{
psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
wprintf(L"\tPID\t线程数\t工作集大小\t进程名\n");
do {
//获取进程号
wprintf(L"\t%d", psp->UniqueProcessId);
//获取线程数量
wprintf(L"\t%d", psp->NumberOfThreads);
//获取工作集大小
wprintf(L"\t%d", psp->WorkingSetSize / 1024);
//获取路径
wprintf(L"\t%s\n", psp->ImageName.Buffer);
//移动
psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset);
} while (psp->NextEntryOffset != 0);
delete[]pBuffer;
pBuffer = NULL;
}
else if (status == STATUS_UNSUCCESSFUL) {
wprintf(L"\n STATUS_UNSUCCESSFUL");
}
else if (status == STATUS_NOT_IMPLEMENTED) {
wprintf(L"\n STATUS_NOT_IMPLEMENTED");
}
else if (status == STATUS_INVALID_INFO_CLASS) {
wprintf(L"\n STATUS_INVALID_INFO_CLASS");
}
else if (status == STATUS_INFO_LENGTH_MISMATCH) {
wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH");
}
}
}
} 进程隐藏
通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32Snapshot、EnumProcesses以及ZwQuerySystemInfomation函数
但是CreateToolhelp32Snapshot与EnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。
由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。
可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。
这里采用内联钩取的方式对ZwQuerySystemInfomation进行钩取处理,具体怎么钩取在浅谈内联钩取原理与实现已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation函数进行说明。
首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation函数,紧接着获取待钩取函数的地址即可。
...//脱钩
UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);
HMODULE hModule = GetModuleHandleA("ntdll.dll");
//获取待钩取函数的地址
PROC pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation");
//调用原始的ZwQuerySystemInfomation函数
NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...
为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。
通过单链表中删除节点的操作,取出目标进程的结构体。代码如下
...pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation);
while (true)
{
if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe"))
{
//需要隐藏的进程是最后一个节点
if (pCur->NextEntryOffset == 0)
pPrev->NextEntryOffset = 0;
//不是最后一个节点,则将该节点取出
else
pPrev->NextEntryOffset += pCur->NextEntryOffset;
}
//不是需要隐藏的节点,则继续遍历
else
pPrev = pCur;
//链表遍历完毕
if (pCur->NextEntryOffset == 0)
break;
pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset);
}
...
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c
但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。
首先利用bl命令查看断点
紧着利用 bc [ID]删除断点
在注入之后任务管理器会在拷贝的时候发生异常
在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限
在windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。
但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。
因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours,可以在钩取过程中确保线程安全。
项目地址:https://github.com/microsoft/Detours
环境配置参考:https://www.cnblogs.com/linxmouse/p/14168712.html
使用vcpkg下载
vcpkg.exe install detours:x86-windowsvcpkg.exe install detours:x64-windows
vcpkg.exe integrate install 实例
挂钩
利用Detours挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。
...//用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性
DetourRestoreAfterWith();
//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//挂钩
DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
...
脱钩
然后根据顺序完成脱钩即可。
...//开始一个新的事务来附加或分离
DetourTransactionBegin();
//进行线程上下文的更新
DetourUpdateThread(GetCurrentThread());
//脱钩
DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
//提交事务
error = DetourTransactionCommit();
... 挂钩的原理
从上述可以看到,Detours是通过事务确保了在DLL加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。
可以利用x ntdl!ZwQuerySystemInformation查看函数地址,可以看到函数的未被挂钩前的情况如下图。
挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。
该地址里面又是一个jmp指令,并且完成间接寻址的跳转。
该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。
跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。
该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。
综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c