找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2025

积分

0

好友

287

主题
发表于 2025-12-25 19:02:06 | 查看: 30| 回复: 0

查壳与修复

使用MT管理器对目标应用进行查壳,结果显示其使用了360加固。我们可以通过在线脱壳网站(如 https://56.al/)直接进行脱壳,也可以手动寻找脱壳点进行dump。由于本文侧重于算法还原,因此选择使用在线网站完成脱壳。

脱壳后可以得到原始的dex文件。
脱壳后的dex文件

将得到的dex文件放入MT管理器中进行修复(若无MT会员,也可使用NP管理器)。修复完成后,将这些dex文件替换到原始APK中(删除原dex文件)。至此,我们已经可以将其放入jadx等反编译工具中进行分析。需要注意的是,由于应用存在onCreate方法被抽取以及残留大量stub特征的情况,若需进行深度分析,建议使用Android Studio动态调试,根据log报错定位问题点。对于大量相似的stub代码,可使用MT管理器的正则匹配功能批量替换为null(部分位置可能无法替换,需根据具体情况判断),此处不展开详述。

抓包分析

抓包配置

本次分析采用Reqable转发至BurpSuite的方式进行抓包,具体配置如下:
Reqable转发配置
Reqable代理设置

随后在BurpSuite中开启对应的端口转发监听。
BurpSuite端口监听
通过此配置即可成功抓取到应用网络请求。

抓包检测绕过

该应用本身没有强力的抓包检测,但最初未能抓取到请求,重启手机后解决。此前在分析字节系应用时也遇到过类似问题,其通常会魔改libsscronet.so库以实现检测。由于目标应用的lib目录下未发现此文件,大概率未进行魔改,但此处仍将相关绕过方法进行分享。

通常需要从内存中dump libsscronet.so进行分析。
内存dump so示意图

dump so的Frida脚本如下:

function dump_so(so_name) {
    Java.perform(function () {
        let currentApplication = Java.use('android.app.ActivityThread').currentApplication()
        let dir = currentApplication.getApplicationContext().getFilesDir().getPath()
        let libso = Process.getModuleByName(so_name)
        console.log('[name]:', libso.name)
        console.log('[base]:', libso.base)
        console.log('[size]:', ptr(libso.size))
        console.log('[path]:', libso.path)
        let file_path = dir + '/' + libso.name + '_' + libso.base + '_' + ptr(libso.size) + '.so'
        let file_handle = new File(file_path, 'wb')
        if (file_handle && file_handle != null) {
            Memory.protect(ptr(libso.base), libso.size, 'rwx')
            let libso_buffer = ptr(libso.base).readByteArray(libso.size)
            file_handle.write(libso_buffer)
            file_handle.flush()
            file_handle.close()
            console.log('[dump]:', file_path)
        }
    })
}

另一种方法是遍历内存,搜索关键函数 SSL_CTX_set_custom_verify()
搜索SSL_CTX_set_custom_verify函数

对其交叉引用进行分析:
交叉引用分析

该函数通常带有参数(图中未显示),可通过网上资料或分析汇编代码找到其回调函数。
关键回调函数

找到目标函数后,编写Frida脚本将其返回值修改为0x0即可绕过检测。

function ssl_pass2() {
    var offest = 0x301184;
    var soName = 'libsscronet.so';
    console.log("==")
    const module = Process.findModuleByName(soName)
    Interceptor.attach(module.base.add(offest), {
        onEnter: function (args) {
        },
        onLeave: function (retval) {
            retval.replace(0x0)
            console.log("返回值", retval)
            return retval
        }
    })
}

该函数通常会在两处被调用,可分别进行测试,有效的交叉引用点可能为第一个或第二个。

Java层请求参数分析 (一)

首先对抓包中获取到的两个参数进行加密还原分析。

Java层定位

jadx中搜索参数关键词,可定位到相关代码。
搜索定位关键代码
定位到的关键类
关键方法

代码逻辑主要是对请求类型进行判断。
请求类型判断逻辑

Java层核心是调用了jmd方法,该方法进一步调用了Native层的函数。
JNI调用
Native函数详情

该Native函数被隐藏,需通过分析汇编代码查找其注册函数表。
汇编查找注册表
注册函数表

继续分析,未发现明显的加密逻辑,但此处是重点。代码中加载了一些文件,具体逻辑未完全理清。我们采用主动调用并测试的方法。

let JNISecurity = Java.use("com.kanman.JNISecurity");
var str1 = "1"
var str2 = "2"
var ret1 = JNISecurity.jmd(str1, str2)
console.log("jmd加密:", ret1)

主动调用结果

得到的结果看起来像是MD5,但由于次数限制,我们使用算法自吐脚本来辅助分析。

function hook_tess() {
    Java.perform(function () {
        // 算法自吐脚本(节选,包含MD5、SHA等摘要算法hook)
        var messageDigest = Java.use('java.security.MessageDigest');
        // ... 详细的hook实现,用于打印算法调用栈和输入输出
    });
}

运行脚本后,可以发现关键调用。
算法自吐输出1
算法自吐输出2

输出显示,输入字符串与一个固定字符串进行了拼接,该字符串同样存在于so层,通过搜索或交叉引用可以定位到MD5函数内部。
So层中的固定字符串

至此逻辑对上,可以使用Python代码实现该算法。

Java层请求参数分析 (二)

分析m-request-did参数,同样通过jadx搜索定位。
搜索did参数

该参数为加密后的设备码。
设备码加密方法

定位到具体加密方法进行分析。
加密方法定位
加密逻辑

逻辑清晰,还原代码如下:

Hook代码:

let Utils = Java.use("com.kanman.allfree.ext.utils.Utils");
Utils["getDeviceId"].implementation = function () {
    console.log('getDeviceId is called');
    let ret = this.getDeviceId();
    console.log('getDeviceId ret value is ' + ret);
    return ret;
};
let InfoUtils = Java.use("com.kanman.allfree.utils.InfoUtils");
InfoUtils["getDeviceId"].implementation = function () {
    console.log('getDeviceId is called');
    let ret = this.getDeviceId();
    console.log('getDeviceId ret value is ' + ret);
    return ret;
};

Python还原算法:

from Cryptodome.Cipher import AES
import base64
from Cryptodome.Util.Padding import pad, unpad

def encryaes(data,key):
    cipher=AES.new(key.encode('utf-8'),AES.MODE_ECB)
    encrypted=cipher.encrypt(data.encode('utf-8'))
    return base64.b64encode(encrypted).decode('utf-8')

def decryaes(base_enc,key):
    enc=base64.b64decode(base_enc)
    cipher=AES.new(key.encode('utf-8'),AES.MODE_ECB)
    dec=cipher.decrypt(enc)
    return dec.decode('utf-8')

s='S5aTaQe22BdwsExgr1ftHPPGb9Sxq69Pue/WdH1HcHI='
key='xujikmlioksjoped'
inp="4efa5850dab58086"
print(decryaes(s,key))
print(encryaes(inp,key))

Java层请求参数分析 (三)

Java层定位

定位签名参数

Hook及主动调用代码:

function hook_sig1() {
    Java.perform(function () {
        let Orange = Java.use("com.yxcorp.kuaishou.addfp.android.Orange");
        let KWEGIDDFP = Java.use("com.yxcorp.kuaishou.addfp.KWEGIDDFP");
        KWEGIDDFP["doSign"].implementation = function (context, str) {
            console.log('doSign is called' + ', ' + 'context: ' + context + ', ' + 'str: ' + str);
            let ret = this.doSign(context, str);
            console.log('doSign ret value is ' + ret);
            return ret;
        };
        //获取活动的 Context
        var current_application = Java.use('android.app.ActivityThread').currentApplication();
        var context = current_application.getApplicationContext();
        // 构造 byte[] 数组
        let bArr = Java.array('byte', [0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38]);
        let i = 4;
        // 主动调用
        let result = Orange.getClock(context, bArr, i);
        console.log("
  • 参数:", "bArr", bArr, "i:", i)         console.log('
  • 返回值: ' + result);         console.log("=====")     }) } hook_sig1()
  • 主动调用结果

    继续搜索相关方法。
    搜索关键函数

    找到核心Native函数。
    Native函数

    深入分析,最终定位到so库及其代码。

    So层代码分析

    该so使用静态注册,但包含大量花指令。
    花指令示例1
    花指令示例2

    花指令主要由无用的死代码构成,影响静态分析,需要进行NOP修复。为此编写了IDAPython脚本进行自动化处理。

    import ida_bytes
    import idc
    import idaapi
    # ... 具体的花指令定位与修复代码

    运行脚本后,代码可读性大大增强。
    修复后代码1
    修复后代码2

    接下来进行静态分析,并辅以Frida脚本进行动态验证。

    // 用于辅助静态分析的Frida Hook脚本(节选)
    function hook_so_sig1() {
        var offest = 0x3F48;
        var soName = 'libsgcore.so';
        const module = Process.findModuleByName(soName)
        Interceptor.attach(module.base.add(offest), {
            onEnter: function (args) {
                console.log("开始调用参数")
                console.log(args[0])
                console.log(args[1])
                console.log(Memory.readByteArray(this.context.x1, 0x40))
            },
            onLeave: function (retval) {
                console.log("返回值", retval)
            }
        })
    }

    以下是核心Native函数的伪代码分析摘要:

    jstring __fastcall Java_com_kwai_sgcore_SGCore_getClock(JNIEnv *JNIEnv, __int64 a2, __int64 a3, void *a4)
    {
      // ... 变量声明、初始化及错误检查
      // 关键步骤:
      // 1. 将输入参数与固定字符串"ca8e86efb32e"拼接。
      inp += "ca8e86efb32e";
      // 2. 对拼接后的字符串进行MD5运算。
      MD5_state(state);
      MD5_update(state, inp, inp_len);
      // 3. 对MD5结果的前15字节求和,并根据和值对前15字节进行异或扰动。
      for(i=0; i<15; i++) sum += md5_bytes[i];
      for(i=0; i<15; i++) tmp[i] = md5_bytes[i] ^ i ^ (sum & 0xff);
      tmp[15] = sum & 0xff;
      // 4. 进行第二轮异或扰动(与一个固定key相关)。
      // 5. 调用 encryfinal_xor 函数进行最终加密。
      result = encryfinal_xor(tmp, 16, fixed_key);
      // 6. 将结果转换为十六进制字符串返回。
      return hex_string;
    }

    encryfinal_xor函数的伪代码如下:

    void __fastcall encryfinal_xor(const void *src, size_t num_16, int key)
    {
      // 分配内存,构建一个特定结构体,包含固定头部和输入的16字节数据。
      // 对结构体的前23字节求和。
      // 根据和值,对结构体数据进行异或扰动。
      for(i=1; i<23; i++) {
        output[i] = input[i] ^ i ^ (sum & 0xff);
      }
      // 返回处理后的数据。
    }

    为了验证算法,使用Unidbg进行了模拟执行。

    public class kanmanenc {
        // ... Unidbg环境初始化
        // 调用so中的关键函数 (0x22D0)
        module.callFunction(emulator, 0x22D0, srcPtr, num_16, key);
    }

    综合动态与静态分析,最终还原出sig1参数的生成Python代码:

    import hashlib
    def sig1(inp:str):
        inp += "ca8e86efb32e" # MD5盐值
        md5_str = hashlib.md5(inp.encode('utf-8')).hexdigest()
        s = list(bytes.fromhex(md5_str))
        # 第一轮异或扰动
        tmp1 = []
        sums1 = sum(s[:15]) if (sum(s[:15]) < 0xff) else (-sum(s[:15])) & 0xff
        for i in range(16):
            if i < 15:
                tmp1.append(s[i] ^ i ^ sums1)
            else:
                tmp1.append(sums1 & 0xff)
        # 第二轮异或扰动 (与固定key: 0xb0da86aa 相关)
        key1 = [0x1, 0x1, 1, 0xaa, 0x86, 0xda, 0xb0]
        tmp2 = key1 + tmp1
        sums2 = sum(tmp2[:23]) & 0xff if (sum(tmp2[:23]) < 0xff) else (-sum(tmp2[:23])) & 0xffff
        tmp2[0] = (sums2 ^ 1) & 0xff
        final=[]
        final.append(tmp2[0])
        for i in range(1, 23):
            if i != 23:
                final.append((tmp2[i] ^ i ^ sums2) & 0xff)
            else:
                final.append(sums2 & 0xff)
        final_str = "".join(f'{byte:02x}' for byte in final)
        return final_str

    登录请求分析还原

    Java层定位

    搜索x_data参数,定位到加密函数。
    登录参数定位

    So层算法分析

    跟踪发现,so层又调用了Java层的加密方法,本质是标准的AES-CBC加密,但使用了特定参数。
    So层调用Java加密
    AES加密调用链
    最终的AES加密方法

    通过Frida Hook获取关键的Key和IV。

    function hook_enc() {
        Java.perform(function () {
            // Hook AES加密相关类,打印密钥和IV
            var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
            SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (keyBytes, algorithm) {
                console.log("Algorithm: " + algorithm + ", Key: " + bytesToHex(keyBytes));
                return this.$init(keyBytes, algorithm);
            };
            var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
            IvParameterSpec.$init.overload('[B').implementation = function (ivBytes) {
                console.log("IV: " + bytesToHex(ivBytes));
                return this.$init(ivBytes);
            };
        });
    }

    Hook结果显示,Key为4548ded8c9e02690,IV为1992360ee9bc4f8f

    登录请求的加解密Python实现如下:

    import base64
    from Cryptodome.Cipher import AES
    from Cryptodome.Util.Padding import pad, unpad
    
    def login_decry(inp:str):
        key="4548ded8c9e02690"
        iv="1992360ee9bc4f8f"
        enc_bytes=base64.b64decode(inp)
        cipher=AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
        dec = cipher.decrypt(enc_bytes)
        # 去除PKCS7填充
        return unpad(dec, AES.block_size).decode('utf-8')
    
    def login_encry(inp:str):
        key="4548ded8c9e02690"
        iv="1992360ee9bc4f8f"
        cipher=AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
        # 添加PKCS7填充
        padded_inp = pad(inp.encode('utf-8'), AES.block_size)
        enc = cipher.encrypt(padded_inp)
        return base64.b64encode(enc).decode('utf-8')
    
    # 示例
    encrypted_data = '0OFm2t/mYjPIhqlECGE+Hs+...' # 截断的密文
    print(login_decry(encrypted_data))
    
    plain_json = '{"client-channel":"qihoo","service":"qmmh","mobile":"xxxxxxxxxxx"...}'
    print(login_encry(plain_json))

    服务器返回的数据是一个PNG图片的base64编码,使用CyberChef等工具即可解码查看。

    VIP功能初步分析

    此部分未进行完整分析,仅定位到关键代码位置供参考。
    VIP功能定位1
    VIP功能定位2

    总结

    通过本次对目标应用的逆向分析,实践了360加固脱壳、Frida动态调试与脚本编写、Unidbg模拟执行、IDAPython去除花指令等多种技术。后续可进一步加强对加固应用脱壳后修复运行的能力。

    免责声明: 本文所有技术内容仅用于安全研究与学习目的,严禁用于任何非法用途。如涉及侵权,请联系删除。




    上一篇:Java split方法陷阱解析:CSV数据分割时末尾空字符串丢失问题
    下一篇:基于AI自动化的工作流:2分钟快速生成高质量电商Listing文案
    您需要登录后才可以回帖 登录 | 立即注册

    手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

    GMT+8, 2026-1-10 18:32 , Processed in 0.324925 second(s), 39 queries , Gzip On.

    Powered by Discuz! X3.5

    © 2025-2025 云栈社区.

    快速回复 返回顶部 返回列表