现在我们距离知道是谁操作了注册表又近了一步,
这次我们看看 WSCSetApplicationCategory
做了什么
本篇已完结,列出目录方便查阅
本文是第三部分
不出意外 WS2_32.dll
会有好多个,这时候我们应该去看这个位置:
因为多数情况下 System32 是 windows 的默认起始目录。为了避免对 windows 造成影响, 我们复制一份出来然后在 cutter 中打开:
因为这个是导出函数,所以我们可以直接从函数列表中找到:
直接看反编译:
void WSCSetApplicationCategory
(LPCWSTR Path, DWORD PathLength, LPCWSTR Extra, DWORD ExtraLength, int64_t arg_28h, int64_t arg_30h,
int64_t arg_38h)
{
int64_t var_48h;
int64_t var_40h;
int64_t var_38h;
int64_t var_30h;
char *pcStack_28;
int64_t var_20h;
undefined8 uStack_18;
int64_t var_8h;
var_20h._0_4_ = 0;
uStack_18 = 0;
var_20h._4_4_ = 2;
if (5 < _data.1800560f8) {
pcStack_28 = "WSCSetApplicationCategory";
var_48h = (int64_t)&pcStack_28;
fcn.18003bff4(var_48h);
}
var_48h = CONCAT44(var_48h._4_4_, (undefined4)arg_28h);
WSCSetApplicationCategoryEx
((int64_t)Path, (uint64_t)PathLength, (int64_t)Extra, (uint64_t)ExtraLength, var_48h, arg_30h, arg_38h,
(int64_t)&var_20h);
return;
}
看来做事的应该是 WSCSetApplicationCategoryEx
:这个函数就很复杂了,
Ghidra 都用到了 goto
,这是我很少见的,我们先粗略看看有没有线索:
if (iVar1 == 0) {
fcn.18000e334((LPCSTR)&lpSubKey, 0x12, (int64_t)"%08X", (uint64_t)uVar9);
} else {
_dwOptions = (LPDWORD *)CONCAT44(uStack_2f4, uVar9);
fcn.18000e334((LPCSTR)&lpSubKey, 0x12, (int64_t)"%08X-%08X", (uint64_t)(uint32_t)lpType);
}
lpdwDisposition = (LPDWORD)((int64_t)&lpType + 4);
arg4_00 = (LPDWORD *)0x0;
placeholder_1 = &lpSubKey;
phkResult = &hKey;
lpSecurityAttributes = 0;
_samDesired = (char **)CONCAT44(uStack_2ec, 0x2001f);
_dwOptions = (LPDWORD *)((uint64_t)_dwOptions & 0xffffffff00000000);
uVar4 = (*api-ms-win-core-registry-l1-1-0.dll_RegCreateKeyExA)(pDVar8, placeholder_1, 0);
最后一行调用了注册表的接口 RegCreateKeyExA
,
那么这里应该就可以找到键值的计算方式,另外,我们还有一个地方要注意:
if (iVar1 == 0) {
fcn.18000e334((LPCSTR)&lpSubKey, 0x12, (int64_t)"%08X", (uint64_t)uVar9);
} else {
_dwOptions = (LPDWORD *)CONCAT44(uStack_2f4, uVar9);
fcn.18000e334((LPCSTR)&lpSubKey, 0x12, (int64_t)"%08X-%08X", (uint64_t)(uint32_t)lpType);
}
这两个调用很像是生成键值的字符串,8 位大写 Hex,和我们的目标一样!
看来这要是我们的最后一篇了,是不是还有点小难过。别担心,正戏才刚刚开始。
WSCSetApplicationCategoryEx
这个函数很长,不过我们还是得从头看,
根据 API,
第一个参数是程序路径,在反编译的代码中它是 arg1
if (((int32_t)arg2 == 0) || (arg1 == 0)) goto code_r0x00018003e0a6;
if (arg_40h == 0) {
uVar4 = 0x2726;
} else {
if (*(int64_t *)(arg_40h + 8) == 0) {
iVar3 = (*api-ms-win-core-processenvironment-l1-1-0.dll_ExpandEnvironmentStringsW)(arg1, &lpDst, 0x104);
if (iVar3 != 0) {
iVar3 = fcn.18000f9c8((LPWSTR)&lpDst, (LPDWORD)0x104, (int64_t)&var_2b0h);
arg3_01 = var_2b0h;
if (iVar3 == 0) goto code_r0x00018003dc0f;
}
else
之前的片段应该都是前置判断,
第一个用到路径的是 ExpandEnvironmentStringsW
。
这个我们看 API 就好,
它是把 %SYSYEM% 之类的变量替换掉,不过同时它把路径复制到了 lpDst
,
所以后面我们需要关注 lpDst
,那么下一个使用到路径的函数就是 fcn.18000f9c8
了。
fcn.18000f9c8
uint32_t fcn.18000f9c8(LPWSTR arg1, LPDWORD arg2, int64_t arg3)
{
uint32_t uVar1;
LPDWORD pDVar2;
if ((arg1 == (LPWSTR)0x0) || ((LPDWORD)0x7fffffff < arg2)) {
uVar1 = 0x80070057;
} else {
pDVar2 = arg2;
if (arg2 != (LPDWORD)0x0) {
do {
if (*(int16_t *)arg1 == 0) break;
arg1 = (LPWSTR)((int64_t)arg1 + 2);
pDVar2 = (LPDWORD)((int64_t)pDVar2 + -1);
} while (pDVar2 != (LPDWORD)0x0);
}
uVar1 = ~-(uint32_t)(pDVar2 != (LPDWORD)0x0) & 0x80070057;
if (arg3 != 0) {
if (pDVar2 == (LPDWORD)0x0) {
*(undefined8 *)arg3 = 0;
} else {
*(int64_t *)arg3 = (int64_t)arg2 - (int64_t)pDVar2;
}
}
if (pDVar2 != (LPDWORD)0x0) {
return uVar1;
}
}
if (arg3 == 0) {
return uVar1;
}
*(undefined8 *)arg3 = 0;
return uVar1;
}
这个函数有三个参数,路径,路径的缓存长度,以及第三个,我们后面会知道,
这是一个输出参数,同样我们从 else 开始往后看,老实讲这段最难的是这个:
uVar1 = ~-(uint32_t)(pDVar2 != (LPDWORD)0x0) & 0x80070057;
,
我现在也不清楚这个 uVar1
是做什么的,但是看 0x80070057
感觉是一个错误码,
所以不重要,不过这行正好把 else 分成两部分,第一部分计算了缓存的剩余长度,
第二部分把第三个参数设为缓存长度减去剩余长度,那么其实就是路径的实际长度,
所以这就是这个函数的功能:计算路径的实际长度,然后输出到第三个参数。
另外注意,这个是 wchar
的长度,不是 char
的长度。
code_r0x00018003dc0f
code_r0x00018003dc0f:
iVar5 = fcn.18000d8c4(*(int64_t *)(iVar5 + 8), in_stack_fffffffffffffe60);
var_2b0h = iVar5;
if (iVar5 == 0) {
if (((uint8_t)data.180056a98 & 1) != 0) {
fcn.180044008(0x15, (int64_t)data.18004cfc8);
}
uVar4 = 0x2afb;
pDVar8 = (DWORD *)0x0;
} else {
uVar4 = fcn.180010188(iVar5, 1, (int64_t)&var_298h);
if (uVar4 == 0) {
lpType._0_4_ = 0;
var_2a8h._0_4_ = 0;
fcn.18000f958((LPWSTR)&lpDst, arg3_01 & 0xffffffff, (int64_t)&lpType);
uVar9 = (uint32_t)lpType;
pDVar8 = var_298h;
确认了路径长度后跳转到了这里,使用到路径的只有 fcn.18000f958
,
arg3_01
保存了路径的实际长度,第三个参数很可能又是输出参数。
fcn.18000f958
int32_t fcn.18000f958(LPWSTR arg1, int64_t arg2, int64_t arg3)
{
uint16_t uVar1;
LPWSTR pWVar2;
uint32_t uVar3;
int32_t iVar4;
int64_t var_8h;
int64_t var_10h;
uint32_t auStack_18 [4];
pWVar2 = (LPWSTR)(*api-ms-win-core-crt-l1-1-0.dll_wcsstr)(arg1, data.180049f08);
uVar3 = (uint32_t)(LPDWORD)(arg2 & 0xffffffffU);
if (pWVar2 != (LPWSTR)0x0) {
arg1 = pWVar2;
fcn.18000f9c8(pWVar2, (LPDWORD)(arg2 & 0xffffffffU), (int64_t)auStack_18);
uVar3 = auStack_18[0];
}
iVar4 = *(int32_t *)arg3;
pWVar2 = (LPWSTR)((int64_t)arg1 + (uint64_t)uVar3 * 2);
do {
uVar1 = (*api-ms-win-core-crt-l1-1-0.dll_towupper)(*(undefined2 *)arg1);
arg1 = (LPWSTR)((int64_t)arg1 + 2);
iVar4 = iVar4 * 0x25 + (uint32_t)uVar1;
} while (arg1 != pWVar2);
uVar3 = iVar4 * 0x12b9b0a5 >> 0x1f;
iVar4 = (iVar4 * 0x12b9b0a5 ^ uVar3) - uVar3;
*(int32_t *)arg3 = iVar4 % 0x3b9aca07;
return iVar4 * 0x44b82f99;
}
一看就很像是哈希函数,所以这个就是重中之重了,wcsstr(p1,p2)
是用来查找字符串
p2
在字符串 p1
中的位置,arg1
我们知道是路径,data.180049f08
是什么呢?
是个冒号”:”,后续的字符串指针都是用从冒号开始的指针了,也就是说,
盘符与哈希值无关。接下来又调用了 fcn.18000f9c8
,来计算字符串长度。
后面的 do…while
首先把当前字符转为大写,然后就开始计算哈希值了,
不过我们看不出是函数返回了哈希值,还是把哈希值传给了参数三,没关系,
我们都算出来看看,为了方便阅读我们转写一份 rust demo,后面也好验证:
use wchar::{wch, wchar_t};
fn main() {
const PATH: &[wchar_t] = wch!(":\\WINDOWS\\SYSTEM32\\SVCHOST.EXE");
let mut sum: u32 = 0;
for &w in PATH {
sum = sum.wrapping_mul(0x25).wrapping_add(w as u32);
}
let mul = sum.wrapping_mul(0x12b9b0a5);
let m = mul >> 0x1f;
let h = mul ^ m - m;
println!("mod: {:08X}", h % 0x3b9aca07);
println!("ret: {:08X}", h.wrapping_mul(0x44b82f99));
}
更新 - 20241013:有读者朋友提醒我,这几个魔法数字其实也是有一些意思的,
不过要转换为十进制。比如 0x12b9b0a5
的十进制是 314159269
,很显然是 π,
但是第九位不是,所以我们先叫它「假π」。
而 0x3b9aca07
的十进制是 1000000007
,这是十亿后的第一个质数。
0x44b82f99
的十进制是 1152921497
,这个与「假π」互质,
这两个数字常被用于线性同余方法(LCG)。
所以我的推测是,微软的程序参考某种线性同余的算法设计了这个哈希函数, 事实上这个算法在当年八点三文件名过渡到长文件名时,就已经被使用了, 详情可以参考《A Tale of Two File Names》, 很有意思的文章,感谢作者分享。
不要问我线性同余法是什么,我对此一无所知。
回到过去
这个 demo 有用到一个依赖,所以我也贴一下 Cargo.toml:
[package]
name = "wctest"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
wchar = "*"
mod
是传给参数三的值,ret
是回传值,根据计算,svchost 的两个值如下:
$ cargo run
Compiling wctest v0.1.0 (D:\src\rs\wctest)
Finished dev [unoptimized + debuginfo] target(s) in 0.35s
Running `target\debug\wctest.exe`
mod: 2C69D9F1
ret: A37E8009
看一下注册表:
所以参数三就是哈希值,结案。
总结一下 AppId_Catalog 计算的要点:
wchar
(两个 byte
)储存路径"\0"
这下,如果我们愿意,我们可以自己写程序修改注册表了。