首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
逆向工程
发新帖
0
0
[原创]某XXE的黑签名和白签名解密
发表于: 12小时前
263
[原创]某XXE的黑签名和白签名解密
Panel_demo
12小时前
263
# xxe-boot.dat 加解密过程 一开始对着 `xxe-boot.dat` 看十六进制,前 16 个字节还能读,后面全是噪声。文件 81960 字节,熵快到 8 了,字符串扫出来也没什么像样的 ASCII。旁边同目录有 `xxe-BOOT.sys`,日志名还带 `xxe-boot-`,基本就是这个驱动在读。 驱动字符串表里搜不到 `xxe-boot.dat`。文件名是栈上拼的宽字符,`sub_14000EBB0` 里写死了两个后缀:`\xxe-boot.dat` 和 `\xxe-bootd.dat`。读文件走的是通用例程 `sub_14003B550`(ZwCreateFile + ZwReadFile),读完交给 `sub_1400451E0`,这个东西是个 thunk,真正干活的在 `.tvm0` 段,`sub_140187A78`。那段被 VM 糊过,反编译会跳、会 not/xchg 一堆寄存器,看着很烦,不过调用点还在,顺着 call 往下扒就行。 ## 文件不是整包糊一层 头 16 字节小端: ``` 00: 10 00 00 00 version = 16 04: 80 00 00 00 128 08: 90 00 00 00 144 0C: 98 3F 01 00 81816 ``` 一开始把 `0x80`、`0x90` 当成偏移,也对得上:签名从 `+0x10` 开始 128 字节,密文从 `+0x90` 开始 81816 字节,`16 + 128 + 81816 = 81960`。后来核对校验函数,这两个 dword 其实是长度和密文起点,值刚好长成偏移的样子。`sub_1400454C0` 要求 version 必须是 16,另外三个字段各有上限,对不上直接走人。 `+0x10` 那 128 字节别当密钥。公钥在驱动 `.rdata`,`unk_140058640`,1024 bit,指数 65537。拿密文做 SHA-1,PKCS#1 v1.5 解开这段,digest 对得上。签的是密文本身,文件头不进哈希。 ## 流密码在 VM 里 真正变换数据的是 `sub_140188C77`。调用约定糊在花指令里,第三个参数是立即数 `0xE628382B`,这就是种子。模数 `0xDC6A2367`。按 dword 走,加密解密同一套: ``` state = (seed * cipher_len) mod MOD for each dword w: out = state XOR w t = (out + w) mod MOD state = (t * t) mod MOD ``` 模运算不是直接 `div`。循环前用 `0x94A6A233` 做 32 位乘法取高再右移 31;循环里乘数是把 `0xB5AAD7E69418C57E` 按位取反得到的 64 位数,乘完 rdx 右移 30,再减模。手写的时候得跟汇编同一套,用 Python 的 `%` 对这个模也能过,但别自己发明另一套约减。 走完以后,最后一个明文 dword 当填充长度,合法值 1~7,驱动按这个数把尾巴砍掉。这份样本砍 7 字节。加密方向要把填充 dword 完全放在明文后面,长度凑成 4 的倍数,常用 4~7。这份明文 81809,补 7 刚好 81816,跟原文件对得上。 种子写死在驱动里,没有第二把钥匙。 ## 解开是什么 砍完填充,81809 字节,UTF-8 XML: ```xml <?xml version='1.0' encoding='utf-8'?> <config 8a46f2a810856326="drv-boot-u" f9f0327b95503039="201001205"> ``` 根上那两个属性名看着像哈希,其实是驱动把 64 位立即数格式化成小写十六进制。`sub_14003FB90` 干这个,`sub_140011310` 拿这两个串去树上查 `drv-boot-u` 和版本号 `201001205`。标签名运行时解码,IDA 字符串窗口里搜不到 `function`、`block_sign` 这种。 下面一堆 `<f>`,这份大概 797 条,分段大概是: - `function` 规则开关,编号从 400 起 - `block_sign` 拦截的签名者名字 - `block_file` / `block_file2` / `block_file3` 文件大小、MD5、拼在一起的 hash,size - `block_sernum` 序列号哈希 - `hook_device` 设备名,GIO、WinRing0、WinIo、inpoutx64 这类 - `trust_*` / `known_sign` 白名单路径和发布者 - `game_sign` / `game_path` 游戏侧,Valorant、CrossFire、DeltaForce 能对上 加载成功之后 `sub_140010440` 把这棵树丢进双缓冲,五分钟内不重复读。失败就记一笔错误码,配置不换。 ## 加载顺序 读文件 → 看头 → RSA 解开 128 字节 → 比对 SHA-1(密文) → 流密码还原 → 砍填充 → 当 XML 解析。 验签在 `sub_1400490D0`(新一点的构建地址会漂,逻辑没变)。PKCS#1 填充按教科书来:`00 01 FF... 00` + DigestInfo + 20 字节 SHA-1。对不上返回错误,上层映射成 101,`sub_140010440` 到此为止,不会去 `sub_140029D20` 装新表。 流密码改明文再加密,密文 SHA-1 必变,原签名立刻废。驱动里只有公钥,没有私钥。`--sig` 把旧签名拷过去,密文没动才过;动过一个字节,加载路径一样拒。 ## 总结 只能看,改了没用,没私钥,已卸载。 算法如下: ``` """ Encrypt and decrypt XXE-boot.dat. Recovered from XXE-BOOT.sys: sub_14000EBB0 reads \\XXE-boot.dat / \\XXE-bootd.dat sub_140187A78 parses the container and verifies the RSA signature sub_140188C77 DWORD stream cipher (seed immediate 0xE628382B) sub_1400490D0 PKCS#1 v1.5 / SHA-1 / RSA-1024 verify Container, little-endian: +0x00 u32 version 16 +0x04 u32 rsa_len 0x80 +0x08 u32 cipher_off 0x90 (header + signature) +0x0C u32 cipher_len ciphertext size, multiple of 4 +0x10 128 bytes RSA-1024 signature of SHA-1(ciphertext) +0x90 ... ciphertext The stream cipher is symmetric. Encryption needs no key beyond the constants in the driver. Re-signing needs the RSA private key, which is not in the driver; `encrypt` keeps the original signature when the ciphertext is unchanged, and otherwise writes 128 zero bytes and reports that verify will fail. Usage: python XXE-boot-crypt.py dec XXE-boot.dat XXE-boot.dec.xml python XXE-boot-crypt.py enc XXE-boot.dec.xml XXE-boot.dat --sig XXE-boot.dat python XXE-boot-crypt.py verify XXE-boot.dat """ from __future__ import annotations import argparse import hashlib import struct import sys from pathlib import Path VERSION = 16 # Header dwords are sizes, not file offsets: # [1] = 0x80 RSA signature length # [2] = 0x90 bytes from start of file to ciphertext RSA_SIZE = 0x80 HEADER_AND_SIG = 0x90 MOD = 0xDC6A2367 SEED = 0xE628382B MAGIC = 0x94A6A233 REDUCE_MUL = (~0xB5AAD7E69418C57E) & 0xFFFFFFFFFFFFFFFF # unk_140058640, RSA-1024 modulus. Exponent is 65537 (unk_1400586C0). RSA_N = int( "807667c0dc670e25e132db5676bd3def" "59aa24f640262bca90b73be6e20fb683" "89e47636ae5b8d6eb798acfe7937a8ea" "7bb0a4456cb21d2205e7c93bf5e7cea9" "35ae1c173d2890b9a1042f5b4979d435" "2436e5c954c78d4d28b0063088c048bd" "b7cfbbf57dd57f936319fd0bc58bb65b" "7224acba5309fbb2fad8bc9dddd0b4df", 16, ) RSA_E = 65537 SHA1_DIGESTINFO = bytes.fromhex("3021300906052b0e03021a05000414") def reduce32(value: int) -> int: """(value mod MOD) as implemented before the cipher loop.""" q = ((MAGIC * (value & 0xFFFFFFFF)) >> 32) >> 31 return (value - q * MOD) & 0xFFFFFFFF def reduce64(value: int) -> int: """(value mod MOD) as implemented inside the cipher loop.""" q = ((REDUCE_MUL * value) >> 64) >> 30 return value - q * MOD def crypt_dwords(data: bytes) -> tuple[bytes, int]: """XOR stream. Same function encrypts and decrypts. Returns (transformed_bytes, last_dword). For a real ciphertext the last dword is the 1..7 pad length stored by the encryptor. """ if len(data) % 4: raise ValueError(f"length {len(data)} is not a multiple of 4") state = reduce32((SEED * len(data)) & 0xFFFFFFFF) out = bytearray(len(data)) last = 0 for i in range(len(data) // 4): word = struct.unpack_from("<I", data, 4 * i)[0] mixed = (state ^ word) & 0xFFFFFFFF struct.pack_into("<I", out, 4 * i, mixed) state = reduce64(reduce64((mixed + word) & 0xFFFFFFFFFFFFFFFF) ** 2) & 0xFFFFFFFF last = mixed return bytes(out), last def default_pad(length: int) -> int: """4..7 bytes, so the pad dword sits entirely past the plaintext. sub_140188C77 reads the last plaintext dword as the strip count. That dword can be written freely only when it does not overlap the payload, which means the pad is at least 4. The value is also chosen so len(plain)+pad is a multiple of 4. """ return 4 + ((4 - (length % 4)) % 4) def pad_plain(plain: bytes, pad_len: int | None = None) -> tuple[bytes, int]: if pad_len is None: pad_len = default_pad(len(plain)) if not 4 <= pad_len <= 7: raise ValueError("pad must be 4..7; a shorter pad overlaps the plaintext dword") if (len(plain) + pad_len) % 4: raise ValueError("len(plain)+pad must be a multiple of 4") total = len(plain) + pad_len buf = bytearray(total) buf[: len(plain)] = plain struct.pack_into("<I", buf, total - 4, pad_len) return bytes(buf), pad_len def parse_container(blob: bytes) -> tuple[int, bytes, bytes]: if len(blob) < HEADER_AND_SIG: raise ValueError("file smaller than header") version, rsa_len, cipher_off, cipher_len = struct.unpack_from("<IIII", blob, 0) if version != VERSION: raise ValueError(f"version {version}, expected {VERSION}") if rsa_len != RSA_SIZE or cipher_off != HEADER_AND_SIG: raise ValueError(f"unexpected layout rsa_len={rsa_len:#x} cipher_off={cipher_off:#x}") if cipher_off + cipher_len > len(blob) or cipher_len % 4 or cipher_len < 4: raise ValueError("bad cipher span") signature = blob[0x10:0x10 + rsa_len] ciphertext = blob[cipher_off:cipher_off + cipher_len] return version, signature, ciphertext def pack_container(signature: bytes, ciphertext: bytes) -> bytes: if len(signature) != RSA_SIZE: raise ValueError(f"signature must be {RSA_SIZE} bytes") if len(ciphertext) % 4 or len(ciphertext) > 0xFFFFFF: raise ValueError("ciphertext length must be a multiple of 4 and <= 0xFFFFFF") header = struct.pack("<IIII", VERSION, RSA_SIZE, HEADER_AND_SIG, len(ciphertext)) return header + signature + ciphertext def decrypt_file(blob: bytes) -> bytes: _version, _signature, ciphertext = parse_container(blob) plain, pad = crypt_dwords(ciphertext) if not 1 <= pad <= 7: raise ValueError(f"pad dword {pad} is not 1..7; not an XXE-boot ciphertext") return plain[: len(plain) - pad] def encrypt_file(plain: bytes, signature: bytes | None = None, pad_len: int | None = None) -> bytes: padded, _pad = pad_plain(plain, pad_len) ciphertext, _last = crypt_dwords(padded) if signature is None: signature = b"\x00" * RSA_SIZE return pack_container(signature, ciphertext) def verify_signature(blob: bytes) -> bool: """RSA-1024 PKCS#1 v1.5 verify of SHA-1(ciphertext). Public key only.""" _version, signature, ciphertext = parse_container(blob) encoded = pow(int.from_bytes(signature, "big"), RSA_E, RSA_N).to_bytes(RSA_SIZE, "big") digest = hashlib.sha1(ciphertext).digest() expect = b"\x00\x01" + b"\xff" * (RSA_SIZE - 3 - len(SHA1_DIGESTINFO) - 20) + b"\x00" + SHA1_DIGESTINFO + digest return encoded == expect def sign_ciphertext(ciphertext: bytes, private_d: int) -> bytes: """PKCS#1 v1.5 SHA-1 sign. private_d is the RSA private exponent.""" digest = hashlib.sha1(ciphertext).digest() body = SHA1_DIGESTINFO + digest pad = RSA_SIZE - 3 - len(body) if pad < 8: raise ValueError("digest info does not fit RSA-1024") encoded = b"\x00\x01" + b"\xff" * pad + b"\x00" + body signed = pow(int.from_bytes(encoded, "big"), private_d, RSA_N) return signed.to_bytes(RSA_SIZE, "big") def _read_sig(path: Path | None) -> tuple[bytes | None, bytes | None]: """Return (signature, plaintext of that container). The second value is set only when path is a full XXE-boot container, so the caller can tell a copied signature from a signature of different bytes. """ if path is None: return None, None blob = path.read_bytes() if len(blob) == RSA_SIZE: return blob, None _version, signature, _ciphertext = parse_container(blob) return signature, decrypt_file(blob) def _report_stale_sig(plain: bytes, signed_plain: bytes | None) -> None: print("signature: absent or stale; loader sub_1400490D0 will reject this file") if signed_plain is None: print(" --sig was not an XXE-boot container, or was omitted") return if plain == signed_plain: print(" plaintext matches the signed file; signature bytes themselves do not verify") return print(f" plaintext {len(plain)} bytes, signed source {len(signed_plain)} bytes") n = min(len(plain), len(signed_plain)) diffs = [i for i in range(n) if plain[i] != signed_plain[i]] extra = abs(len(plain) - len(signed_plain)) print(f" {len(diffs)} different bytes in the overlap, length delta {extra}") for i in diffs[:3]: a = plain[max(0, i - 16): i + 24] b = signed_plain[max(0, i - 16): i + 24] print(f" at {i}: input {a!r}") print(f" signed {b!r}") if len(plain) != len(signed_plain): print(f" input tail {plain[-8:]!r}") print(f" signed tail {signed_plain[-8:]!r}") print(" RSA covers SHA-1(ciphertext). A copied signature verifies only when these bytes match.") def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) sub = parser.add_subparsers(dest="cmd", required=True) dec = sub.add_parser("dec", help="decrypt XXE-boot.dat to XML/plaintext") dec.add_argument("src", type=Path) dec.add_argument("dst", type=Path) dec.add_argument("--check-sig", action="store_true", help="require a valid RSA signature") enc = sub.add_parser("enc", help="encrypt plaintext back to XXE-boot.dat") enc.add_argument("src", type=Path) enc.add_argument("dst", type=Path) enc.add_argument("--sig", type=Path, help="128-byte signature, or an existing XXE-boot.dat to copy it from") enc.add_argument("--key", type=Path, help="text file containing the RSA private exponent (hex)") enc.add_argument("--pad", type=int, default=None, help="strip count 4..7 (default: 4..7 chosen so the pad dword does not overlap plaintext)") ver = sub.add_parser("verify", help="check the RSA-1024 SHA-1 signature") ver.add_argument("src", type=Path) info = sub.add_parser("info", help="print container fields") info.add_argument("src", type=Path) args = parser.parse_args(argv) if args.cmd == "dec": blob = args.src.read_bytes() if args.check_sig and not verify_signature(blob): print("signature: FAIL", file=sys.stderr) return 1 plain = decrypt_file(blob) args.dst.write_bytes(plain) print(f"decrypted {len(blob)} -> {len(plain)} bytes: {args.dst}") if verify_signature(blob): print("signature: OK") else: print("signature: FAIL (stream decrypt still completed)") return 0 if args.cmd == "enc": plain = args.src.read_bytes() signature, signed_plain = _read_sig(args.sig) blob = encrypt_file(plain, signature, args.pad) if args.key is not None: private_d = int(args.key.read_text().strip(), 16) _version, _old_sig, ciphertext = parse_container(blob) blob = pack_container(sign_ciphertext(ciphertext, private_d), ciphertext) args.dst.write_bytes(blob) print(f"encrypted {len(plain)} -> {len(blob)} bytes: {args.dst}") if verify_signature(blob): print("signature: OK") else: _report_stale_sig(plain, signed_plain) return 0 if args.cmd == "verify": ok = verify_signature(args.src.read_bytes()) print("signature:", "OK" if ok else "FAIL") return 0 if ok else 1 version, signature, ciphertext = parse_container(args.src.read_bytes()) print(f"version={version} rsa_len={RSA_SIZE:#x} cipher_off={HEADER_AND_SIG:#x} cipher_len={len(ciphertext)}") print(f"signature={signature[:16].hex()}...") print("signature:", "OK" if verify_signature(args.src.read_bytes()) else "FAIL") return 0 if __name__ == "__main__": raise SystemExit(main()) ```
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
12小时前 被Panel_demo编辑 ,原因:
#加密算法
收藏
・
0
点赞
・
0
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
Panel_demo
雪 币:
656
活跃值:
(2407)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
32
粉丝
10
关注
私信
Panel_demo
2
楼
Power by Doubao
12小时前
0
淡然他徒弟
雪 币:
5589
活跃值:
(7273)
能力值:
( LV10,RANK:160 )
在线值:
发帖
23
回帖
297
粉丝
124
关注
私信
淡然他徒弟
1
3
楼
删前留名
10小时前
0
X-Blades
雪 币:
43
活跃值:
(4203)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
56
粉丝
5
关注
私信
X-Blades
4
楼
希望不要删帖
8小时前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Panel_demo
6
发帖
32
回帖
10
RANK
关注
私信
他的文章
[原创]某XXE的黑签名和白签名解密
262
[原创]EDR-DataProtector
2423
[分享]万径寻踪:Windows 入侵检测与防御编程(免费版)读后感
1584
[分享]驱动隐藏傀儡兼容修复
4670
如何拥有更好的方案解决在透明加解密时需要把加密信息写到文件头带来的性能损耗
2822
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部