“A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践。”
01
—
背景
某框架程序 jar 包中的 class 做了加密,直接反编译是会报错无法反编译的。
Web 启动时通过以下代码启动
...... 省略
JAVA_OPT="-Dxxxxxx.appHome=${APP_HOME} -Xmx1344M -Xms1344M -Xmn512M -XX:MaxMetaspaceSize=256M -XX:MetaspaceSize=256M -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses -XX:+CMSClassUnloadingEnabled -XX:+ParallelRefProcEnabled -XX:+CMSScavengeBeforeRemark -XX:ErrorFile=${DIR_LOG}/hs_err_pid%p.log -Xloggc:${DIR_LOG}/gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${DIR_LOG}/dump -XX:+PrintGCDetails -XX:+PrintGCDateStamps -agentpath:${DORAEMON_AGENT}";
${JAVA_HOME}/bin/java $DEBUG_OPTS ${JAVA_OPT} -cp "${APP_HOME}/lib/*" -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -Dxxxxxx.appEnv=prod -Dxxxxxx.appMode=server -Dlog.logDir=${DIR_LOG} -Dlog.appender=common -Dapollo.meta=http://apollo.xxxxxx.com:8501 -Dxxxxxx.config.dir=${APP_HOME}/config com.xxxxxx.aaa.aaaWMain > ${FILE_STDOUT_LOG} 2>${FILE_STDERR_LOG} &
从上面启动代码中,可以看到 `-agentpath` 这里设置了本地代理库。来看下 `${DORAEMON_AGENT}` 是怎么定义的
#DORAEMON_VERSION="r2.1"
## 判断 doraemon 的动态库是否存在,不存在则报错
#DORAEMON_AGENT="${APP_HOME}/dlibs/libxxxxxx_doraemon-${DORAEMON_VERSION}-linux-x64.so"
#if [ ! -e ${DORAEMON_AGENT} ]; then
# echo "ERROR: can not find the ${DORAEMON_AGENT}"
# exit 1
#fi
OS_NAME=`uname -s`
# compare version1 adn version2, return true if version1 great than version2
function version_gt() {
if [ "${OS_NAME}" == "Linux" ]; then
test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1";
else
test "$(echo "$@" | tr " " "\n" | sort -c | head -n 1)" != "$1";
fi
}
# find the latest doraemon version and apply it.
DORAEMON_AGENT="0.0"
for doraemon in ${APP_HOME}/dlibs/libxxxxxx_doraemon*
do
if version_gt $doraemon $DORAEMON_AGENT; then
DORAEMON_AGENT=$doraemon
fi
done
# 判断 doraemon 的动态库是否存在,不存在则报错
if [[ ! -e ${DORAEMON_AGENT} ]]; then
echo "[xxxxxx ] ERROR: can not find the ${DORAEMON_AGENT}"
exit 1
fi
echo "[xxxxxx ] Find doraemon agent: $DORAEMON_AGENT "
可以看到,本地代理库是在 dlibs 目录下的 libxxxxxx_doraemon_client-r2.6-linux-amd64.so。
综合以上,程序使用 libxxxxxx_doraemon_client-r2.6-linux-amd64.so 在 Web 启动前对代码进行解密,然后再运行。那么加解密操作主要在这 so 文件中,我们要如何通过这文件将原本的 class 还原出来呢?
主要思路:通过 Java 的 -agentpath 这个参数直接挂载 SO 文件
$ /usr/lib/jvm/java-20-jdk/bin/java -agentpath:~/Temp/dlibs/amd64/libxxxxxx_doraemon_client-r2.6-linux-amd64.so -cp ~/Tools/java-decompiler.jar org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true aaa-start-4.12.21.1-SNAPSHOT.jar src
INFO -> doraemon version: 2.6
ERROR -> Error: Please use internal JDK to run the application!
可以看到 SO 文件成功挂载了,但这个 SO 还验证了当前的 JDK 环境,必须要他们内部的 JDK。寄!但就是不信邪是他们二开了 JDK,尝试一下下绕过这 JDK 的检测。
使用 IDA,`SHIFT + F12` 打开字符串列表,搜索 `internal JDK`
按快捷键 `x` 搜索引用
可以看到这里使用了 jz 进行了跳转,那我们将 jz 改成 jnz 即可跳过这个报错输出了。点击 `Edit --> Patch program --> Change byte...`,将其中的 84 修改成 85
最后点击 `Edit --> Patch program --> Apply patch to input file...` 保存。
修改完 SO 后再次挂载尝试直接使用 SO:
$ /usr/lib/jvm/java-20-jdk/bin/java -agentpath:~/Temp/dlibs/amd64/libxxxxxx_doraemon_client-r2.6-linux-amd64.so -cp ~/Tools/java-decompiler.jar org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -dgs=true aaa-start-4.12.21.1-SNAPSHOT.jar src
INFO -> doraemon version: 2.6
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x0000000000000000, pid=82476, tid=82477
#
# JRE version: (20.0.2+9) (build )
# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.0.2+9-78, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# C [libxxxxxx_doraemon_client-r2.6-linux-amd64.so+0x61e5] Agent_OnLoad+0xd5
#
# Core dump will be written. Default location: Core dumps may be processed with "/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h" (or dumping to /home/hacker/Temp/dlibs/amd64/core.82476)
#
# An error report file with more information is saved as:
# /home/hacker/Temp/dlibs/amd64/hs_err_pid82476.log
#
#
[1] 82476 IOT instruction (core dumped) /usr/lib/jvm/java-20-jdk/bin/java -cp -dgs=true src
然后就报错了,看来该厂商确实推出了二开的 JVM。寄x2!
思路二:逆向获取解密算法
既然无法直接使用 SO,那么只能通过逆向找到解密算法了。
直接搜索 dec 看看有没有解密相关的方法。
可以看到主要都是 AES 解密。一个一个搜索 aes_decrypt 引用,最终找到了解密密钥
找到密钥还不够,对于 AES 还需要知道 Mode 和 Padding。进入到 Decrypt::aes_decrypt 方法,按 F5 查看伪代码
void __fastcall Decrypt::aes_decrypt(
void **this,
const unsigned __int8 *a2,
int a3,
const unsigned __int8 *a4,
unsigned __int8 *a5)
{
int (__fastcall *v9)(const unsigned __int8 *, __int64); // rax
int v10; // ebp
__int64 v11; // rbx
__int64 *v12; // rdx
unsigned __int8 *v13; // rsi
__int64 v14; // rax
__int64 v15; // rax
char *v16; // rax
void *v17; // rbx
char *v18; // r12
__int64 v19; // rax
__int64 v20; // rax
__int64 v21; // rax
__int64 v22; // rax
__int64 v23; // rax
_DWORD *exception; // rbp
__int64 v25; // rbx
char *v26; // rax
void *v27; // rbx
char *v28; // r12
__int64 v29; // rax
__int64 v30; // rax
__int64 v31; // rax
__int64 v32; // rax
__int64 v33; // rax
__int64 v34; // rbx
int v35; // [rsp+14h] [rbp-164h]
void (__fastcall *v36)(__int64 *, unsigned __int8 *, char *); // [rsp+18h] [rbp-160h]
char v37[256]; // [rsp+20h] [rbp-158h] BYREF
__int64 v38; // [rsp+120h] [rbp-58h] BYREF
__int64 v39; // [rsp+128h] [rbp-50h]
__int64 v40; // [rsp+130h] [rbp-48h] BYREF
char v41; // [rsp+138h] [rbp-40h] BYREF
char v42[3]; // [rsp+139h] [rbp-3Fh] BYREF
char v43; // [rsp+13Ch] [rbp-3Ch] BYREF
char v44[11]; // [rsp+13Dh] [rbp-3Bh] BYREF
if ( !a2 || a3 <= 0 || !a4 || !a5 )
return;
if ( a3 <= 15 )
{
memcpy(a5, a2, a3);
return;
}
v35 = a3;
if ( (a3 & 0xF) != 0 )
v35 = a3 & 0x7FFFFFF0;
v9 = (int (__fastcall *)(const unsigned __int8 *, __int64))dlsym(this[2], "AES_set_decrypt_key");
if ( !v9 )
{
v16 = dlerror();
v17 = this[2];
v18 = v16;
v19 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v20 = std::operator<<<std::char_traits<char>>(v19, "get symbol ");
v21 = std::operator<<<std::char_traits<char>>(v20, "AES_set_decrypt_key");
v22 = std::operator<<<std::char_traits<char>>(v21, " error, msg: ");
v23 = std::ostream::operator<<(v22, v17);
std::endl<char,std::char_traits<char>>(v23);
exception = __cxa_allocate_exception(0x18uLL);
exception[4] = -1000;
*(_QWORD *)exception = &`vtable for'DoraemonException + 2;
*((_QWORD *)exception + 1) = &std::string::_Rep::_S_empty_rep_storage[24];
if ( v18 )
{
std::string::string(&v40, v18, &v41);
std::string::assign(exception + 2, &v40);
v25 = v40 - 24;
if ( std::string::_Rep::_S_empty_rep_storage != (char *)(v40 - 24)
&& (int)__gnu_cxx::__exchange_and_add((volatile int *)(v25 + 16), -1) <= 0 )
{
std::string::_Rep::_M_destroy(v25, v42);
}
}
LABEL_21:
__cxa_throw(
exception,
(struct type_info *)&`typeinfo for'DoraemonException,
(void (__fastcall *)(void *))DoraemonException::~DoraemonException);
}
if ( v9(a4, 128LL) < 0 )
{
v14 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v15 = std::operator<<<std::char_traits<char>>(v14, "set decrypt key error.");
std::endl<char,std::char_traits<char>>(v15);
return;
}
v36 = (void (__fastcall *)(__int64 *, unsigned __int8 *, char *))dlsym(this[2], "AES_decrypt");
if ( !v36 )
{
v26 = dlerror();
v27 = this[2];
v28 = v26;
v29 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v30 = std::operator<<<std::char_traits<char>>(v29, "get symbol ");
v31 = std::operator<<<std::char_traits<char>>(v30, "AES_decrypt");
v32 = std::operator<<<std::char_traits<char>>(v31, " error, msg: ");
v33 = std::ostream::operator<<(v32, v27);
std::endl<char,std::char_traits<char>>(v33);
exception = __cxa_allocate_exception(0x18uLL);
exception[4] = -1000;
*(_QWORD *)exception = &`vtable for'DoraemonException + 2;
*((_QWORD *)exception + 1) = &std::string::_Rep::_S_empty_rep_storage[24];
if ( v28 )
{
std::string::string(&v40, v28, &v43);
std::string::assign(exception + 2, &v40);
v34 = v40 - 24;
if ( std::string::_Rep::_S_empty_rep_storage != (char *)(v40 - 24)
&& (int)__gnu_cxx::__exchange_and_add((volatile int *)(v34 + 16), -1) <= 0 )
{
std::string::_Rep::_M_destroy(v34, v44);
}
}
goto LABEL_21;
}
v38 = 0LL;
v39 = 0LL;
if ( v35 >> 4 > 0 )
{
v10 = 0;
v11 = 0LL;
do
{
v12 = (__int64 *)&a2[v11];
++v10;
v13 = &a5[v11];
v11 += 16LL;
v38 = *v12;
v39 = v12[1];
v36(&v38, v13, v37);
}
while ( v10 != v35 >> 4 );
}
if ( a3 - v35 > 0 )
memcpy(&a5[v35], &a2[v35], a3 - v35);
}
我们先看下面这一段
v9 = (int (__fastcall *)(const unsigned __int8 *, __int64))dlsym(this[2], "AES_set_decrypt_key");
if ( !v9 )
{
v16 = dlerror();
v17 = this[2];
v18 = v16;
v19 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v20 = std::operator<<<std::char_traits<char>>(v19, "get symbol ");
v21 = std::operator<<<std::char_traits<char>>(v20, "AES_set_decrypt_key");
v22 = std::operator<<<std::char_traits<char>>(v21, " error, msg: ");
v23 = std::ostream::operator<<(v22, v17);
std::endl<char,std::char_traits<char>>(v23);
exception = __cxa_allocate_exception(0x18uLL);
exception[4] = -1000;
*(_QWORD *)exception = &`vtable for'DoraemonException + 2;
*((_QWORD *)exception + 1) = &std::string::_Rep::_S_empty_rep_storage[24];
if ( v18 )
{
std::string::string(&v40, v18, &v41);
std::string::assign(exception + 2, &v40);
v25 = v40 - 24;
if ( std::string::_Rep::_S_empty_rep_storage != (char *)(v40 - 24)
&& (int)__gnu_cxx::__exchange_and_add((volatile int *)(v25 + 16), -1) <= 0 )
{
std::string::_Rep::_M_destroy(v25, v42);
}
}
LABEL_21:
__cxa_throw(
exception,
(struct type_info *)&`typeinfo for'DoraemonException,
(void (__fastcall *)(void *))DoraemonException::~DoraemonException);
}
if ( v9(a4, 128LL) < 0 )
{
v14 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v15 = std::operator<<<std::char_traits<char>>(v14, "set decrypt key error.");
std::endl<char,std::char_traits<char>>(v15);
return;
}
这一段其实就是调用了 OpenSSL 中内置的 AES_set_decrypt_key 方法,之后通过一系列判断解密密钥是否设置成功。
接下来我们来看下面这一段
v36 = (void (__fastcall *)(__int64 *, unsigned __int8 *, char *))dlsym(this[2], "AES_decrypt");
if ( !v36 )
{
v26 = dlerror();
v27 = this[2];
v28 = v26;
v29 = std::operator<<<std::char_traits<char>>(std::cerr, "ERROR -> ");
v30 = std::operator<<<std::char_traits<char>>(v29, "get symbol ");
v31 = std::operator<<<std::char_traits<char>>(v30, "AES_decrypt");
v32 = std::operator<<<std::char_traits<char>>(v31, " error, msg: ");
v33 = std::ostream::operator<<(v32, v27);
std::endl<char,std::char_traits<char>>(v33);
exception = __cxa_allocate_exception(0x18uLL);
exception[4] = -1000;
*(_QWORD *)exception = &`vtable for'DoraemonException + 2;
*((_QWORD *)exception + 1) = &std::string::_Rep::_S_empty_rep_storage[24];
if ( v28 )
{
std::string::string(&v40, v28, &v43);
std::string::assign(exception + 2, &v40);
v34 = v40 - 24;
if ( std::string::_Rep::_S_empty_rep_storage != (char *)(v40 - 24)
&& (int)__gnu_cxx::__exchange_and_add((volatile int *)(v34 + 16), -1) <= 0 )
{
std::string::_Rep::_M_destroy(v34, v44);
}
}
goto LABEL_21;
}
可以看到,这里和上面调用 `AES_set_decrypt_key` 的过程很类似,其中的 if 是为了判断 `AES_decrypt` 是否调用成功,说明 SO 底层其实调用的就是 OpenSSL 中的 `AES_decrypt` 来进行解密的。
然而这 `AES_decrypt` 是什么模式呢?问 GPT 问不出啥,最终在 OpenSSL 的源码中 `crypto/aes/aes_ecb.c` 找到了突破。
在 OpenSSL 底层,`AES_ecb_encrypt` 其实就是调用 `AES_decrypt` 进行解密的。所以得到 ==Mode=ECB==
[!note]
但如果现在直接写脚本对 class 解密,是会报错的:ValueError: Data must be aligned to block boundary in ECB mode。因为 ECB 加密需要密文是 16 的倍数,然后看文件的二进制并不是 16 的倍数
问问 GPT,C++ 中是怎么使用 AES_decrypt 对文件进行解密的。经过睿智的交流,成功解密代码如下:
int decryptFiles(std::string fileName) {
AES_KEY key;
if (AES_set_decrypt_key(aesKey, 128, &key) < 0) {
std::cerr << "Error setting up decryption key" << std::endl;
return -1;
}
// 打开加密文件进行读取
std::ifstream encryptedFile(fileName, std::ios::binary);
if (!encryptedFile.is_open()) {
std::cerr << "Unable to open encrypted file" << std::endl;
return -1;
}
// 读取文件内容到内存
std::vector<unsigned char> ciphertext((std::istreambuf_iterator<char>(encryptedFile)), std::istreambuf_iterator<char>());
encryptedFile.close();
// 创建输出文件
std::ofstream decryptedFile(fileName, std::ios::binary);
if (!decryptedFile.is_open()) {
std::cerr << "Unable to open output file" << std::endl;
return -1;
}
// 解密每个块
for (size_t i = 0; i < ciphertext.size(); i += AES_BLOCK_SIZE) {
unsigned char block[AES_BLOCK_SIZE];
std::copy(&ciphertext[i], &ciphertext[i + AES_BLOCK_SIZE], block); // 复制密文块到数组
AES_encrypt(block, block, &key); // 解密操作
decryptedFile.write(reinterpret_cast<char*>(block), AES_BLOCK_SIZE); // 写入解密后的数据
}
// 清理资源
decryptedFile.close();
std::cout << "[+] " << fileName << " Decryption completed successfully." << std::endl;
}
可以看到,其实解密的时候是分块进行解密的,结合 SO 中的 `Decrypt::aes_decrypt` 方法
if ( a3 <= 15 )
{
a2, a3);
return;
}
v35 = a3;
if ( (a3 & 0xF) != 0 )
v35 = a3 & 0x7FFFFFF0;
v38 = 0LL;
v39 = 0LL;
if ( v35 >> 4 > 0 )
{
v10 = 0;
v11 = 0LL;
do
{
v12 = (__int64 *)&a2[v11];
++v10;
v13 = &a5[v11];
v11 += 16LL;
v38 = *v12;
v39 = v12[1];
v13, v37);
}
while ( v10 != v35 >> 4 );
}
if ( a3 - v35 > 0 )
&a2[v35], a3 - v35);
可以看到,v35 = a3 & 0x7FFFFFF0 这里就是 a3 向下调整为 16 的倍数,这样就能 ECB 解密了。
[!note]
memcpy(&a5[v35], &a2[v35], a3 - v35); 这里对多余部分直接拼接到最后
最终通过以下 Python 脚本解密:
from Crypto.Cipher import AES as Cipher_AES
def decrypt_files(filepath):
cipher = AES.new(key="1234567890ABCDEF".encode(), mode=AES.MODE_ECB)
with open(filepath, "rb") as file:
cipher_content = file.read()
suffix_content = b''
if len(cipher_content) % 16 != 0:
suffix_content = cipher_content[len(cipher_content) & 0x7FFFFF0:]
cipher_content = cipher_content[:len(cipher_content) & 0x7FFFFF0]
# 分块解密数据
plaintext_blocks = []
for i in range(0, len(cipher_content), AES.block_size):
block = cipher_content[i:i + AES.block_size]
plaintext_block = self.decrypt(block)
plaintext_blocks.append(plaintext_block)
plaintext_padded = b''.join(plaintext_blocks)
plain_content = plaintext_padded + suffix_content
if plain_content is not None and len(plain_content) > 0:
with open(filepath, "wb") as file:
file.write(plain_content)
最终得到源码: