记一次基于 SO 解密 Jar 包的反编译破解思路

文摘   科技   2024-05-13 10:22   广东  

“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 version2function 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 fidone
# 判断 doraemon 的动态库是否存在,不存在则报错if [[ ! -e ${DORAEMON_AGENT} ]]; then echo "[xxxxxx ] ERROR: can not find the ${DORAEMON_AGENT}" exit 1fiecho "[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 还原出来呢?


02
思路一:直接使用SO文件

主要思路:通过 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 srcINFO -> doraemon version: 2.6ERROR -> 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 srcINFO -> 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!


03

思路二:逆向获取解密算法

既然无法直接使用 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 )  {    memcpy(a5, 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]; AES_decrypt(&v38, v13, v37); } while ( v10 != v35 >> 4 ); } if ( a3 - v35 > 0 ) memcpy(&a5[v35], &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)

最终得到源码:












A9 Team
A9 Team 甲方攻防团队,成员来自某证券、微步、青藤、长亭、安全狗等公司。成员能力涉及安全运营、威胁情报、攻防对抗、渗透测试、数据安全、安全产品开发等领域,持续分享安全运营和攻防的思考和实践,期望和朋友们共同进步,守望相助,合作共赢。
 最新文章