-
-
[原创]某平台文件加密分析
-
发表于: 4天前 879
-
前言
本贴内容记录对文件解密常见还原方法以及思路,如有不妥请多多指教,如有侵权违规请沟通删除。
问题描述
再挂载盘时发现内部文件乱码,包括.php,.sh,ELF文件等

还有这文件头

还原方法
1. 方法一 修改root密码
在使用时发现控制台

挂载尝试修改
其中passwd如下


通过Shadow更改已知密码,看是否能进入bash

查看目标文件是否加密

成功解除
方法二 内存取证
通过暂停虚拟机运行的方法,在虚拟机暂停时会生成一个.vmem文件,但要注意到的是它能提取的是:当时在内存中出现过、被缓存过、被映射过、被进程打开过的PHP文件内容。
在这里通过脚本可以看到,提取出来的有限,且不是很规范。

加密分析
对于最初问题描述来看,文件的加密方法可能在以下三种;
1.PHP层解密
比如PHP加载器、隐藏扩展、自动预加载之类。
2. 应用层解密
比如某个守护进程启动时,先把文件解开,再让Apache/PHP/后端去读。
3. 文件系统层透明解密(最可能)
也就是磁盘上看到的是密文,但系统运行时看到的是明文。
第一种PHP层解密
常用手段
1. PHP扩展/Zend扩展解密
通过:extension=xxx.so,zend_extension=xxx.so在PHP执行前拦截文件、解密源码、替换opcode。
2. 商业Loader/私有Loader
比如类似:ionCube,SourceGuardian,私有加载器
表现是:
磁盘上不是正常PHP
运行时却能正常执行
3. auto_prepend_file/auto_append_file注入
每次执行PHP之前,自动先跑一个脚本。
这个脚本可以做:解密,include劫持,动态eval,注册stream wrapper
4. 脚本自身动态解码
常见特征:base64_decode,gzinflate,gzdecode,openssl_decrypt,eval拼接函数名再调用,也可能先解密到临时文件再include。
5.stream wrapper/autoloader劫持
比如:stream_wrapper_register()改include_path,phar://、data://等包装器,autoload时动态加载真实内容
等等
核心判断思路
如果只有PHP文件异常,优先怀疑PHP层loader/动态解码。
如果PHP、ELF、shell 脚本、配置文件 在同一目录里都表现成:
离线看是乱码运行时正常那更像是:文件系统级透明解密
第二种 应用层解密
一 现象
思路是:某个程序先把密文处理成明文,然后业务程序再去读这个明文。
它和文件系统层最大的区别是:解密动作是某个用户态程序主动做的;明文通常会以某种形式“落下来”或“存在某处”
常见模式
1启动时批量解密到另一个目录
比如:
/var/ceshi.enc/存密文启动脚本运行后,解到/var/ceshi/然后Apache/PHP/daemon去读/var/ceshi/
现象
离线看密文目录是乱码,运行后的目录是正常文件,能看到某个进程在启动时做了解包/解密动作
2 启动时解密到临时目录
比如解到:
/tmp/...,/dev/shm/...,/run/...然后再:include这些临时文件,execve这些临时ELF。
建软链接/绑定挂载到正式路径
现象
原始路径可能还是密文,但程序真正执行的是临时明文副本
3 按需解密
不是一次性全解,而是:访问某个PHP时现解启动某个daemon时现解用完再删
现象
平时看不到明文只有运行那个功能时才会出现
4 解密到内存,不落盘
某个守护进程把密文读出来,在内存里解密,然后:
直接exec内存对象通过匿名映射供子进程使用,或者通过IPC/socket把内容给后端
现象
磁盘上找不到明文副本但进程内存和打开文件映射异常
重点看几类东西:
1. 异常进程
ps aux
ps -ef --forest
关注自定义守护进程,很早启动的wrapper,名字伪装成系统组件的程序,会拉起 Apache/PHP/后端的父进程
2. 启动链
systemctl list-units --type=service
systemctl cat <service>
看ExecStart,ExecStartPre,Environment,额外shell脚本等等
第三种 文件系统层透明解密
它的核心是:
磁盘底层存的是密文,但系统把某个目录“挂载”成明文视图。上层程序并不知道自己在读密文,它看到的就是正常文件。
也就是说:PHP不需要自己解密,daemon不需要自己解密,Apache也不需要自己解密,因为它们打开那个路径时,内核/文件系统层已经把内容还原好了。
它以为自己在读普通文件,实际上文件系统层先解密,再把明文返回给它
常见实现方式
1 eCryptfs
最典型的“目录级透明加密”。
特点是
某个目录作为密文底层,再挂载成明文视图,未挂载时你看到的是乱码/密文块,挂载后同一路径或另一路径看到的是明文
2 dm-crypt / LUKS
这个更偏块设备层。
特点:
整个分区在底层是加密的,解锁后挂载成普通文件系统,上层看到的一切都正常,不过这种一般离线镜像层面表现和eCryptfs有点不同。
3 overlay/bind/FUSE +解密组件
也可能不是传统 eCryptfs,而是:FUSE 文件系统做透明解密,overlay 把明文层盖在密文层上,bind mount把另一个已解密目录映射过来等等
怎么看
主要不是盯“异常业务进程”,而是盯:
1. 挂载点
mount
cat /proc/mounts
Findmnt -A
看有没有:
Ecryptfs,fuse,overlay,crypt,异常bind mount
2. 目录是不是挂载覆盖点
比如:
离线镜像里/var/ceshi是密文目录,运行系统里/var/ceshi 实际被挂载成另一个视图
检查
findmnt /var/ceshi
findmnt /var/www/html
3. 谁触发了挂载
如果是透明解密,往往会有某个启动动作:
shell 脚本,systemd service,守护进程,伪装成系统程序的二进制
也就是说:“第三种”底层是挂载机制,但“触发第三种的人”仍然可能是第二种里提到的异常进程/异常启动链。
综上所述猜测是第三种,mount一下。

很明显eCryptfs挂载
接下来寻找设在开机触发这个`eCryptfs`挂载
通过寻找发现一个很可疑的程序:/usr/sbin/ModemManager
通过string搜索mount发现如下内容,不太符合名字,正常来说是管modem的,不该负责挂载`eCryptfs`

进入main函数查看

发现自定义base64字典解码
size_t sub_177D()
{
char haystack[20488]; // [rsp+10h] [rbp-5010h] BYREF
unsigned __int64 v2; // [rsp+5018h] [rbp-8h]
v2 = __readfsqword(0x28u);
s_ = 0;
memset(&s_, 0, 0x5000u);
sub_158A(
bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V,// "bW91bnRhMXRhZWNyeXC0ZnL省略=="
&s_);
sub_E5A(&s_, haystack);
s_ = 0;
memset(&s_, 0, 0x5000u);
sub_158A(
aBw91bnrhmxrhzw_0, // "bW91bnRhMXRhZWNyeXC0ZnL省略=="
&s_);
sub_E5A(&s_, haystack);
s_ = 0;
memset(&s_, 0, 0x5000u);
sub_158A(
aBw91bnrhmxrhzw_1, // "bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9saWLhM3省略=="
&s_);
sub_E5A(&s_, haystack);
s_ = 0;
memset(&s_, 0, 0x5000u);
sub_158A(
aBw91bnrhmxrhzw_2, // "bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf91cBDvdmGyM3省略=="
&s_);
sub_E5A(&s_, haystack);
while ( 1 )
{
haystack[0] = 0;
sub_E5A("service mysql status", haystack);
if ( strstr(haystack, "running") )
break;
sleep(1u);
}
s_ = 0;
memset(&s_, 0, 0x5000u);
sub_158A(
aM3zgcf9veeqyzw, // "M3Zgcf9veEQyZWHvb3g0cmVgcnVuMnNo"
&s_);
sub_E5A(&s_, haystack);
return strlen(haystack);
}
__int64 __fastcall sub_158A(char *bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V, char *s)
{
int v2; // eax
int v3; // eax
int v4; // ecx
int v5; // eax
int v7; // [rsp+18h] [rbp-28h]
int v8; // [rsp+1Ch] [rbp-24h]
char v9; // [rsp+20h] [rbp-20h]
int v10; // [rsp+24h] [rbp-1Ch]
int v11; // [rsp+28h] [rbp-18h]
v7 = 0;
v8 = 0;
while ( bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7] )
{
v9 = sub_1A7B(
::s,
(unsigned int)bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7]);
v10 = sub_1A7B(
::s,
(unsigned int)bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7 + 1]);
v2 = v8++;
s[v2] = (v10 >> 4) & 3 | (4 * v9);
if ( bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7 + 2] != 61 )
{
v11 = sub_1A7B(
::s,
(unsigned int)bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7 + 2]);
v3 = v8++;
s[v3] = (v11 >> 2) & 0xF | (16 * v10);
if ( bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7 + 3] != 61 )
{
v4 = sub_1A7B(
::s,
(unsigned int)bW91bnRhMXRhZWNyeXC0ZnLhM3Zgcf9veEQyZWHhM3Zgcf9veEQyZWHhMW8ha2V[v7 + 3]) & 0x3F
| (v11 << 6);
v5 = v8++;
s[v5] = v4;
}
}
v7 += 4;
}
s[v8] = 0;
return 0;
}解码之后得到
mount -t ecryptfs /var/ceshi /var/ceshi -o key=passphrase:passphrase_passwd=passphrase_passwadecryptfs_cipher,ecryptfs_cipher=aes,ecryptfs_key_bytes=16,ecryptfs_passthrough=n,no_sig_cache,ecryptfs_enable_filename_crypto=n
mount -t ecryptfs /var/www /var/www -o key=passphrase:passphrase_passwd=passphrase_passwadecryptfs_cipher,ecryptfs_cipher=aes,ecryptfs_key_bytes=16,ecryptfs_passthrough=n,no_sig_cache,ecryptfs_enable_filename_crypto=n
那么此处就是挂载点所在,那么接下来就是找到key,再前面mount时可以看见key
也可以再跟一下,这里就不展示了。
那接下用codex写个挂载脚本,最后成功获得明文代码
#!/usr/bin/env bash
set -euo pipefail
ROOTFS="${ROOTFS:-/run/media/kali/ceshi}"
PASS="${PASS:-passphrase_passwadecryptfs_cipher}"
CIPHER="${CIPHER:-aes}"
KEY_BYTES="${KEY_BYTES:-16}"
MOUNT_BASE="${MOUNT_BASE:-/mnt/offline_plain}"
EXPORT_BASE="${EXPORT_BASE:-}"
usage() {
cat <<'EOF'
Usage:
sudo ./ceshi_ecryptfs_offline_mount.sh mount [name...]
sudo ./ceshi_ecryptfs_offline_mount.sh export /abs/export/dir [name...]
sudo ./ceshi_ecryptfs_offline_mount.sh umount [name...]
sudo ./ceshi_ecryptfs_offline_mount.sh cleanup
Names:
ceshi www lic up
Defaults:
ROOTFS=/run/media/kali/ceshi
PASS=passphrase_passwadecryptfs_cipher
CIPHER=aes
KEY_BYTES=16
MOUNT_BASE=/mnt/offline_plain
EOF
}
resolve_names() {
if [[ $# -gt 0 ]]; then
printf '%s\n' "$@"
else
printf '%s\n' ceshi www lic up
fi
}
source_path() {
case "$1" in
ceshi) printf '/var/ceshi\n' ;;
www) printf '/var/www\n' ;;
lic) printf '/var/lic\n' ;;
up) printf '/var/up\n' ;;
*)
echo "unknown name: $1" >&2
exit 1
;;
esac
}
target_path() {
printf '%s/%s\n' "$MOUNT_BASE" "$1"
}
bind_runtime_fs() {
mkdir -p "$ROOTFS/proc" "$ROOTFS/sys" "$ROOTFS/dev"
mountpoint -q "$ROOTFS/proc" || mount --bind /proc "$ROOTFS/proc"
mountpoint -q "$ROOTFS/sys" || mount --bind /sys "$ROOTFS/sys"
mountpoint -q "$ROOTFS/dev" || mount --bind /dev "$ROOTFS/dev"
}
cleanup_runtime_fs() {
mountpoint -q "$ROOTFS/dev" && umount "$ROOTFS/dev" || true
mountpoint -q "$ROOTFS/sys" && umount "$ROOTFS/sys" || true
mountpoint -q "$ROOTFS/proc" && umount "$ROOTFS/proc" || true
}
mount_one() {
local name="$1"
local src tgt opt
src="$(source_path "$name")"
tgt="$(target_path "$name")"
mkdir -p "$ROOTFS$tgt"
if mountpoint -q "$ROOTFS$tgt"; then
echo "[*] already mounted: $ROOTFS$tgt"
return
fi
opt="key=passphrase:passphrase_passwd=$PASS,ecryptfs_cipher=$CIPHER,ecryptfs_key_bytes=$KEY_BYTES,ecryptfs_passthrough=n,no_sig_cache,ecryptfs_enable_filename_crypto=n"
chroot "$ROOTFS" /sbin/mount.ecryptfs "$src" "$tgt" -o "$opt"
echo "[+] mounted $src -> $ROOTFS$tgt"
}
export_one() {
local name="$1"
local src out
src="$ROOTFS$(target_path "$name")"
out="$EXPORT_BASE/$name"
[[ -n "$EXPORT_BASE" ]] || {
echo "EXPORT_BASE is empty" >&2
exit 1
}
mountpoint -q "$src" || mount_one "$name"
mkdir -p "$out"
cp -a "$src/." "$out/"
echo "[+] exported $src -> $out"
}
umount_one() {
local name="$1"
local tgt
tgt="$ROOTFS$(target_path "$name")"
if mountpoint -q "$tgt"; then
umount "$tgt"
echo "[+] unmounted $tgt"
else
echo "[*] not mounted: $tgt"
fi
}
main() {
local cmd
cmd="${1:-}"
[[ -n "$cmd" ]] || {
usage
exit 1
}
shift || true
case "$cmd" in
mount)
modprobe ecryptfs
bind_runtime_fs
while IFS= read -r name; do
mount_one "$name"
done < <(resolve_names "$@")
;;
export)
[[ $# -ge 1 ]] || {
usage
exit 1
}
EXPORT_BASE="$1"
shift
modprobe ecryptfs
bind_runtime_fs
while IFS= read -r name; do
export_one "$name"
done < <(resolve_names "$@")
;;
umount)
while IFS= read -r name; do
umount_one "$name"
done < <(resolve_names "$@")
;;
cleanup)
cleanup_runtime_fs
;;
*)
usage
exit 1
;;
esac
}
main "$@"