@yufengtadian/freedom-cli 1.13.2 → 1.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/README.md +44 -14
- package/lib/agents.js +182 -52
- package/lib/build.js +22 -7
- package/lib/cli.js +100 -7
- package/lib/shell.js +165 -61
- package/lib/toolchain.js +339 -0
- package/lib/utils.js +51 -0
- package/lib/verify.js +5 -4
- package/lib/webkit.js +45 -0
- package/package.json +2 -2
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +3 -3
- package/templates/desktop/backend/desktop.mjs +16 -6
- package/templates/desktop/freedom.config.js +4 -1
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +13 -2
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +52 -1
- package/templates/go/pkg/freedom/security.go +57 -7
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +3 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
|
@@ -6,19 +6,22 @@ package freedom
|
|
|
6
6
|
// 命中时静默退出(退出码 77,无提示),防止攻击者在调试器下单步追踪
|
|
7
7
|
// 密钥派生 / 容器解密逻辑,或直接读内存里的明文密钥。
|
|
8
8
|
//
|
|
9
|
-
//
|
|
9
|
+
// 七道独立信号(任一命中即判定被调试),覆盖不同层次的附加手段:
|
|
10
10
|
// 1. IsDebuggerPresent —— 读 PEB.BeingDebugged 的快捷 API(最易被 hook 篡改);
|
|
11
11
|
// 2. CheckRemoteDebuggerPresent —— 内核调试器 / 远程调试器;
|
|
12
12
|
// 3. 直接读 PEB.BeingDebugged —— 不经 API,绕过"只 patch API 返回值"的调试辅助;
|
|
13
13
|
// 4. NtQueryInformationProcess(ProcessDebugPort) —— 调试端口句柄;
|
|
14
14
|
// 5. NtQueryInformationProcess(ProcessDebugObjectHandle) —— 调试对象(无端口也命中);
|
|
15
|
-
// 6. NtQueryInformationProcess(ProcessDebugFlags) ——
|
|
15
|
+
// 6. NtQueryInformationProcess(ProcessDebugFlags) —— 标志位反向确认;
|
|
16
|
+
// 7. 线程上下文 DR0–DR3 —— 硬件断点(数据/内存断点常常不落在前六道上)。
|
|
16
17
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
18
|
+
// 第七道的实现约束:对本线程直接 GetThreadContext 会返回 ERROR_ACCESS_DENIED,
|
|
19
|
+
// 故用 Toolhelp 枚举进程内线程、以 THREAD_GET_CONTEXT 打开后读取(不挂起任何线程),
|
|
20
|
+
// 读不到上下文一律记未命中。只在启动期跑、只在 high 模式跑;
|
|
21
|
+
// 已知局限:抓的是"检测时刻已设下的硬件断点",运行中途 attach 再下断点不在射程内。
|
|
22
|
+
//
|
|
23
|
+
// 未纳入:时间差检测在 CI/低配机误判率高,误杀正常用户的代价高于其收益。
|
|
24
|
+
// 所有信号都遵循"探测失败即视为未命中",只在确证被调试时退出,杜绝误杀。
|
|
22
25
|
//
|
|
23
26
|
// 诚实边界:这些是抬升成本的对抗,不是不可绕过的墙。绕开者仍需逆向壳二进制、
|
|
24
27
|
// 复现掩码后的主密钥与 PBKDF2 参数,并在无调试器环境下取密钥。
|
|
@@ -39,10 +42,12 @@ var (
|
|
|
39
42
|
)
|
|
40
43
|
|
|
41
44
|
// 进程信息类别(PROCESSINFOCLASS)与各类别的返回长度,编号与长度均为本机实测校准:
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
45
|
+
//
|
|
46
|
+
// ProcessBasicInformation(0) 48 字节(x64,PEB 在偏移 8)
|
|
47
|
+
// ProcessDebugPort(7) 8 字节(HANDLE);len 给 4 会返回 STATUS_INFO_LENGTH_MISMATCH
|
|
48
|
+
// ProcessDebugObjectHandle(30) 8 字节;未被调试时返回 STATUS_PORT_NOT_SET(0xC0000353)
|
|
49
|
+
// ProcessDebugFlags(31) 4 字节(ULONG);未被调试时返回 1
|
|
50
|
+
//
|
|
46
51
|
// 注意 30/31 两号易写成 32/33(那是 LUIDDeviceMapsEnabled / BreakOnTermination),
|
|
47
52
|
// 编号或长度写错不会报错、只会静默失去一路信号,故此处配套测试锁死。
|
|
48
53
|
const (
|
|
@@ -66,7 +71,7 @@ func antiDebugCheck() {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
// debuggerPresent
|
|
74
|
+
// debuggerPresent 汇总七道信号。lazy DLL 加载失败(Proc.Find 出错)时该信号记为未命中。
|
|
70
75
|
func debuggerPresent() bool {
|
|
71
76
|
if r, _, _ := procIsDebuggerPresent.Call(); r != 0 {
|
|
72
77
|
return true
|
|
@@ -90,6 +95,10 @@ func debuggerPresent() bool {
|
|
|
90
95
|
if r, ok := ntQueryProcessInfo(processDebugFlags, debugFlagsSize); ok && r == 0 {
|
|
91
96
|
return true
|
|
92
97
|
}
|
|
98
|
+
// 硬件断点:数据断点 / 内存断点常不改动上面任何一处状态,只能看 DR0–DR3。
|
|
99
|
+
if hardwareBreakpointSet() {
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
93
102
|
return false
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -138,3 +147,118 @@ func pebBeingDebuggedSet() bool {
|
|
|
138
147
|
}
|
|
139
148
|
return *(*byte)(unsafe.Pointer(info.pebAddress + pebBeingDebuggedOff)) != 0
|
|
140
149
|
}
|
|
150
|
+
|
|
151
|
+
// ---- 第七道信号:硬件断点(DR0–DR3)----
|
|
152
|
+
//
|
|
153
|
+
// 数据断点与部分内存断点不改 PEB、不建调试端口,只写在线程上下文的调试寄存器里。
|
|
154
|
+
// 对本线程调 GetThreadContext 会被拒(ERROR_ACCESS_DENIED),因此走 Toolhelp 枚举
|
|
155
|
+
// 进程内线程 + THREAD_GET_CONTEXT 读取;任何一步失败都记未命中(宁漏不误杀)。
|
|
156
|
+
|
|
157
|
+
const (
|
|
158
|
+
th32csSnapThread = 0x00000002
|
|
159
|
+
threadQueryInfo = 0x0040
|
|
160
|
+
threadGetContext = 0x0010
|
|
161
|
+
threadAllAccess = threadQueryInfo | threadGetContext
|
|
162
|
+
// CONTEXT_AMD64(0x00100000) | CONTEXT_DEBUG_REGISTERS(0x10):不置此标志,
|
|
163
|
+
// GetThreadContext 根本不回填 Dr0–Dr7(读到的只是栈上残值,会误报)。
|
|
164
|
+
contextDebugRegisters = 0x00100000 | 0x10
|
|
165
|
+
dr7LocalBitsMask = 0b1111
|
|
166
|
+
invalidHandleVal = ^uintptr(0)
|
|
167
|
+
threadEntrySize = unsafe.Sizeof(threadEntry32{})
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
var (
|
|
171
|
+
procCreateToolhelp32Snapshot = kernel32AntiDebug.NewProc("CreateToolhelp32Snapshot")
|
|
172
|
+
procThread32First = kernel32AntiDebug.NewProc("Thread32First")
|
|
173
|
+
procThread32Next = kernel32AntiDebug.NewProc("Thread32Next")
|
|
174
|
+
procOpenThread = kernel32AntiDebug.NewProc("OpenThread")
|
|
175
|
+
procGetThreadContext = kernel32AntiDebug.NewProc("GetThreadContext")
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// threadEntry32 是 Toolhelp 线程快照项(x64 布局:32 字节,末尾有对齐填充)。
|
|
179
|
+
type threadEntry32 struct {
|
|
180
|
+
dwSize uint32
|
|
181
|
+
cntUsage uint32
|
|
182
|
+
th32ThreadID uint32
|
|
183
|
+
th32OwnerPID uint32
|
|
184
|
+
tpBasePrio int32
|
|
185
|
+
dwFlags uint32
|
|
186
|
+
hModule uintptr
|
|
187
|
+
_pad uint32
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// contextAMD64 是 Windows x64 CONTEXT 的局部视图,偏移严格按 winnt.h:
|
|
191
|
+
// P1Home–P6Home 占 0x00–0x2F,ContextFlags@0x30,MxCsr@0x34,段寄存器 0x38–0x43,
|
|
192
|
+
// EFlags@0x44,Dr0@0x48、Dr1@0x50、Dr2@0x58、Dr3@0x60、Dr6@0x68、Dr7@0x70,
|
|
193
|
+
// 其后寄存器/XMM/XSTATE 一并归入 padding(总长取 0x400)。
|
|
194
|
+
// 偏移错一位就会把 EFlags/残值当成断点地址(误杀),故 anti_debug_windows_test.go 逐字段挂断言。
|
|
195
|
+
type contextAMD64 struct {
|
|
196
|
+
_ [48]byte
|
|
197
|
+
contextFlags uint32
|
|
198
|
+
_ [20]byte // MxCsr + 6 个段寄存器 + EFlags
|
|
199
|
+
dr0 uint64
|
|
200
|
+
dr1 uint64
|
|
201
|
+
dr2 uint64
|
|
202
|
+
dr3 uint64
|
|
203
|
+
dr6 uint64
|
|
204
|
+
dr7 uint64
|
|
205
|
+
_ [1024 - 120]byte
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
func hardwareBreakpointSet() bool {
|
|
209
|
+
for _, fn := range []uintptr{
|
|
210
|
+
procCreateToolhelp32Snapshot.Addr(), procThread32First.Addr(), procThread32Next.Addr(),
|
|
211
|
+
procOpenThread.Addr(), procGetThreadContext.Addr(),
|
|
212
|
+
} {
|
|
213
|
+
if fn == 0 {
|
|
214
|
+
return false // DLL 未加载成功:本信号记未命中
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
snap, _, _ := procCreateToolhelp32Snapshot.Call(th32csSnapThread, 0)
|
|
218
|
+
if snap == invalidHandleVal || snap == 0 {
|
|
219
|
+
return false
|
|
220
|
+
}
|
|
221
|
+
defer syscall.CloseHandle(syscall.Handle(snap))
|
|
222
|
+
pid := uintptr(syscall.Getpid())
|
|
223
|
+
var te threadEntry32
|
|
224
|
+
te.dwSize = uint32(threadEntrySize)
|
|
225
|
+
for ok, _, _ := procThread32First.Call(snap, uintptr(unsafe.Pointer(&te))); ok != 0; ok, _, _ = procThread32Next.Call(snap, uintptr(unsafe.Pointer(&te))) {
|
|
226
|
+
if te.th32OwnerPID != uint32(pid) {
|
|
227
|
+
continue // 只看本进程的线程
|
|
228
|
+
}
|
|
229
|
+
if threadHasHardwareBreakpoint(uintptr(te.th32ThreadID)) {
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return false
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// threadHasHardwareBreakpoint 读一个线程的调试寄存器;读不到即未命中。
|
|
237
|
+
// 刻意不 SuspendThread:挂起 Go 运行时线程有死锁风险,而调试器 attach 时本就把线程挂住了
|
|
238
|
+
// ——那种状态下 GetThreadContext 才会成功并带回 Dr0–Dr7,正是这一路要抓的场景。
|
|
239
|
+
func threadHasHardwareBreakpoint(tid uintptr) bool {
|
|
240
|
+
h, _, _ := procOpenThread.Call(threadAllAccess, 0, tid)
|
|
241
|
+
if h == invalidHandleVal || h == 0 {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
defer syscall.CloseHandle(syscall.Handle(h))
|
|
245
|
+
// 16 字节留白供 round16 向上对齐(GetThreadContext 要求 CONTEXT 16 字节对齐)。
|
|
246
|
+
var ctxRaw [unsafe.Sizeof(contextAMD64{}) + 16]byte
|
|
247
|
+
ctx := (*contextAMD64)(unsafe.Pointer(round16(&ctxRaw[0])))
|
|
248
|
+
ctx.contextFlags = contextDebugRegisters // 输入:只取调试寄存器
|
|
249
|
+
r, _, _ := procGetThreadContext.Call(h, uintptr(unsafe.Pointer(ctx)))
|
|
250
|
+
if r == 0 || ctx.contextFlags&contextDebugRegisters == 0 {
|
|
251
|
+
return false // 线程在跑 / 权限不足 / 未回填:一律未命中
|
|
252
|
+
}
|
|
253
|
+
return (ctx.dr7&dr7LocalBitsMask) != 0 &&
|
|
254
|
+
(ctx.dr0 != 0 || ctx.dr1 != 0 || ctx.dr2 != 0 || ctx.dr3 != 0)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// round16 把切片首地址向上对齐到 16 字节(GetThreadContext 要求 CONTEXT 16 字节对齐)。
|
|
258
|
+
func round16(p *byte) *byte {
|
|
259
|
+
a := uintptr(unsafe.Pointer(p))
|
|
260
|
+
if d := a % 16; d != 0 {
|
|
261
|
+
a += 16 - d
|
|
262
|
+
}
|
|
263
|
+
return (*byte)(unsafe.Pointer(a))
|
|
264
|
+
}
|
|
@@ -42,11 +42,11 @@ type ProcBackend struct {
|
|
|
42
42
|
maxLine int // 单条 stdout 行(一次响应)上限字节;超限即那条响应过大,不得中断整个通道
|
|
43
43
|
|
|
44
44
|
// —— W4 崩溃自愈(对标 Tauri shell spawn 的失败处理 / sidecar 重启)——
|
|
45
|
-
rp RestartPolicy
|
|
46
|
-
gen int
|
|
47
|
-
pdone chan struct{}
|
|
48
|
-
attempts int
|
|
49
|
-
startedAt time.Time
|
|
45
|
+
rp RestartPolicy // 重启策略(零值=不重启,保持旧行为)
|
|
46
|
+
gen int // 每次拉起递增;readLoop 据此识别"自己那一期",过期实例不触发重启
|
|
47
|
+
pdone chan struct{} // 当前这期进程 Wait 完成后由 readLoop 关闭(Close 等它,不再自己 Wait,避免双重 Wait)
|
|
48
|
+
attempts int // 连续崩溃重启计数
|
|
49
|
+
startedAt time.Time // 本期进程启动时刻(存活够久则重置 attempts)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// RestartPolicy 是后端进程崩溃后的自动重启策略(经 SetRestartPolicy 启用)。
|
|
@@ -18,10 +18,10 @@ import (
|
|
|
18
18
|
|
|
19
19
|
const (
|
|
20
20
|
wmSize = 0x0005
|
|
21
|
-
wmMove
|
|
21
|
+
wmMove = 0x0003
|
|
22
22
|
wmExitSizeMove = 0x0212
|
|
23
|
-
wmSetFocus
|
|
24
|
-
wmKillFocus
|
|
23
|
+
wmSetFocus = 0x0007
|
|
24
|
+
wmKillFocus = 0x0008
|
|
25
25
|
|
|
26
26
|
sizeMaximized = 1
|
|
27
27
|
sizeMinimized = 2
|
|
@@ -244,8 +244,8 @@ func enumMonitorsProc(hmon, hdc, lprci, data uintptr) uintptr {
|
|
|
244
244
|
Width: int(mi.rcMonitor.right - mi.rcMonitor.left),
|
|
245
245
|
Height: int(mi.rcMonitor.bottom - mi.rcMonitor.top),
|
|
246
246
|
WorkX: int(mi.rcWork.left), WorkY: int(mi.rcWork.top),
|
|
247
|
-
WorkWidth:
|
|
248
|
-
WorkHeight:
|
|
247
|
+
WorkWidth: int(mi.rcWork.right - mi.rcWork.left),
|
|
248
|
+
WorkHeight: int(mi.rcWork.bottom - mi.rcWork.top),
|
|
249
249
|
ScaleFactor: scale,
|
|
250
250
|
IsPrimary: mi.dwFlags&monitorInfofPrimary != 0,
|
|
251
251
|
})
|
|
@@ -65,6 +65,11 @@ type Config struct {
|
|
|
65
65
|
MinHeight int
|
|
66
66
|
// Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
|
|
67
67
|
Debug bool
|
|
68
|
+
// DisableAntiDebug 关闭 high 安全模式的调试器探测(见 antiDebugEnabled)。
|
|
69
|
+
// 默认 false:探测在位。仅在合法运行环境被误判(部分沙箱 / EDR / 虚拟化环境会让
|
|
70
|
+
// NtQueryInformationProcess 一类调用给出命中值)时由发布方显式置真。
|
|
71
|
+
// 关闭不影响容器解密与 .integrity 校验——那两道才是源码保护的本体。
|
|
72
|
+
DisableAntiDebug bool
|
|
68
73
|
// Backend 指定后端适配器。为空时默认使用内嵌 Go 后端(配合 Bind 使用)。
|
|
69
74
|
Backend Backend
|
|
70
75
|
// HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
|
|
@@ -113,6 +118,10 @@ type App struct {
|
|
|
113
118
|
backendExplicit bool
|
|
114
119
|
secureBackendDir string
|
|
115
120
|
|
|
121
|
+
// runtimeVersion = config.json / app.bin 容器里声明的「本应用」版本。
|
|
122
|
+
// 与编译期的 Version(通用壳的版本)分属两个版本域,自更新比较只认前者。
|
|
123
|
+
runtimeVersion string
|
|
124
|
+
|
|
116
125
|
// upMu 保护 upPending:CheckUpdate 验签通过的更新条目,install 桥接只认它
|
|
117
126
|
//(前端无法注入未验签的 URL/哈希)。见 updater.go。
|
|
118
127
|
upMu sync.Mutex
|
|
@@ -231,7 +240,7 @@ func (a *App) Unbind(name string) {
|
|
|
231
240
|
func (a *App) Run() {
|
|
232
241
|
// high 产物:解密前先跑一次反调试(此时密钥与明文都还没进内存),
|
|
233
242
|
// 解密后再跑一次(兜住"启动后才附加调试器")。非 high 应用不受影响。
|
|
234
|
-
if hasSecureResources() {
|
|
243
|
+
if hasSecureResources() && a.antiDebugEnabled() {
|
|
235
244
|
antiDebugCheck()
|
|
236
245
|
}
|
|
237
246
|
// 通用壳:先加载 exe 同目录 resources/ 覆盖窗口与后端配置(CLI build 写入;
|
|
@@ -248,8 +257,10 @@ func (a *App) Run() {
|
|
|
248
257
|
}
|
|
249
258
|
// high 模式的临时后端目录随进程退出清理(defer 早于后端 Close 注册 → 后于其执行)。
|
|
250
259
|
defer a.cleanupSecureBackend()
|
|
260
|
+
// Ctrl+C / SIGTERM / 控制台关闭:defer 不会执行,须显式清扫(见 installSecureShutdown)。
|
|
261
|
+
a.installSecureShutdown()
|
|
251
262
|
// high 模式:解密后复查调试器(见 Run 开头的"解密前一次")。
|
|
252
|
-
if a.secure {
|
|
263
|
+
if a.secure && a.antiDebugEnabled() {
|
|
253
264
|
antiDebugCheck()
|
|
254
265
|
}
|
|
255
266
|
// 平台层回调(单实例转发/热键)经 currentApp 找到本实例推送事件。
|
|
@@ -35,14 +35,14 @@ func validCopyData(dwData uintptr, cbData uint32, lpData uintptr) bool {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
type msgWindow struct {
|
|
38
|
-
mu
|
|
39
|
-
hwnd
|
|
40
|
-
ready
|
|
41
|
-
created
|
|
42
|
-
hotkeys
|
|
43
|
-
nextHKID
|
|
44
|
-
onHotkey
|
|
45
|
-
onData
|
|
38
|
+
mu sync.Mutex
|
|
39
|
+
hwnd uintptr
|
|
40
|
+
ready chan struct{}
|
|
41
|
+
created bool
|
|
42
|
+
hotkeys map[string]uint32 // 前端 id → 热键数值 id
|
|
43
|
+
nextHKID uint32
|
|
44
|
+
onHotkey func(id string)
|
|
45
|
+
onData func(payload []byte)
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
var (
|
|
@@ -52,6 +52,7 @@ type runtimeUpdater struct {
|
|
|
52
52
|
// 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成;Name 仅作来源标记不应用。
|
|
53
53
|
type runtimeConfigFile struct {
|
|
54
54
|
Name string `json:"name"`
|
|
55
|
+
Version string `json:"version"`
|
|
55
56
|
Title string `json:"title"`
|
|
56
57
|
TitleBar string `json:"titlebar"`
|
|
57
58
|
Width int `json:"width"`
|
|
@@ -64,6 +65,9 @@ type runtimeConfigFile struct {
|
|
|
64
65
|
URL string `json:"url"`
|
|
65
66
|
SingleInstance *bool `json:"singleInstance"`
|
|
66
67
|
Updater *runtimeUpdater `json:"updater"`
|
|
68
|
+
// Capabilities 收口前端可调用面。通用壳里编译不进应用作者的 Config 字面量,
|
|
69
|
+
// 没有这条透传则 freedom.config.js 写 capabilities 会被静默忽略(= 全开)。
|
|
70
|
+
Capabilities *Capabilities `json:"capabilities"`
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
// resourcesDirOverride 是单测缝:非空时替代 exe 同目录的 resources 定位。
|
|
@@ -135,6 +139,15 @@ func (a *App) loadRuntimeConfigPlain() error {
|
|
|
135
139
|
|
|
136
140
|
// applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
|
|
137
141
|
func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
|
|
142
|
+
// 应用版本:通用壳编译期注入的 Version 是「壳」的版本,不是「这个应用」的版本。
|
|
143
|
+
// 自更新比较必须用后者,否则要么永远报 uncomparable(壳未打戳时是 "dev"),
|
|
144
|
+
// 要么拿壳的版本号跟应用的清单比——换来的仍是同一个壳,更新永远收敛不了。
|
|
145
|
+
if rc.Version != "" {
|
|
146
|
+
a.runtimeVersion = rc.Version
|
|
147
|
+
}
|
|
148
|
+
if rc.Capabilities != nil && a.cfg.Capabilities == nil {
|
|
149
|
+
a.cfg.Capabilities = rc.Capabilities
|
|
150
|
+
}
|
|
138
151
|
// 窗口标题:config.json 的 title 覆盖编译期默认("Freedom App")。
|
|
139
152
|
if rc.Title != "" {
|
|
140
153
|
a.cfg.Title = rc.Title
|
|
@@ -193,6 +206,16 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
|
|
|
193
206
|
}
|
|
194
207
|
}
|
|
195
208
|
|
|
209
|
+
// appVersion 返回「本应用」的版本:优先用 config.json / app.bin 容器里声明的值,
|
|
210
|
+
// 回落编译期 Version(examples 自建壳走 ldflags 注入)。通用壳的 Version 是壳自己的
|
|
211
|
+
// 版本域,拿它跟应用发布清单比是两个数在比——见 updater.go 的 CheckUpdate。
|
|
212
|
+
func (a *App) appVersion() string {
|
|
213
|
+
if a != nil && a.runtimeVersion != "" {
|
|
214
|
+
return a.runtimeVersion
|
|
215
|
+
}
|
|
216
|
+
return Version
|
|
217
|
+
}
|
|
218
|
+
|
|
196
219
|
// backendWorkDir 返回后端进程的工作目录。
|
|
197
220
|
func (a *App) backendWorkDir() (string, error) {
|
|
198
221
|
if a.secureBackendDir != "" {
|
|
@@ -201,6 +224,31 @@ func (a *App) backendWorkDir() (string, error) {
|
|
|
201
224
|
return resourcesDir()
|
|
202
225
|
}
|
|
203
226
|
|
|
227
|
+
// installSecureShutdown 注册信号退出通道(Run 调用)。
|
|
228
|
+
//
|
|
229
|
+
// 清扫顺序刻意是「先后端、后目录」:后端子进程的工作目录就是 high 模式解出的那个明文临时
|
|
230
|
+
// 目录,它还活着时 Windows 会因文件被占用让 RemoveAll 静默失败——明文源码继续留在 %TEMP%,
|
|
231
|
+
// 而信号路径紧接着就 os.Exit,下次启动的 GC 也帮不上(PID 仍存活)。ProcBackend.Close 自带
|
|
232
|
+
// closed 幂等与 3s 等待上界,与 Run 正常退出的 defer 双跑无害。
|
|
233
|
+
// 非 high 模式下 cleanupSecureBackend 是 no-op,但后端该停照样得停,故不再判安全档。
|
|
234
|
+
func (a *App) installSecureShutdown() {
|
|
235
|
+
secureCleanup = func() {
|
|
236
|
+
a.closeBackendForShutdown()
|
|
237
|
+
a.cleanupSecureBackend()
|
|
238
|
+
}
|
|
239
|
+
installShutdownCleanup()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// closeBackendForShutdown 停后端子进程;未配置后端(nil)时为空操作。
|
|
243
|
+
func (a *App) closeBackendForShutdown() {
|
|
244
|
+
if a.backend == nil {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if err := a.backend.Close(); err != nil {
|
|
248
|
+
fmt.Fprintf(os.Stderr, "freedom: shutdown backend close: %v\n", err)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
204
252
|
// cleanupSecureBackend 删除 high 模式解出的临时后端目录(Run 退出时调用)。
|
|
205
253
|
// 进程被强杀时目录会残留在系统临时区——内容仍可被下一次启动的壳重新解出,
|
|
206
254
|
// 且明文源码本就来自内存,故不引入跨进程清扫。
|
|
@@ -208,7 +256,10 @@ func (a *App) cleanupSecureBackend() {
|
|
|
208
256
|
if a.secureBackendDir == "" {
|
|
209
257
|
return
|
|
210
258
|
}
|
|
211
|
-
|
|
259
|
+
// 删不掉必须在退出前喊出来:这是「磁盘不留明文」承诺当场失守的时刻,静默等于隐瞒。
|
|
260
|
+
if err := os.RemoveAll(a.secureBackendDir); err != nil {
|
|
261
|
+
fmt.Fprintf(os.Stderr, "freedom: 明文后端临时目录未清除(后端源码可能仍可读取):%s: %v\n", a.secureBackendDir, err)
|
|
262
|
+
}
|
|
212
263
|
a.secureBackendDir = ""
|
|
213
264
|
}
|
|
214
265
|
|
|
@@ -70,6 +70,7 @@ var masterKeyCipher = []byte{
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// masterSecret 还原主密钥(PBKDF2 的 password)。
|
|
73
|
+
// 生产路径请用 withMasterSecret:明文只存在于回调作用域内,返回前逐字节抹零。
|
|
73
74
|
func masterSecret() []byte {
|
|
74
75
|
out := make([]byte, len(masterKeyCipher))
|
|
75
76
|
for i, b := range masterKeyCipher {
|
|
@@ -78,6 +79,21 @@ func masterSecret() []byte {
|
|
|
78
79
|
return out
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
// withMasterSecret 把还原出的主密钥交给 fn,并在 fn 返回后立刻抹零该缓冲区
|
|
83
|
+
// (Go 的分配器会复用内存,留着明文等于给内存扫描留靶子)。
|
|
84
|
+
func withMasterSecret(fn func(secret []byte)) {
|
|
85
|
+
secret := masterSecret()
|
|
86
|
+
defer clearBytes(secret)
|
|
87
|
+
fn(secret)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// clearBytes 抹零一段字节。编译器不会把对切片的显式写零当死代码删掉(逐元素赋值)。
|
|
91
|
+
func clearBytes(b []byte) {
|
|
92
|
+
for i := range b {
|
|
93
|
+
b[i] = 0
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
81
97
|
// securePayload 是 app.bin 解密后的载荷(与 JS 侧加密载荷结构一致)。
|
|
82
98
|
// Backend 为 high 模式下的后端源码表:键是相对 resources 的路径(斜杠分隔),
|
|
83
99
|
// 值是 base64 内容 d 与 POSIX 权限位 m——后端源码不再明文落盘(FRDM1 的缺口)。
|
|
@@ -172,11 +188,19 @@ func deriveSecurityKey(appName string, salt []byte) secureKey {
|
|
|
172
188
|
return k
|
|
173
189
|
}
|
|
174
190
|
base := append([]byte(securityDeriveSalt+":"+appIdentityName(appName)), salt...)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
var k secureKey
|
|
192
|
+
withMasterSecret(func(secret []byte) {
|
|
193
|
+
kek := pbkdf2HMACSHA256(secret, base, securityPbkdf2Iter, securityKeyLen)
|
|
194
|
+
h := hmac.New(sha256.New, kek)
|
|
195
|
+
h.Write([]byte(securityMacLabel))
|
|
196
|
+
k = secureKey{enc: kek, mac: h.Sum(nil)}
|
|
197
|
+
})
|
|
179
198
|
if len(secureKeyCache) >= secureKeyCacheMax { // 防御性上限:单进程正常只用一两条
|
|
199
|
+
// 淘汰不是"丢掉引用"就完事:被丢弃的密钥字节仍会被分配器复用,先抹零。
|
|
200
|
+
for _, stale := range secureKeyCache {
|
|
201
|
+
clearBytes(stale.enc)
|
|
202
|
+
clearBytes(stale.mac)
|
|
203
|
+
}
|
|
180
204
|
secureKeyCache = make(map[string]secureKey)
|
|
181
205
|
}
|
|
182
206
|
secureKeyCache[id] = k
|
|
@@ -247,6 +271,11 @@ func decryptAppBin(appName string, data []byte) (*securePayload, error) {
|
|
|
247
271
|
if err := json.Unmarshal(plain, &p); err != nil {
|
|
248
272
|
return nil, fmt.Errorf("app.bin 载荷无效:%w", err)
|
|
249
273
|
}
|
|
274
|
+
// 与打包端 lib/security.js 同一道门:认证通过不等于结构可用。少了 html/config 的容器
|
|
275
|
+
// 会让上层回落到内置占位页——那是「加密产物跑起来了但内容是空的」级别的静默降级。
|
|
276
|
+
if p.HTML == "" || p.Config == "" {
|
|
277
|
+
return nil, errors.New("app.bin 载荷结构无效:缺少 html/config 字段")
|
|
278
|
+
}
|
|
250
279
|
for rel := range p.Backend {
|
|
251
280
|
if !isSafeRelPath(rel) {
|
|
252
281
|
return nil, fmt.Errorf("app.bin 后端路径非法:%q", rel)
|
|
@@ -272,15 +301,18 @@ func isSafeRelPath(rel string) bool {
|
|
|
272
301
|
return true
|
|
273
302
|
}
|
|
274
303
|
|
|
275
|
-
// verifyIntegrity 校验 resources/.integrity
|
|
304
|
+
// verifyIntegrity 校验 resources/.integrity 清单。
|
|
276
305
|
// 比对 app.bin 的 HMAC-SHA256,防容器被整体替换;backend 已进容器,其完整性由
|
|
277
306
|
// 容器认证标签一并保证,清单里的 backend 段仅作向后兼容。
|
|
278
|
-
//
|
|
307
|
+
// 清单缺失是致命错误而非"旧产物豁免":app.bin 的认证钥由「exe 名 + 容器自带盐」派生,
|
|
308
|
+
// 两者都在产物里,攻击者能为任意内容自造合法容器;.integrity 正是那道产物外的重放绑定,
|
|
309
|
+
// 而「删掉 .integrity」就是绕过它最省事的手法——跳过校验等于把这道门留给攻击者关。
|
|
310
|
+
// 自 FRDM2 起 CLI 恒写清单(lib/build.js),拒绝无清单容器不会误伤任何正规产物。
|
|
279
311
|
func verifyIntegrity(dir string, k secureKey, appBin []byte) error {
|
|
280
312
|
raw, err := os.ReadFile(filepath.Join(dir, ".integrity"))
|
|
281
313
|
if err != nil {
|
|
282
314
|
if errors.Is(err, os.ErrNotExist) {
|
|
283
|
-
return
|
|
315
|
+
return errors.New("resources/.integrity 缺失:app.bin 未经完整性绑定,产物可能被整体替换(请重新 freedom build)")
|
|
284
316
|
}
|
|
285
317
|
return err
|
|
286
318
|
}
|
|
@@ -328,6 +360,24 @@ func hasSecureResources() bool {
|
|
|
328
360
|
return err == nil
|
|
329
361
|
}
|
|
330
362
|
|
|
363
|
+
// antiDebugEnabled 是 high 模式调试器探测的开关。
|
|
364
|
+
//
|
|
365
|
+
// 探测本身是 fail-safe 的(每个信号取不到数据一律按"未命中"处理),但六路信号里
|
|
366
|
+
// IsDebuggerPresent / NtQueryInformationProcess 在部分合法环境本就会给出真值:
|
|
367
|
+
// 逆向沙箱、EDR/杀软注入、某些虚拟化与远程桌面环境。被误判的用户需要一条**不必重新
|
|
368
|
+
// 打包**就能脱身的路,所以除了发布方编译期的 Config.DisableAntiDebug,这里再认一个
|
|
369
|
+
// 运行期环境变量。
|
|
370
|
+
//
|
|
371
|
+
// 关掉只影响"探测"这一道:容器解密、HMAC 认证与 .integrity 校验照常,
|
|
372
|
+
// 源码保护的本体不在此。config.json 里刻意不给这个开关——非 high 产物的
|
|
373
|
+
// config.json 是明文可改的,那样等于把防御交给攻击者。
|
|
374
|
+
func (a *App) antiDebugEnabled() bool {
|
|
375
|
+
if a != nil && a.cfg.DisableAntiDebug {
|
|
376
|
+
return false
|
|
377
|
+
}
|
|
378
|
+
return os.Getenv("FREEDOM_DISABLE_ANTIDEBUG") != "1"
|
|
379
|
+
}
|
|
380
|
+
|
|
331
381
|
// loadSecureResources 尝试加载 high 模式加密资源。
|
|
332
382
|
// 返回 (载荷, 是否命中, 错误):resources/app.bin 不存在时命中=false(非 high 产物),
|
|
333
383
|
// 命中但解密/校验失败时返回错误(拒绝静默回退明文,避免降级攻击)。
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// 退出前清扫(跨平台):high 模式把后端源码解密到系统临时目录,正常退出由 Run 的
|
|
4
|
+
// defer 删除;但 SIGINT / SIGTERM(Ctrl+C、`kill`、systemd stop)会让 Go 直接终止
|
|
5
|
+
// 进程,defer 与 cleanup 都不执行,明文后端源码就永久躺在 %TEMP% / /tmp 里——
|
|
6
|
+
// 直到用户下次再启动这个应用才会被启动期 GC 回收。对「磁盘不留明文」的承诺来说
|
|
7
|
+
// 这是真空档,故在此显式接管常规信号:命中即先清扫再退。
|
|
8
|
+
//
|
|
9
|
+
// 边界:SIGKILL / taskkill /F / 断电无法拦截(无用户态代码可跑),仍靠下次启动的
|
|
10
|
+
// gcStaleSecureBackendDirs 兜底;Windows 的控制台事件见 shutdown_windows.go。
|
|
11
|
+
|
|
12
|
+
import (
|
|
13
|
+
"os"
|
|
14
|
+
"os/signal"
|
|
15
|
+
"sync"
|
|
16
|
+
"syscall"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// secureCleanup 由 Run 注册(Run 未跑过就没有可清的东西)。
|
|
20
|
+
var secureCleanup func()
|
|
21
|
+
|
|
22
|
+
// cleanupOnce 用指针:单测需要一个可复位的实例来断言「只挂一次」。
|
|
23
|
+
var cleanupOnce = new(sync.Once)
|
|
24
|
+
|
|
25
|
+
// platformShutdownHooks 由各平台补挂退出通道(Windows 控制台事件),无额外通道的平台为 nil。
|
|
26
|
+
var platformShutdownHooks func()
|
|
27
|
+
|
|
28
|
+
// installShutdownCleanup 幂等地挂上退出清扫通道,由 Run 在注册 secureCleanup 后调用。
|
|
29
|
+
func installShutdownCleanup() {
|
|
30
|
+
cleanupOnce.Do(func() {
|
|
31
|
+
ch := make(chan os.Signal, 1)
|
|
32
|
+
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
|
|
33
|
+
go waitForShutdownSignal(ch)
|
|
34
|
+
if platformShutdownHooks != nil {
|
|
35
|
+
platformShutdownHooks()
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// waitForShutdownSignal 命中信号即清扫退场;拆成变量便于单测注入(真实信号无法跨平台自投)。
|
|
41
|
+
var waitForShutdownSignal = func(ch <-chan os.Signal) {
|
|
42
|
+
<-ch
|
|
43
|
+
exitAfterCleanup()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// exitAfterCleanup 清扫后以 130(128+SIGINT 惯例)退出。
|
|
47
|
+
func exitAfterCleanup() {
|
|
48
|
+
if fn := secureCleanup; fn != nil {
|
|
49
|
+
fn()
|
|
50
|
+
}
|
|
51
|
+
shutdownExit(130)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// shutdownExit 是 os.Exit 的槽位:单测靠它断言"先清扫再退出",否则会把测试进程一起带走。
|
|
55
|
+
var shutdownExit = os.Exit
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
// Windows 退出通道补挂:SIGTERM 在本平台不存在,控制台关闭 / 注销 / 关机走
|
|
6
|
+
// SetConsoleCtrlHandler。命中 CTRL_CLOSE_EVENT 时系统只给约 5 秒宽限,
|
|
7
|
+
// 删一个临时目录绑绑有余;GUI 无控制台时注册会失败,静默跳过(不影响信号通道)。
|
|
8
|
+
//
|
|
9
|
+
// 覆盖面要说清:`-H windowsgui` 发布的正式壳根本没有控制台,这条通道对它永不触发,
|
|
10
|
+
// 其正常关窗由 WM_CLOSE → DestroyWindow → Run 的 defer 清扫,强杀/崩溃由下次启动的
|
|
11
|
+
// gcStaleSecureBackendDirs 兜底。本通道实际服务控制台子系统构建(裸 go build、调试期直接跑 exe)。
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"syscall"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
var (
|
|
18
|
+
kernel32Shutdown = syscall.NewLazyDLL("kernel32.dll")
|
|
19
|
+
procSetConsoleCtrlHandler = kernel32Shutdown.NewProc("SetConsoleCtrlHandler")
|
|
20
|
+
|
|
21
|
+
consoleCtrlCloseEvent uint32 = 2
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// consoleCtrlHandlerProc 的形参与返回必须是 uintptr:syscall.NewCallback 会校验
|
|
25
|
+
// 「一个 uintptr 宽返回值」,写成 BOOL 习惯的 uint32 直接 panic
|
|
26
|
+
// (compileCallback: expected function with one uintptr-sized result)。
|
|
27
|
+
func consoleCtrlHandlerProc(ctrlType uintptr) uintptr {
|
|
28
|
+
if uint32(ctrlType) == consoleCtrlCloseEvent {
|
|
29
|
+
exitAfterCleanup() // 不返回:清扫后立即退场
|
|
30
|
+
}
|
|
31
|
+
return 0 // 继续交给链上其它处理器
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// consoleCtrlHandler 固定在包级求值一次:syscall.NewCallback 每次调用都占用全局句柄表,
|
|
35
|
+
// 重复申请会泄漏(与 events_windows.go 的同款约束)。
|
|
36
|
+
var consoleCtrlHandler = syscall.NewCallback(consoleCtrlHandlerProc)
|
|
37
|
+
|
|
38
|
+
func init() {
|
|
39
|
+
platformShutdownHooks = installConsoleCtrlCleanup
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func installConsoleCtrlCleanup() {
|
|
43
|
+
if err := procSetConsoleCtrlHandler.Find(); err != nil {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
procSetConsoleCtrlHandler.Call(consoleCtrlHandler, 1)
|
|
47
|
+
}
|
|
@@ -232,7 +232,7 @@ func saveWindowState(dir string, st WindowState) error {
|
|
|
232
232
|
|
|
233
233
|
// ---- os / process 信息 ----
|
|
234
234
|
|
|
235
|
-
func osInfo() map[string]interface{} {
|
|
235
|
+
func osInfo(app *App) map[string]interface{} {
|
|
236
236
|
host, _ := os.Hostname()
|
|
237
237
|
info := map[string]interface{}{
|
|
238
238
|
"platform": runtime.GOOS,
|
|
@@ -240,7 +240,7 @@ func osInfo() map[string]interface{} {
|
|
|
240
240
|
"hostname": host,
|
|
241
241
|
"osVersion": osVersionString(), // 平台分文件:osver_windows.go / osver_other.go
|
|
242
242
|
"goVersion": runtime.Version(),
|
|
243
|
-
"appVersion":
|
|
243
|
+
"appVersion": app.appVersion(), // 运行时声明优先,回落 ldflags 注入值
|
|
244
244
|
"numCPU": runtime.NumCPU(),
|
|
245
245
|
}
|
|
246
246
|
// M6:WebView2 Runtime 探测回显(仅 Windows 且检出时给键,其他平台省略)
|
|
@@ -311,7 +311,7 @@ func (a *App) sysGeneric(method string, args map[string]json.RawMessage) (result
|
|
|
311
311
|
}
|
|
312
312
|
return sf.keys(), true, nil
|
|
313
313
|
case "os.info":
|
|
314
|
-
info := osInfo()
|
|
314
|
+
info := osInfo(a)
|
|
315
315
|
// M3:回显生效的能力收口,前端可据此隐藏入口(nil=全开时省略该键)。
|
|
316
316
|
if a.cfg.Capabilities != nil {
|
|
317
317
|
info["capabilities"] = map[string][]string{
|