WinSock 小分析(三)WSCSetApplicationCategory 如何操作注册表

现在我们距离知道是谁操作了注册表又近了一步, 这次我们看看 WSCSetApplicationCategory 做了什么

导航

本篇已完结,列出目录方便查阅

  1. 代理软件和 docker 的冲突
  2. NoLSP.exe 主要做了什么
  3. WSCSetApplicationCategory 如何操作注册表

本文是第三部分

文件位置

不出意外 WS2_32.dll 会有好多个,这时候我们应该去看这个位置:

ws dll location

因为多数情况下 System32 是 windows 的默认起始目录。为了避免对 windows 造成影响, 我们复制一份出来然后在 cutter 中打开:

cutter open ws2

因为这个是导出函数,所以我们可以直接从函数列表中找到:

WSCSetApplicationCategory

直接看反编译:

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,和我们的目标一样!

reg appid catalog

看来这要是我们的最后一篇了,是不是还有点小难过。别担心,正戏才刚刚开始。

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.18000f958arg3_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 是什么呢?

p2

是个冒号”:”,后续的字符串指针都是用从冒号开始的指针了,也就是说, 盘符与哈希值无关。接下来又调用了 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));
}

这个 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

看一下注册表:

reg appid catalog

所以参数三就是哈希值,结案。

总结

总结一下 AppId_Catalog 计算的要点:

  1. 使用 wchar(两个 byte )储存路径
  2. 路径从冒号开始计算,不计算最后字符串的 "\0"
  3. 路径使用全大写字母

这下,如果我们愿意,我们可以自己写程序修改注册表了。