Posted in

【Go语言系统级安全禁用指南】:从PE启动隔离到内存防护的7层加固实战

第一章:Go语言禁用PE系统的核心原理与边界定义

Go语言本身并不直接“禁用”PE(Portable Executable)系统,而是通过其编译模型与运行时设计,在特定场景下规避或绕过Windows PE生态的典型依赖链。这一行为并非强制性封锁,而是源于Go静态链接默认策略、无C运行时依赖、以及对Windows子系统的抽象层级选择。

PE系统在Windows中的角色定位

PE格式是Windows可执行文件的标准容器,承载代码节、数据节、导入表(IAT)、重定位信息等。传统C/C++程序依赖MSVCRT或UCRT动态链接库,并通过PE导入表声明外部函数调用。Go程序默认以-ldflags="-s -w"静态编译,生成的二进制文件不包含导入表(Import Table为空),也不依赖任何DLL——这使其在PE结构上“合法但精简”,实质脱离了PE动态链接生态。

Go编译器对PE头的裁剪机制

当使用GOOS=windows GOARCH=amd64 go build -ldflags="-H=pe(默认即启用)时,Go链接器生成标准PE头,但主动清空以下字段:

  • IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_IMPORT] → RVA/Size设为0
  • IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT] → 置零
  • 不写入.rdata节中的导入名称表(INT)

可通过objdump -x main.exe | grep -A5 "Import"验证该行为。

边界定义:什么情况下Go仍需PE兼容性

场景 是否依赖PE机制 说明
调用Windows API(syscall) 否 Go通过syscall.NewLazySystemDLL在运行时手动LoadLibrary+GetProcAddress,绕过PE导入解析
使用cgo调用C函数 是 启用cgo后生成带完整导入表的PE,依赖msvcrt.dll等
加载资源(图标、版本信息) 是 需向PE资源节(.rsrc)嵌入数据,此时保留PE结构完整性

验证PE精简状态的命令

# 检查导入表是否存在(预期输出为空)
$ objdump -p main.exe | grep -A10 "Import"

# 查看节区信息,确认无.rdata(导入相关节)或其大小为0
$ objdump -h main.exe | grep -E "\.rdata|\.idata"

此机制使Go二进制在Windows上表现为“PE格式外壳+自包含内核”,既满足操作系统加载要求,又消除了传统PE程序的动态链接耦合性。

第二章:PE启动隔离机制的深度实现

2.1 PE文件头解析与Go运行时注入点识别

PE(Portable Executable)文件头是Windows可执行体的元数据枢纽,包含IMAGE_DOS_HEADER、IMAGE_NT_HEADERS及节表等关键结构。Go编译生成的二进制默认启用-buildmode=exe,其.text节末尾紧邻runtime.textflag符号,构成运行时初始化的天然锚点。

关键注入时机窗口

  • runtime·rt0_go 函数入口(位于.text节起始)
  • main_init调用前的runtime.doInit链
  • runtime.main启动前的_cgo_init(若启用CGO)

Go二进制典型节布局(x86-64)

节名 偏移特征 运行时语义
.text 包含runtime·rt0_go 启动引导代码
.data 含runtime·firstmoduledata 模块数据链表头
.rdata 存go.func.*函数元信息 反射与栈帧解析基础
// 获取PE可选头中ImageBase地址(Go默认为0x400000)
peHdr := (*imageNtHeaders)(unsafe.Pointer(&buf[0x3C]))
optHdr := (*imageOptionalHeader64)(unsafe.Pointer(uintptr(unsafe.Pointer(peHdr)) + unsafe.Sizeof(*peHdr)))
fmt.Printf("ImageBase: 0x%x\n", optHdr.ImageBase) // 输出:0x400000

该代码从PE签名偏移0x3C定位NT头,再跳过标准头获取64位可选头,读取ImageBase——此值决定Go运行时全局变量基址重定位基准,是注入后修正runtime.g0或m0指针的关键依据。

graph TD
    A[读取DOS头e_lfanew] --> B[定位NT头]
    B --> C[解析可选头ImageBase/SizeOfImage]
    C --> D[遍历节表定位.text/.data]
    D --> E[搜索runtime·rt0_go符号RVA]

2.2 Windows可执行体签名绕过检测的Go原生拦截策略

核心拦截点:CreateProcessW 钩子注入

Go 程序可通过 syscall.NewLazyDLL 加载 kernel32.dll,在进程创建前拦截签名验证路径:

// hook CreateProcessW to inspect image signature pre-execution
proc := kernel32.NewProc("CreateProcessW")
ret, _, _ := proc.Call(
    uintptr(0),          // lpApplicationName
    uintptr(unsafe.Pointer(&cmd)), // lpCommandLine
    0, 0, 0, 0x00000004, // CREATE_SUSPENDED
    0, 0, 0, 0,
)

该调用以 CREATE_SUSPENDED 启动进程,使系统暂不校验 Authenticode 签名,为后续内存补丁留出窗口。

关键绕过条件

  • 必须在 WinVerifyTrust 调用前完成内存映射修改
  • 仅对 IMAGE_NT_OPTIONAL_HDR64_MAGIC 结构中 DataDirectory[4](CERTIFICATE_TABLE)置零有效
  • 不触发 CiValidateImageHeader 的内核级签名强制检查

绕过有效性对比表

检测层级 原生Go拦截 PowerShell注入 Sysmon v14+
用户态签名检查 ✅ 规避 ⚠️ 易被钩子捕获 ❌ 无法绕过
内核签名验证 ❌ 不适用 ❌ 失败 ✅ 记录事件
graph TD
    A[Go程序调用CreateProcessW] --> B[以CREATE_SUSPENDED启动]
    B --> C[读取PE头CertificateTable]
    C --> D[将VirtualAddress/Size置零]
    D --> E[ResumeThread触发执行]

2.3 基于syscall包的PE加载器Hook与动态禁用实践

Windows PE加载器核心行为(如LdrLoadDll)常通过syscall指令触发内核系统调用。Go语言中可利用syscall包直接构造NTAPI调用链,绕过用户态API层实现细粒度拦截。

Hook关键入口点

  • 定位ntdll.dll中LdrLoadDll导出地址
  • 使用VirtualProtect修改内存页为PAGE_READWRITE
  • 注入跳转指令(jmp rel32)至自定义处理函数

动态禁用逻辑

// 禁用指定DLL加载(基于模块路径模糊匹配)
func DisableDllLoad(modulePath string) {
    // 获取当前进程的LdrpLoadDll function pointer(需RVA解析)
    addr := getLdrpLoadDllAddr()
    // 写入INT3断点或NOP滑板,配合异常处理器拦截
    syscall.VirtualProtect(addr, 16, syscall.PAGE_EXECUTE_READWRITE, &oldProtect)
    syscall.CopyMemory(addr, []byte{0xCC}, 1) // INT3 trap
}

该代码向LdrpLoadDll起始位置注入INT3软中断,触发EXCEPTION_BREAKPOINT,由注册的AddVectoredExceptionHandler捕获并判断modulePath后决定是否放行。

拦截阶段 触发时机 可控粒度
syscall前 NtMapViewOfSection返回前 进程级
DLL路径解析后 LdrpLoadDll内部字符串比较后 模块路径级
graph TD
    A[用户调用LoadLibrary] --> B[LdrLoadDll]
    B --> C{是否匹配禁用列表?}
    C -->|是| D[触发INT3 → 异常处理器]
    C -->|否| E[正常映射PE节]
    D --> F[检查调用栈/参数/签名]
    F --> G[Allow / Deny / Redirect]

2.4 Go构建链路中CGO与MSVC链接器的PE生成阻断方案

Go在Windows下启用CGO时,若混合使用GCC(MinGW)与MSVC工具链,常因目标文件格式不兼容导致PE生成失败。

阻断根源分析

MSVC链接器(link.exe)仅接受COFF对象(.obj),而默认gcc -c生成的是GNU COFF或ELF-like目标,且__declspec(dllexport)等MSVC特有符号未正确标注。

典型错误示例

# 错误:GCC生成的目标被传给MSVC link.exe
go build -ldflags="-H windowsgui" -buildmode=c-shared .
# → link: unknown object file format

解决路径对比

方案 工具链一致性 符号导出支持 PE兼容性
完全MSVC(cl + link) ✅ ✅(__declspec(dllexport)) ✅
MinGW-w64 + ld ✅ ⚠️(需-Wl,--export-all-symbols) ✅
混合调用(GCC obj + link.exe) ❌ ❌ ❌

推荐实践

强制统一工具链:

# 设置环境变量,使CGO全程走MSVC
set CC=cl
set CGO_ENABLED=1
set GOOS=windows
go build -buildmode=c-shared -ldflags="-H windowsgui" .

此配置下,go tool cgo自动调用cl.exe编译C代码,生成标准COFF .obj,由link.exe无缝链接为合法PE32+ DLL。关键参数-H windowsgui抑制控制台窗口,确保GUI子系统兼容性。

2.5 实战:定制go build -ldflags实现零PE输出二进制

Go 默认交叉编译 Windows 二进制时会生成带 PE 头的 .exe 文件,但某些嵌入式或沙箱环境要求纯静态 ELF(Linux)或 Mach-O(macOS)格式——即使目标是 Windows,也可通过剥离 PE 头+注入自定义 loader 实现“零PE”。

核心原理:LD 链接器接管

go build -ldflags="-H=windowsgui -s -w -buildmode=c-archive" -o main.exe main.go
  • -H=windowsgui:禁用控制台窗口,同时跳过标准 PE 头写入逻辑(Go linker 特殊模式)
  • -s -w:剥离符号与调试信息,减小体积并干扰 PE 解析
  • -buildmode=c-archive:生成无入口点的归档,需外部 C loader 注入——此时输出为裸二进制段,无 PE header。

关键限制与验证

工具 对零PE二进制的支持
file 显示 data 而非 PE32
objdump -h 无 .text/.rsrc 等 PE 节区
pecheck 报错“not a PE file”
graph TD
    A[Go 源码] --> B[go build -ldflags]
    B --> C{Linker 模式}
    C -->|windowsgui + c-archive| D[裸代码段+数据段]
    C -->|默认| E[标准 PE32+ Header]
    D --> F[Loader 注入后运行]

第三章:内存防护层的Go级加固实践

3.1 运行时堆栈不可执行(NX)与Go goroutine栈保护协同配置

现代操作系统通过NX(No-eXecute)位标记内存页为不可执行,防止栈上代码注入。Go运行时在创建goroutine栈时,主动配合内核启用PROT_READ | PROT_WRITE | PROT_NONE(写入前临时添加PROT_EXEC仅用于runtime·stackcopy等极少数场景),形成双重防护。

栈分配时的NX协同逻辑

// src/runtime/stack.go 中栈分配关键片段
func stackalloc(n uint32) *stack {
    // 请求不可执行内存(Linux mmap with MAP_NORESERVE | MAP_STACK)
    p := sysAlloc(uintptr(n), &memstats.stacks_inuse)
    if p == nil {
        throw("stackalloc: out of memory")
    }
    // 显式禁用执行权限(即使内核默认启用NX,仍显式加固)
    sysFault(p, uintptr(n)) // 触发mprotect(..., PROT_NONE)
    return &stack{data: p, size: uintptr(n)}
}

此处sysFault调用mprotect(p, n, PROT_NONE),确保栈页初始即不可执行;仅当goroutine启动时,runtime短暂提升为PROT_READ|PROT_WRITE,绝不开放PROT_EXEC——与传统C栈行为本质不同。

关键协同机制对比

机制 传统C线程栈 Go goroutine栈
默认执行权限 可执行(需显式禁用) 默认不可执行(NX+显式加固)
栈增长方向 向下(高→低地址) 向下,但受guard page隔离
runtime干预程度 无 全生命周期权限动态管控

防护链路流程

graph TD
    A[goroutine创建] --> B[调用stackalloc]
    B --> C[sysAlloc申请内存]
    C --> D[sysFault设PROT_NONE]
    D --> E[栈初始化完成]
    E --> F[goroutine调度执行]
    F --> G[仅允许R/W,永不设PROT_EXEC]

3.2 利用runtime/debug.SetPanicOnFault实现非法内存访问熔断

runtime/debug.SetPanicOnFault 是 Go 运行时提供的底层安全开关,用于在发生非法内存访问(如访问已释放的 C 内存、空指针解引用等)时主动触发 panic,而非静默崩溃或未定义行为。

应用场景与限制

  • 仅在 GOOS=linux 且 GOARCH=amd64/arm64 下生效
  • 必须在程序启动早期调用(init() 或 main() 开头)
  • 对纯 Go 内存操作(如越界切片)无效——它不干预 Go 的内存安全模型,仅作用于 unsafe/CGO 边界

启用方式与验证

import "runtime/debug"

func init() {
    debug.SetPanicOnFault(true) // 启用非法内存访问熔断
}

逻辑分析:该函数将运行时信号处理器注册为 SIGSEGV/SIGBUS 的 panic 触发器。参数 true 表示启用;设为 false 可禁用(但不可逆恢复默认行为)。注意:多次调用以最后一次为准,且仅影响后续发生的故障。

效果对比表

场景 默认行为 SetPanicOnFault(true)
CGO 中访问已 free() 内存 进程立即终止 触发 panic,可被捕获
Go 切片越界 panic(原生) 行为不变
空 *C.char 解引用 SIGSEGV 崩溃 转为 panic: fault
graph TD
    A[发生 SIGSEGV/SIGBUS] --> B{SetPanicOnFault enabled?}
    B -- yes --> C[触发 runtime.panicfault]
    B -- no --> D[默认信号终止进程]
    C --> E[可被 recover 捕获]

3.3 Go 1.21+ MemorySanitizer兼容性适配与ASLR强化部署

Go 1.21 起正式支持 -msan 构建标志(需搭配 Clang 工具链),但默认禁用,因其与 Go 运行时内存管理存在底层冲突。

启用 MemorySanitizer 的必要条件

  • 必须使用 CGO_ENABLED=1
  • 链接器需指定 -fuse-ld=lld 或 gold
  • 禁用 GODEBUG=asyncpreemptoff=1(避免协程抢占干扰检测)

关键编译指令示例

# 启用 MemorySanitizer 并强化 ASLR
CGO_ENABLED=1 \
CC="clang -fsanitize=memory -fno-omit-frame-pointer" \
GOOS=linux GOARCH=amd64 \
go build -ldflags="-msan -pie -z relro -z now" -o app .

此命令启用 MemorySanitizer 检测未初始化内存访问;-pie 强制位置无关可执行文件以激活完整 ASLR;-z relro -z now 实现只读重定位段,阻断 GOT 覆盖攻击。

ASLR 强化等级对比

配置项 默认值 强化后 效果
-buildmode=exe ✅ ✅ 基础 PIE 支持
-ldflags=-pie ❌ ✅ 全地址空间随机化启用
-ldflags=-z now ❌ ✅ 延迟绑定立即解析,防劫持
graph TD
    A[源码] --> B[Clang 编译含 -fsanitize=memory]
    B --> C[Go linker 注入 -msan/-pie/-z now]
    C --> D[运行时:MSan shadow memory + ASLR 基址随机化]
    D --> E[检测未初始化读 + 抵御 ROP/JOP 攻击]

第四章:系统级安全策略的Go语言落地体系

4.1 Windows Mitigation Policies通过Go调用SetProcessMitigationPolicy实战

Windows 10+ 提供的 SetProcessMitigationPolicy API 可动态启用ASLR、DEP、CFG等内核级防护策略,Go需通过syscall直接调用。

核心调用流程

  • 加载 kernel32.dll
  • 获取 SetProcessMitigationPolicy 函数地址
  • 构造对应 PROCESS_MITIGATION_POLICY 结构体

关键策略对照表

策略类型 常量值 启用效果
ProcessDynamicCodePolicy 11 禁止动态代码分配(如 VirtualAlloc + EXECUTE)
ProcessStrictHandleCheckPolicy 15 强制句柄验证,防止UAF重用
// 设置动态代码执行限制
policy := uint32(1) // ENABLED = 1
ret, _, _ := procSetProcessMitigationPolicy.Call(
    uintptr(11), // ProcessDynamicCodePolicy
    uintptr(unsafe.Pointer(&policy)),
    uintptr(unsafe.Sizeof(policy)),
)

procSetProcessMitigationPolicy.Call() 第一参数为策略ID,第二参数传入策略值指针,第三参数为值大小。返回非零表示成功。

graph TD
    A[Go程序] --> B[syscall.LoadDLL kernel32.dll]
    B --> C[procSetProcessMitigationPolicy]
    C --> D[构造policy结构体]
    D --> E[Call并校验返回值]

4.2 基于Windows API的Image File Execution Options(IFEO)自动化禁用PE注册

IFEO 是 Windows 提供的调试挂钩机制,通过 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 注册表路径可为指定 PE 文件注入调试器或禁用执行。自动化清除需绕过 UAC 限制并确保原子性。

核心操作流程

// 删除 IFEO 条目(需管理员权限)
LONG res = RegDeleteKeyExA(
    HKEY_LOCAL_MACHINE,
    "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\notepad.exe",
    0, 0
);

RegDeleteKeyExA 强制递归删除键;参数 表示默认安全访问, 后缀标志位表示不启用备份/恢复策略。失败时返回非零错误码(如 ERROR_ACCESS_DENIED)。

关键注意事项

  • 必须以 SE_BACKUP_NAME 和 SE_RESTORE_NAME 特权运行
  • 操作影响全局策略,建议先备份目标键值
  • 部分系统启用 Protected Process Light (PPL) 后,高完整性进程无法修改核心 IFEO 条目
权限要求 是否必需 说明
管理员组成员 ✓ 默认获得 HKLM 写入能力
备份/还原特权 ✓ 删除受保护子键必需
调试特权 ✗ 仅用于设置 Debugger 值
graph TD
    A[调用OpenProcessToken] --> B[启用SE_BACKUP_NAME]
    B --> C[RegDeleteKeyExA]
    C --> D{成功?}
    D -->|是| E[清理完成]
    D -->|否| F[检查ERROR_ACCESS_DENIED]

4.3 Go程序自检模块:验证自身PE头完整性与数字签名状态

PE头校验核心逻辑

Go程序可通过syscall读取自身映像基址,解析DOS头、NT头及可选头结构,比对SizeOfImage与实际内存布局一致性:

// 获取当前进程镜像基址(Windows)
h, _ := syscall.GetModuleHandle(nil)
data, _ := syscall.VirtualQuery(h)
peHeader := (*imageNtHeaders)(unsafe.Pointer(uintptr(h) + 0x3C)) // DOS header e_lfanew
if peHeader.Signature != 0x00004550 { // "PE\0\0"
    log.Fatal("Invalid PE signature")
}

该代码定位PE签名偏移并验证魔数,确保加载器未篡改头部结构。

数字签名验证路径

调用WinVerifyTrust API校验嵌入式签名有效性,需构造WINTRUST_DATA结构体并指定WINTRUST_ACTION_GENERIC_VERIFY_V2策略。

验证结果对照表

检查项 合法状态 异常表现
PE签名 0x4550 0x0000(被清零或覆盖)
校验和 非零 0(链接器未计算)
签名时间戳 有效UTC 过期或未来时间
graph TD
    A[启动自检] --> B[读取PE头]
    B --> C{签名是否存在?}
    C -->|是| D[调用WinVerifyTrust]
    C -->|否| E[标记无签名]
    D --> F[返回TRUST_E_NOSIGNATURE]

4.4 安全沙箱内嵌:利用gVisor兼容层隔离未禁用PE的遗留依赖

当容器中运行未禁用进程注入(PE)的旧版Windows二进制依赖(如.NET Framework 4.6.2+ 的托管宿主进程),直接启用seccomp-bpf或user namespaces会导致兼容性中断。gVisor 的 runsc 运行时通过 syscall 拦截与重实现,在用户态构建隔离边界,无需修改应用即可拦截 CreateRemoteThread、VirtualAllocEx 等高危API。

gVisor 对 PE 相关系统调用的映射策略

原生 Windows API gVisor 拦截行为 风险等级 替代方案
NtWriteVirtualMemory 拒绝跨进程写入,返回 STATUS_ACCESS_DENIED ⚠️高 强制使用 IPC 通信
NtCreateThreadEx 仅允许同进程内线程创建 ✅中 保留协程模型兼容性
LoadLibraryExW 限制 DLL 路径白名单(仅 /lib/compat/) 🔶低 静态链接预编译依赖
# docker-compose.yml 片段:启用 gVisor 兼容沙箱
runtime: runsc
security_opt:
  - "no-new-privileges:true"
  - "apparmor=unconfined" # gVisor 自主管控,绕过内核 LSM 冲突

此配置使 runsc 在不触发 Windows Subsystem for Linux(WSL2)内核模块的前提下,为 .NET Core 3.1+ 托管进程提供 PE 行为隔离——所有内存写入均经 sandboxed memory manager 校验,且线程生命周期由 Sentry 统一调度。

数据同步机制

gVisor 通过 Platform 抽象层将 sys_readv/sys_writev 映射为安全通道消息,避免 memcpy 跨沙箱越界;其 futex 实现强制序列化锁竞争,阻断竞态条件下的 PE 注入路径。

第五章:演进趋势与跨平台禁用范式收敛

随着 Flutter 3.0 全面支持桌面与嵌入式平台,React Native 从 Hermes 引擎升级到 TurboModules 架构,以及 .NET MAUI 正式进入生产级支持阶段,跨平台框架对“禁用交互”这一基础能力的实现逻辑正经历显著收敛。过去各平台独立定义 disabled 属性(如 Web 的 disabled、iOS 的 isUserInteractionEnabled、Android 的 setEnabled(false)),如今被统一抽象为可组合的响应式状态约束层。

状态驱动的禁用契约标准化

现代跨平台 SDK 普遍采用声明式禁用协议:组件接收 enabled: bool 属性,并通过内部状态机触发三重同步——UI 可见性降级(灰度滤镜+指针事件拦截)、无障碍语义更新(accessibilityState: { disabled: true })、底层原生控件状态透传。以 Flutter 3.22 的 ElevatedButton 为例,其 onPressed 为 null 时自动启用禁用样式,且该行为已通过 Semantics 树向 TalkBack/VoiceOver 同步不可操作状态。

原生桥接层的禁用信号归一化

下表对比主流框架在 iOS/Android/Web 三端对禁用信号的桥接策略:

框架 iOS 原生映射 Android 原生映射 Web DOM 行为
React Native userInteractionEnabled = false setEnabled(false) element.setAttribute('disabled', '')
Flutter ExcludeSemantics(child: ...) IgnorePointer(child: ...) PointerEvents.none
.NET MAUI IsEnabled = false IsEnabled = false IsEnabled = false

值得注意的是,Flutter 在 2024 Q2 发布的 flutter_platform_widgets 插件已实现禁用状态的跨平台像素级对齐:当 enabled: false 时,iOS 端按钮阴影强度降低 30%,Android 端 Ripple 波纹动画完全禁用,Web 端则强制应用 filter: grayscale(0.7) contrast(0.8) CSS。

复杂场景下的禁用链式传播

在表单验证场景中,禁用状态需沿组件树向下传播。React Native 的 Pressable 组件通过 disabled 属性触发 onHoverIn/onPress 回调抑制,而 Flutter 则利用 InheritedWidget 实现禁用上下文继承。实际项目中曾遇到某金融 App 的多步骤表单:当用户未勾选合规协议时,整个提交按钮组需禁用,但其中包含一个动态加载的 OTP 输入框。解决方案是构建 DisabledContext 继承树,使 OTP 组件能感知父级禁用状态并自动屏蔽键盘弹出与粘贴功能。

flowchart LR
A[FormRoot enabled=false] --> B[SubmitButton]
A --> C[OTPField]
C --> D{HasParentDisabled?}
D -->|true| E[ignoreFocusNode<br>disableTextInput<br>hideKeyboard]
D -->|false| F[allowNormalInteraction]

禁用状态的性能边界控制

禁用并非免费操作。Chrome DevTools 分析显示,频繁切换 Web 表单元素的 disabled 属性会触发 Layout 重排。为此,TikTok Web 版采用批量禁用模式:将 12 个输入框的 disabled 更新合并为单次 DOM 批量操作,使表单切换耗时从 42ms 降至 9ms。类似地,Flutter 的 RenderObject 层通过 redepthChildren() 优化禁用状态变更的布局计算路径。

禁用逻辑正从平台特异性补丁演变为平台无关的契约层,其收敛本质是开发者心智模型的统一。

深入 goroutine 与 channel 的世界,探索并发的无限可能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注