Bridge Security source deep dive
一个原生 x64dbg 插件,把调试器能力通过 MCP Streamable HTTP 与 SSE 暴露给 AI 客户端。真正值得拆的是:它不是脚本壳,而是把 Win32 socket、JSON-RPC、工具 schema、x64dbg bridge 和菜单配置全部编进同一个 Zig 插件。
Bridge 是能力边界,不只是 FFI
bridge 层决定 AI 客户端最后能触到什么。src/core/bridge.zig:267(证据 src/evidence.md:28) 声明运行时函数指针,src/core/bridge.zig:338(证据 src/evidence.md:29) 解析 x64dbg/bridge DLL,src/core/bridge.zig:419(证据 src/evidence.md:30) 暴露表达式求值,src/core/bridge.zig:433(证据 src/evidence.md:31) 与 src/core/bridge.zig:437(证据 src/evidence.md:32) 直接读写调试目标内存。
▸边界意识
server 的 token 校验在 src/core/mcp_server.zig:374(证据 src/evidence.md:22) 和 src/core/mcp_server.zig:388(证据 src/evidence.md:23),配置 token 在 src/core/config.zig:250(证据 src/evidence.md:39) 生成并保存,UI 在 src/core/config.zig:406(证据 src/evidence.md:58) 可复制/生成 token。它有认证,但不是完整安全沙箱。
运行时符号解析
▸动态解析
267var bridge_module: ?HMODULE = null; 268var dbg_module: ?HMODULE = null; 269 270// Bridge functions 271pub var DbgCmdExec: *const fn ([*:0]const u8) callconv(.c) BOOL = undefined; 272pub var DbgCmdExecDirect: *const fn ([*:0]const u8) callconv(.c) BOOL = undefined; 273pub var DbgIsDebugging: *const fn () callconv(.c) BOOL = undefined; 274pub var DbgIsRunning: *const fn () callconv(.c) BOOL = undefined; 275pub var DbgIsRunLocked: *const fn () callconv(.c) BOOL = undefined; 276pub var DbgValFromString: *const fn ([*:0]const u8) callconv(.c) duint = undefined; 277pub var DbgMemRead: *const fn (va: duint, dest: [*]u8, size: duint) callconv(.c) BOOL = undefined; 278pub var DbgMemWrite: *const fn (va: duint, src: [*]const u8, size: duint) callconv(.c) BOOL = undefined; 279pub var DbgGetLabelAt: *const fn (addr: duint, segment: c_int, text: [*]u8) callconv(.c) BOOL = undefined; 280pub var DbgGetCommentAt: *const fn (addr: duint, text: [*]u8) callconv(.c) BOOL = undefined; 281pub var DbgGetModuleAt: *const fn (addr: duint, text: [*]u8) callconv(.c) BOOL = undefined; 282pub var DbgModBaseFromName: *const fn ([*:0]const u8) callconv(.c) duint = undefined; 283pub var DbgMemFindBaseAddr: *const fn (addr: duint, size: *duint) callconv(.c) duint = undefined; 284pub var DbgSetCommentAt: *const fn (addr: duint, text: [*:0]const u8) callconv(.c) BOOL = undefined; 285pub var DbgSetLabelAt: *const fn (addr: duint, text: [*:0]const u8) callconv(.c) BOOL = undefined; 286 287// Plugin functions 288pub var _plugin_registercallback: *const fn (pluginHandle: c_int, cbType: CBTYPE, cbPlugin: CBPLUGIN) callconv(.c) void = undefined; 289pub var _plugin_unregistercallback: *const fn (pluginHandle: c_int, cbType: CBTYPE) callconv(.c) BOOL = undefined; 290pub var _plugin_registercommand: *const fn (pluginHandle: c_int, command: [*:0]const u8, cbCommand: CBPLUGINCOMMAND, debugonly: BOOL) callconv(.c) BOOL = undefined; 291pub var _plugin_unregistercommand: *const fn (pluginHandle: c_int, command: [*:0]const u8) callconv(.c) BOOL = undefined; 292pub var _plugin_menuadd: *const fn (hMenu: c_int, title: [*:0]const u8) callconv(.c) c_int = undefined; 293pub var _plugin_menuaddentry: *const fn (hMenu: c_int, hEntry: c_int, title: [*:0]const u8) callconv(.c) BOOL = undefined; 294pub var _plugin_menuaddseparator: *const fn (hMenu: c_int) callconv(.c) BOOL = undefined; 295const MenuSetIconFn = *const fn (hMenu: c_int, icon: *const ICONDATA) callconv(.c) void; 296pub var _plugin_menuseticon: ?MenuSetIconFn = null; 297pub var _plugin_logputs: *const fn ([*:0]const u8) callconv(.c) void = undefined; 298pub var _plugin_menuclear: *const fn (hMenu: c_int) callconv(.c) BOOL = undefined; 299const EntrySetIconFn = *const fn (pluginHandle: c_int, hEntry: c_int, icon: *const ICONDATA) callconv(.c) void; 300pub var _plugin_menuentryseticon: ?EntrySetIconFn = null; 301 302// Extended bridge functions (optional) 303const DbgDisasmFastAtFn = *const fn (addr: duint, info: *BASIC_INSTRUCTION_INFO) callconv(.c) BOOL; 304pub var DbgDisasmFastAt: ?DbgDisasmFastAtFn = null; 305const DbgGetBpListFn = *const fn (bptype: c_int, list: *BPMAP) callconv(.c) BOOL; 306pub var DbgGetBpList: ?DbgGetBpListFn = null; 307const DbgMemMapFn = *const fn (memmap: *MEMMAP) callconv(.c) BOOL; 308pub var DbgMemMap: ?DbgMemMapFn = null; 309const DbgGetThreadListFn = *const fn (list: *THREADLIST) callconv(.c) void; 310pub var DbgGetThreadList: ?DbgGetThreadListFn = null; 311const BridgeFreeFn = *const fn (ptr: *anyopaque) callconv(.c) void; 312pub var BridgeFree: ?BridgeFreeFn = null; 313 314// Extended bridge functions (optional) — continued 315const DbgFunctionGetFn = *const fn (addr: duint, start: *duint, end: *duint) callconv(.c) BOOL; 316pub var DbgFunctionGet: ?DbgFunctionGetFn = null; 317 318// GUI functions (from x64bridge.dll) 319const GuiGetDisassemblyFn = *const fn (addr: duint, text: [*]u8) callconv(.c) BOOL; 320pub var GuiGetDisassembly: ?GuiGetDisassemblyFn = null;
338pub fn init() !void { 339 const bridge_dll: [*:0]const u8 = if (@sizeOf(usize) == 8) 340 "x64bridge.dll" 341 else 342 "x32bridge.dll"; 343 344 const plugin_dll: [*:0]const u8 = if (@sizeOf(usize) == 8) 345 "x64dbg.dll" 346 else 347 "x32dbg.dll"; 348 349 bridge_module = GetModuleHandleA(bridge_dll) orelse 350 return error.BridgeNotFound; 351 352 dbg_module = GetModuleHandleA(plugin_dll) orelse 353 return error.BridgeNotFound; 354 355 const m = bridge_module.?; 356 const d = dbg_module.?; 357 358 // Dbg functions (from x64bridge.dll / x32bridge.dll) 359 DbgCmdExec = resolve(@TypeOf(DbgCmdExec), m, "DbgCmdExec"); 360 DbgCmdExecDirect = resolve(@TypeOf(DbgCmdExecDirect), m, "DbgCmdExecDirect"); 361 DbgIsDebugging = resolve(@TypeOf(DbgIsDebugging), m, "DbgIsDebugging"); 362 DbgIsRunning = resolve(@TypeOf(DbgIsRunning), m, "DbgIsRunning"); 363 DbgIsRunLocked = resolve(@TypeOf(DbgIsRunLocked), m, "DbgIsRunLocked"); 364 DbgValFromString = resolve(@TypeOf(DbgValFromString), m, "DbgValFromString"); 365 DbgMemRead = resolve(@TypeOf(DbgMemRead), m, "DbgMemRead"); 366 DbgMemWrite = resolve(@TypeOf(DbgMemWrite), m, "DbgMemWrite"); 367 DbgGetLabelAt = resolve(@TypeOf(DbgGetLabelAt), m, "DbgGetLabelAt"); 368 DbgGetCommentAt = resolve(@TypeOf(DbgGetCommentAt), m, "DbgGetCommentAt"); 369 DbgGetModuleAt = resolve(@TypeOf(DbgGetModuleAt), m, "DbgGetModuleAt"); 370 DbgModBaseFromName = resolve(@TypeOf(DbgModBaseFromName), m, "DbgModBaseFromName"); 371 DbgMemFindBaseAddr = resolve(@TypeOf(DbgMemFindBaseAddr), m, "DbgMemFindBaseAddr"); 372 DbgSetCommentAt = resolve(@TypeOf(DbgSetCommentAt), m, "DbgSetCommentAt"); 373 DbgSetLabelAt = resolve(@TypeOf(DbgSetLabelAt), m, "DbgSetLabelAt"); 374 375 // Extended bridge functions (optional) 376 DbgDisasmFastAt = resolveOptional(DbgDisasmFastAtFn, m, "DbgDisasmFastAt"); 377 DbgGetBpList = resolveOptional(DbgGetBpListFn, m, "DbgGetBpList"); 378 DbgMemMap = resolveOptional(DbgMemMapFn, m, "DbgMemMap"); 379 DbgGetThreadList = resolveOptional(DbgGetThreadListFn, m, "DbgGetThreadList"); 380 BridgeFree = resolveOptional(BridgeFreeFn, m, "BridgeFree"); 381 GuiGetDisassembly = resolveOptional(GuiGetDisassemblyFn, m, "GuiGetDisassembly"); 382 DbgFunctionGet = resolveOptional(DbgFunctionGetFn, m, "DbgFunctionGet"); 383 384 // Plugin functions (from x64dbg.dll / x32dbg.dll) 385 _plugin_registercallback = resolve(@TypeOf(_plugin_registercallback), d, "_plugin_registercallback"); 386 _plugin_unregistercallback = resolve(@TypeOf(_plugin_unregistercallback), d, "_plugin_unregistercallback"); 387 _plugin_registercommand = resolve(@TypeOf(_plugin_registercommand), d, "_plugin_registercommand"); 388 _plugin_unregistercommand = resolve(@TypeOf(_plugin_unregistercommand), d, "_plugin_unregistercommand"); 389 _plugin_menuadd = resolve(@TypeOf(_plugin_menuadd), d, "_plugin_menuadd"); 390 _plugin_menuaddentry = resolve(@TypeOf(_plugin_menuaddentry), d, "_plugin_menuaddentry"); 391 _plugin_menuaddseparator = resolve(@TypeOf(_plugin_menuaddseparator), d, "_plugin_menuaddseparator"); 392 _plugin_menuseticon = resolveOptional(MenuSetIconFn, d, "_plugin_menuseticon"); 393 _plugin_menuclear = resolve(@TypeOf(_plugin_menuclear), d, "_plugin_menuclear"); 394 _plugin_menuentryseticon = resolveOptional(EntrySetIconFn, d, "_plugin_menuentryseticon"); 395 _plugin_logputs = resolve(@TypeOf(_plugin_logputs), d, "_plugin_logputs"); 396}
src/core/bridge.zig:338(证据 src/evidence.md:29) 依指针宽度选择 x64dbg 或 x32dbg 相关 DLL,src/core/bridge.zig:267(证据 src/evidence.md:28) 后的函数指针把必须能力和 optional 能力分开。好处是单代码库可建双架构,坏处是缺失符号只能运行期发现。
令牌和配置
▸AuthToken 生命周期
152pub const Config = struct { 153 ip: [64]u8 = undefined, 154 ip_len: usize = 0, 155 port: u16 = 0, 156 auto_start: bool = true, 157 auth_token: [64]u8 = undefined, 158 auth_token_len: usize = 0, 159 160 pub fn ipSlice(self: *const Config) []const u8 { 161 return self.ip[0..self.ip_len]; 162 } 163 164 pub fn tokenSlice(self: *const Config) []const u8 { 165 return self.auth_token[0..self.auth_token_len]; 166 } 167 168 pub fn hasAuth(self: *const Config) bool { 169 return self.auth_token_len > 0; 170 } 171}; 172 173var config_path: [512]u8 = undefined; 174var config_path_len: usize = 0; 175 176fn getConfigPath() [*:0]const u8 { 177 if (config_path_len > 0) return @ptrCast(&config_path); 178 179 var module_path: [512]u8 = undefined; 180 const len = GetModuleFileNameA(null, &module_path, 512); 181 if (len == 0) { 182 const fallback = "mcp_config.json"; 183 @memcpy(config_path[0..fallback.len], fallback); 184 config_path[fallback.len] = 0; 185 config_path_len = fallback.len; 186 return @ptrCast(&config_path); 187 } 188 189 // Find last backslash 190 var last_sep: usize = 0; 191 for (0..len) |i| { 192 if (module_path[i] == '\\') last_sep = i; 193 } 194 195 const filename = "mcp_config.json"; 196 @memcpy(config_path[0 .. last_sep + 1], module_path[0 .. last_sep + 1]); 197 @memcpy(config_path[last_sep + 1 .. last_sep + 1 + filename.len], filename); 198 config_path[last_sep + 1 + filename.len] = 0; 199 config_path_len = last_sep + 1 + filename.len; 200 return @ptrCast(&config_path); 201} 202 203pub fn load() Config { 204 const default_port: u16 = if (@sizeOf(usize) == 8) 9094 else 9095; 205 var cfg = Config{ .port = default_port }; 206 const default_ip = "0.0.0.0"; 207 @memcpy(cfg.ip[0..default_ip.len], default_ip); 208 cfg.ip_len = default_ip.len; 209 210 const path = getConfigPath(); 211 const h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, null); 212 if (h == INVALID_HANDLE or h == null) return ensureToken(&cfg); 213 defer _ = CloseHandle(h); 214 215 var buf: [512]u8 = undefined; 216 var bytes_read: u32 = 0; 217 if (ReadFile(h, &buf, 512, &bytes_read, null) == 0) return ensureToken(&cfg); 218 219 const json_data = buf[0..bytes_read]; 220 221 // Parse "IpAddress":"<value>" 222 if (findJsonString(json_data, "IpAddress")) |ip_val| { 223 if (ip_val.len > 0 and ip_val.len < 64) { 224 @memcpy(cfg.ip[0..ip_val.len], ip_val); 225 cfg.ip_len = ip_val.len; 226 } 227 } 228 229 // Parse "Port":<number> 230 if (findJsonNumber(json_data, "Port")) |port_val| { 231 cfg.port = port_val; 232 } 233 234 // Parse "AutoStart":true/false 235 if (findJsonBool(json_data, "AutoStart")) |val| { 236 cfg.auto_start = val; 237 } 238 239 // Parse "AuthToken":"<value>" 240 if (findJsonString(json_data, "AuthToken")) |token_val| { 241 if (token_val.len > 0 and token_val.len <= 64) { 242 @memcpy(cfg.auth_token[0..token_val.len], token_val); 243 cfg.auth_token_len = token_val.len; 244 } 245 } 246 247 return ensureToken(&cfg); 248} 249 250fn ensureToken(cfg: *Config) Config { 251 if (cfg.auth_token_len == 0) { 252 var tok: [32]u8 = undefined; 253 generateToken(&tok); 254 @memcpy(cfg.auth_token[0..32], tok[0..32]); 255 cfg.auth_token_len = 32; 256 save(cfg); 257 } 258 return cfg.*;
src/core/config.zig:152(证据 src/evidence.md:65) 的 Config 同时存 IP、端口、auto_start 和 token;src/core/config.zig:203(证据 src/evidence.md:38) 设默认值,src/core/config.zig:250(证据 src/evidence.md:39) 首次缺 token 就生成并保存,src/core/config.zig:286(证据 src/evidence.md:40) 写回 JSON。这个 token 是基本门锁,不是细粒度权限系统。
配置 UI 也是治理面
▸用户可见边界
406pub fn showDialog(parentHwnd: usize) void { 407 parent_hwnd = @ptrFromInt(parentHwnd); 408 _ = CreateThread(null, 0, dialogThread, null, 0, null); 409} 410 411fn dialogThread(_: ?*anyopaque) callconv(.winapi) u32 { 412 const hInst = GetModuleHandleA(null); 413 414 if (!class_registered) { 415 const wc = WNDCLASSEXA{ 416 .lpfnWndProc = wndProc, 417 .hInstance = hInst, 418 .hbrBackground = @ptrFromInt(@as(usize, @intCast(1 + @as(c_int, 15)))), 419 .lpszClassName = CLASS_NAME, 420 .hCursor = null, 421 }; 422 _ = RegisterClassExA(&wc); 423 class_registered = true; 424 } 425 426 ui_font = CreateFontA(-14, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 5, 0, "Segoe UI\x00"); 427 ui_font_bold = CreateFontA(-14, 0, 0, 0, 700, 0, 0, 0, 0, 0, 0, 5, 0, "Segoe UI\x00"); 428 ui_font_small = CreateFontA(-12, 0, 0, 0, 400, 0, 0, 0, 0, 0, 0, 5, 0, "Segoe UI\x00"); 429 430 dlg_hwnd = CreateWindowExA( 431 WS_EX_DLGMODALFRAME, 432 CLASS_NAME, 433 "MCP Server Configuration\x00", 434 WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, 435 100, 436 100, 437 DLG_W, 438 DLG_H, 439 parent_hwnd, 440 null, 441 hInst, 442 null, 443 ); 444 445 if (dlg_hwnd == null) return 1; 446 447 // Disable parent 448 if (parent_hwnd != null) _ = EnableWindow(parent_hwnd, 0); 449 450 // Center on screen 451 const sw = GetSystemMetrics(SM_CXSCREEN); 452 const sh = GetSystemMetrics(SM_CYSCREEN); 453 _ = SetWindowPos(dlg_hwnd, null, @divTrunc(sw - DLG_W, 2), @divTrunc(sh - DLG_H, 2), 0, 0, SWP_NOZORDER | SWP_NOSIZE); 454 455 _ = ShowWindow(dlg_hwnd, 5); 456 _ = UpdateWindow(dlg_hwnd); 457 458 var msg: MSG = undefined; 459 while (GetMessageA(&msg, null, 0, 0) > 0) { 460 if (IsDialogMessageA(dlg_hwnd, &msg) == 0) { 461 _ = TranslateMessage(&msg); 462 _ = DispatchMessageA(&msg); 463 } 464 } 465 466 if (ui_font != null) { _ = DeleteObject(ui_font); ui_font = null; } 467 if (ui_font_bold != null) { _ = DeleteObject(ui_font_bold); ui_font_bold = null; } 468 if (ui_font_small != null) { _ = DeleteObject(ui_font_small); ui_font_small = null; } 469 470 return 0; 471} 472 473fn createCtrl(class: [*:0]const u8, text: ?[*:0]const u8, style: u32, x: c_int, y: c_int, w: c_int, h: c_int, id: c_int) HWND { 474 const hInst = GetModuleHandleA(null);
src/core/config.zig:406(证据 src/evidence.md:58) 附近生成 token 字段、Generate、Copy 按钮,src/core/config.zig:550(证据 src/evidence.md:41) 保存时校验 IP/port 并重启服务。UI 文案提醒 0.0.0.0 与 127.0.0.1,但默认仍需用户主动改。
事件回流
▸调试事件进入日志
344 pushEvent(msg[0 .. msg.len - 1]); // strip null terminator for event log 345} 346 347fn cbInitDebug(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 348 logAndPush("[x64dbg-MCP Server] Debug session started.\x00"); 349} 350 351fn cbStopDebug(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 352 logAndPush("[x64dbg-MCP Server] Debug session stopped.\x00"); 353} 354 355fn cbCreateProcess(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 356 logAndPush("[x64dbg-MCP Server] Process created.\x00"); 357} 358 359fn cbExitProcess(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 360 logAndPush("[x64dbg-MCP Server] Process exited.\x00"); 361} 362 363fn cbCreateThread(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 364 logAndPush("[x64dbg-MCP Server] Thread created.\x00"); 365} 366 367fn cbExitThread(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 368 logAndPush("[x64dbg-MCP Server] Thread exited.\x00"); 369} 370 371fn cbSystemBreakpoint(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 372 logAndPush("[x64dbg-MCP Server] System breakpoint.\x00"); 373} 374 375fn cbLoadDll(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 376 logAndPush("[x64dbg-MCP Server] DLL loaded.\x00"); 377} 378 379fn cbUnloadDll(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 380 logAndPush("[x64dbg-MCP Server] DLL unloaded.\x00"); 381} 382 383fn cbOutputDebugString(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 384 logAndPush("[x64dbg-MCP Server] OutputDebugString received.\x00"); 385} 386 387fn cbException(_: bridge.CBTYPE, info: *anyopaque) callconv(.c) void { 388 const cb: *bridge.PLUG_CB_EXCEPTION = @ptrCast(@alignCast(info)); 389 const rec = &cb.Exception.ExceptionRecord; 390 const first_chance = cb.Exception.dwFirstChance; 391 392 var buf: [256]u8 = undefined; 393 const prefix = "Exception 0x"; 394 @memcpy(buf[0..prefix.len], prefix); 395 var pos: usize = prefix.len; 396 pos += fmtHex(rec.ExceptionCode, buf[pos..]); 397 const at = " at 0x"; 398 @memcpy(buf[pos .. pos + at.len], at); 399 pos += at.len; 400 pos += fmtHexAddr(rec.ExceptionAddress, buf[pos..]); 401 if (first_chance != 0) { 402 const fc = " (first chance)"; 403 @memcpy(buf[pos .. pos + fc.len], fc); 404 pos += fc.len; 405 } else { 406 const sc = " (second chance)"; 407 @memcpy(buf[pos .. pos + sc.len], sc); 408 pos += sc.len; 409 } 410 pushEvent(buf[0..pos]); 411 412 // Also log to x64dbg console with prefix 413 var log_buf: [256]u8 = undefined; 414 const log_prefix = "[x64dbg-MCP Server] "; 415 @memcpy(log_buf[0..log_prefix.len], log_prefix); 416 @memcpy(log_buf[log_prefix.len .. log_prefix.len + pos], buf[0..pos]); 417 log_buf[log_prefix.len + pos] = 0; 418 bridge.logPuts(@ptrCast(&log_buf)); 419} 420 421fn cbBreakpoint(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 422 logAndPush("[x64dbg-MCP Server] Breakpoint hit.\x00"); 423} 424 425fn cbPauseDebug(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 426 logAndPush("[x64dbg-MCP Server] Debugger paused.\x00"); 427} 428 429fn cbResumeDebug(_: bridge.CBTYPE, _: *anyopaque) callconv(.c) void { 430 logAndPush("[x64dbg-MCP Server] Debugger resumed.\x00");
src/main.zig:344(证据 src/evidence.md:42)、src/main.zig:387(证据 src/evidence.md:43)、src/main.zig:421(证据 src/evidence.md:44) 说明插件把生命周期、异常和断点事件推入 event log。结合 src/core/mcp_server.zig:499(证据 src/evidence.md:45) 的 SSE,这能给客户端反馈,但它还不是不可抵赖审计。
安全建议
▸部署建议
基于源码,最务实的安全配置是:把 src/core/mcp_server.zig:37(证据 src/evidence.md:52) 的默认开放监听改为 127.0.0.1;保留 src/core/mcp_server.zig:374(证据 src/evidence.md:22) 的 bearer 检查;对 src/mcp/tools.zig:943(证据 src/evidence.md:53) ExecuteDebuggerCommand、src/mcp/tools.zig:1321(证据 src/evidence.md:35) WriteMemToAddress 和 src/mcp/tools.zig:2506(证据 src/evidence.md:54) DumpMemory 做 per-tool allowlist。