首页
社区
课程
招聘
[原创]某平台文件加密分析
发表于: 4天前 879

[原创]某平台文件加密分析

4天前
879

前言

本贴内容记录对文件解密常见还原方法以及思路,如有不妥请多多指教,如有侵权违规请沟通删除。

问题描述

再挂载盘时发现内部文件乱码,包括.php.shELF文件等


 还有这文件头

还原方法

1. 方法一 修改root密码

在使用时发现控制台

挂载尝试修改

其中passwd如下



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


查看目标文件是否加密


成功解除

方法二 内存取证

通过暂停虚拟机运行的方法,在虚拟机暂停时会生成一个.vmem文件,但要注意到的是它能提取的是:当时在内存中出现过、被缓存过、被映射过、被进程打开过的PHP文件内容。

在这里通过脚本可以看到,提取出来的有限,且不是很规范。




加密分析

对于最初问题描述来看,文件的加密方法可能在以下三种;

1.PHP层解密

比如PHP加载器、隐藏扩展、自动预加载之类。

2. 应用层解密

比如某个守护进程启动时,先把文件解开,再让Apache/PHP/后端去读。

3. 文件系统层透明解密(最可能)

也就是磁盘上看到的是密文,但系统运行时看到的是明文。

第一种PHP层解密

常用手段

1. PHP扩展/Zend扩展解密

通过:extension=xxx.sozend_extension=xxx.soPHP执行前拦截文件、解密源码、替换opcode

2. 商业Loader/私有Loader

比如类似:ionCubeSourceGuardian私有加载器

表现是:

磁盘上不是正常PHP

运行时却能正常执行

3. auto_prepend_file/auto_append_file注入

每次执行PHP之前,自动先跑一个脚本。

这个脚本可以做:解密include劫持动态eval注册stream wrapper

4. 脚本自身动态解码

常见特征:base64_decodegzinflategzdecodeopenssl_decrypteval拼接函数名再调用也可能先解密到临时文件再include

5.stream wrapper/autoloader劫持

比如:stream_wrapper_register()include_pathphar://data://等包装器autoload时动态加载真实内容

等等

核心判断思路

如果只有PHP文件异常,优先怀疑PHPloader/动态解码。

如果PHPELFshell 脚本、配置文件 在同一目录里都表现成:

离线看是乱码运行时正常那更像是:文件系统级透明解密

 

 

第二种 应用层解密

现象

思路是:某个程序先把密文处理成明文,然后业务程序再去读这个明文。

它和文件系统层最大的区别是:解密动作是某个用户态程序主动做的;明文通常会以某种形式“落下来”或“存在某处”

常见模式

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>

ExecStartExecStartPreEnvironment,额外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

看有没有:

Ecryptfsfuseoverlaycrypt,异常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 "$@"













传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回