JS&Android逆向记录
type: channel
link: https://www.xiayutian.xyz/
description: JS&Android逆向
items: 安卓逆向-HmacMd5算法
items: 安卓逆向-NDK开发基础
items: 安卓逆向-ARM汇编学习一
items: 安卓逆向-ARM可执行文件生成过程
items: 安卓逆向-ChaCha20算法
items: 安卓逆向-unidbg文件读写
items: 安卓逆向-Des算法详解
items: 安卓逆向-unidbg补系统调用(一)
items: 安卓逆向-快手去花指令
items: 安卓逆向-Anti Unidbg之JNI层检测

安卓逆向-HmacMd5算法
type: item
date: 2025-10-23T14:49:00+08:00
description: hmacmd5算法1. 简介HMAC-MD5算法是一种基于MD5散列函数和密钥的消息认证码算法，它通过结合密钥和消息内容来生成一个长度为128位的哈希值，用于验证消息的完整性和身份；与普通的 MD5 哈希算法不同，HMAC-MD5 在计算过程中使用了一个密钥，使得攻击者无法仅凭猜测数据本身来伪造消息的认证码;如果密钥太长，它需要被压缩；如果太短，它需要被填充到特定长度；总结就是：两次加盐，两次哈希；2. 算法流程输入：消息（Message）：需要进行验证的消息数据；密钥（Key）：用于生成消息认证码的密钥；输出：HMAC-MD5值：一个128位的签名值(32个十六进制位的数据)，用于验证消息的完整性和真实性；步骤详述：步骤 1：密钥准备如果密钥长度大于MD5的块大小（64字节），则先对密钥执行一次MD5哈希运算，生成一个16字节（128位）的密钥摘要（K'），然后再进行填充；如果密钥长度小于64字节，则用零字节（0x00）填充密钥至64字节；如果密钥长度正好是64字节，则直接使用密钥；步骤2：异或0x36，也叫ipad逐字节异或得到扩展密钥1；0x36的十进制是54；这里的ipad不是一个数据，应该是与key相同长度的数组；步骤3：异或0x5c，也叫opad逐字节异或得到扩展密钥2；0x54的十进制是92；步骤5：第一次加盐扩展密钥1与明文拼接；步骤6：第一次哈希加盐的结果进行哈希；步骤7：第二次加盐扩展密钥2与第一次哈希的结果拼接；步骤8：第二次哈希将上述拼接的结果进行哈希；总结：HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))K：密钥M：明文H：哈希函数K'：扩展密钥⊕：逐字节异或opad：0x5cipad：0x36|| ：拼接算法流程简述如上，以一个简单的示例来看整体的流程；3. 完整示例以sana为明文，xiayutian为key，进行整个流程的介绍；参考公式： H((K' ⊕ opad) || H((K' ⊕ ipad) || M))3.1 密钥扩展首先进行密钥扩展，xiayutian的十六进制表示为：78 69 61 79 75 74 69 61 6e长度不够512个比特位，所以需要进行填充，填充后的结果如下：78 69 61 79 75 74 69 61 6E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 一共是55个零字节，加上原有的数据就是64个字节；3.2 与0x36异或随后密钥与0x36（ipad）逐字节异或，这里的0x36不是单独的一个数据，而是64个，毕竟要一一对应；得到异或的结果，这就对应着第一个扩展密钥，也叫扩展密钥1；4E 5F 57 4F 43 42 5F 57 58 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 3.3 第一次加盐然后拼接明文，将明文放在最后，这就是第一次加盐；明文sana的十六进制：73 61 6e 61拼接后数据（68字节）: 4E 5F 57 4F 43 42 5F 57 58 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 73 61 6E 61 3.4 第一次哈希将这个结果做一个md5加密，这就是第一次哈希；fa23080448b15547fe4b2a19226cf9b73.5 与0x5c异或获取扩展密钥2，也就是密钥与opad（0x5c）逐字节异或；24 35 3D 25 29 28 35 3D 32 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C …

安卓逆向-NDK开发基础
type: item
date: 2025-10-23T14:44:47+08:00
description: NDK开发基础1. 基础1.1 指定平台编译apk时可以指定编译哪些平台的so，在app下的build.gradle里配置 ndk 即可，默认不写则是四个平台都编译；defaultConfig { applicationId "com.example.devicecheckfinger" minSdk 22 targetSdk 34 versionCode 1 versionName "1.0" ndk{ abiFilters 'armeabi-v7a', 'arm64-v8a' } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }1.2 可选平台查看自己的cpu支持哪些平台，可以使用Build下的一个api，如下：Log.d("sana abi support", Arrays.toString(Build.SUPPORTED_ABIS)); // [arm64-v8a, armeabi-v7a, armeabi]2. jni相关首先，JNI是Java Native Interface缩写，JNI提供了一套标准API，允许Java代码与本地编译语言（如C/C++）交互；Java调用本地代码：通过native关键字声明方法，由C/C++实现具体逻辑。本地代码调用Java：C/C++可通过JNI访问Java对象、调用Java方法；实际上ndk与jni是一回事，写的代码是c或者c++，而不是java的代码了；在生成的示例文件中，可以发现有些函数前会有 extern "C" 字样，他是什么意思？extern "C"是为了解决C++ 的重载特性，会出现名称粉碎这个特性，C++编译器会把void foo(int)编译为类似_foo_int的符号，而C编译器只会生成 _foo ，若 C++ 直接调用 C 函数，会因符号名不匹配导致链接错误，这么写就是为了消除这种特性；众所周知，jni函数前两个参数都是固定的，第一个为jnienv，第二个则是jclass或者jobject，第二个参数分别是如下情况：jclass对应的java方法是静态方法，有static修饰符；jobject对应的java方法则是实例方法；JVM虚拟机：JVM是Java虚拟机的简称，是Java字节码的运行环境，JVM标准实现是栈架构虚拟机，执行class文件，采用JIT即时编译，运行时效率不高；ART虚拟机：ART是Android Runtime，是Android系统上的运行环境，ART处理的是dex字节码文件格式；Android早期确实使用JVM（具体是Dalvik虚拟机）来运行应用，但Dalvik采用JIT即时编译，运行时效率不高；2014年安卓4.4开始引入ART作为实验选项，5.0后全面取代Dalvik；ART最大的改变是采用AOT预先编译技术，把字节码提前编译成本地机器码；so加载主要会执行的几个函数：首先是init、init_array、Jni_OnLoad；3. java反射资料：https://www.jianshu.com/p/9be58ee20dee https://www.runoob.com/java/java-reflection.html3.1 基本概念在Android开发中，反射（Reflection） 是Java语言的核心特性之一，它允许程序在运行时动态加载类、查看类的信息（比如类名、方法、字段、构造器等）、动态调用对象的方法、动态访问或修改对象的字段；或者说：JAVA反射机制是在运行状态中，对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意方法和属性；这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制；3.2 重要api与Java反射相关的类如下：类名用途Class类代表类的实体，在运行的Java应用程序中表示类和接口Field类代表类的成员变量（成员变量也称为类的属性）Method类代表类的方法Constructor类代表类的构造方法Class代表类的实体，在运行的Java应用程序中表示类和接口，在这个类中提供了很多有用的方法，这里对他们简单的分类介绍；获得类相关的方法：方法用途asSubclass(Class clazz)把传递的类的对象转换成代表其子类的对象Cast把对象转换成代表类或是接口的对象getClassLoader()获得类的加载器getClasses()返回一个数组，数组中包含该类中所有公共类和接口类的对象getDeclaredClasses()返回一个数组，数组中包含该类中所有类和接口类的对象forName(String className)根据类名返回类的对象getName()…

安卓逆向-ARM汇编学习一
type: item
date: 2025-10-23T14:33:00+08:00
description: ARM汇编学习一参考资料：ARM汇编语言官方手册.pdf1. ARM汇编常见指令1.1 数据处理指令Ⅰ MOV数据赋值指令；mov r0, #0将 立即数 0 加载到寄存器 r0 中，也就是给r0赋值为0；Ⅱ ADD加法指令；add r0, r0, r1将 r0 + r1的值赋值给 r0；Ⅲ SUB减法指令；sub r0, r0, r1将 r0 - r1 的值赋值给r0；Ⅳ AND与操作指令；and r0, r0, #6先给r0赋值为5，然后将r0与一个立即数按位与操作，并将结果给r0，这里也可以与另一个寄存器操作；Ⅴ ORR或操作指令；orr r0, r0, #6将r0寄存器的值与6进行或操作，将结果写入r0寄存器中；Ⅵ EOR异或操作指令；eor r0, r0, #8将r0寄存器的值与8进行异或操作，并将结果写入r0寄存器中；Ⅶ BIC数据位清0指令；bic r0, r0, #0xF0000000将 R0 的高4位清零，保留低28位；假设想要将后两位置0，则写3，也就是0011，其他的以此类推；1.2 移位指令Ⅰ LSL数据左移指令；lsl r0, r0, #2左移 n 位等价于乘以2的n次方，例如，LSL R0, R0, #2 等同于 R0 = R0 * 4；@ 快速乘法运算 MOV R1, #5 @ R1 = 5 LSL R0, R1, #2 @ R0 = 5 << 2 = 20（等价于5×4）Ⅱ LSR数据右移指令；lsr r0, r0, #1立即数范围为 0–31（32位寄存器）或 0–63（64位寄存器）,右移 n 位等价于无符号数除以2的n次方;例如，LSR R0, R1, #2 等同于 R0 = R1 / 4;@ 快速无符号除法 MOV R1, #60 @ R1 = 60（二进制111100） LSR R0, R1, #2 @ R0 = 60 >> 2 = 15（二进制001111）[5](@ref)Ⅲ ROR循环右移指令；并且这里没有循环左移的指令；ror r0, r0, #1将寄存器 R0 中的二进制数据向右移动 1 位，并将最低位移出的位填充到最高位，形成循环移位；ROR R0, R0, #1 @ R0 = R0 循环右移1位 @右移时，寄存器中的每一位向右移动指定位数，最低位移出的位依次填充到最高位 MOV R0, #0x12345678 @ R0 = 0001 0010 0011 0100 0101 0110 0111 1000 ROR R0, R0, #8 @ R0 = 0111 1000 0001 0010 0011 0100 0101 0110（即 0x78123456）ror与lsr有什么区别？@ LSR（逻辑右移）： @ 将寄存器中的二进制数据向右移动指定位数，​左侧空出的高位补0，右侧移出的低位直接丢弃 LSR R0, R1, #3 @ R1右移3位（如R1=0b1000，结果R0=0b0001） @ ROR（循环右移）: @ 将寄存器中的二进制数据向右移动指定位数，右侧移出的低位依次填充到左侧高位，形成循环移位 ROR R0, R1, #1 @ R1循环右移1位（如R1=0b1010，结果R0=0b0101） @ LSR 和 ROR 是ARM汇编中两种互补的移位指令： @ 选择 LSR：当需要无符号除法、数据对齐或清除高位时 @ 选择 ROR：当需要保留所有位（如加密、循环缓冲区）或复杂位操作时Ⅳ ASR算数右移指令；将寄存器中的二进制数据向右移动指定位数，高位填充符号位（正数补0，负数补1），低位移出并丢弃；asr r0, r1, #2将 R1 的值算术右移2位，结果存入 R0（如 R1=-8，结果 R0=-2）；@ 特性 符号保留 高位填充符号位，确保有符号数的正确性（如负数右移后仍为负数）1.3 乘法指令Ⅰ MUL乘法指令；不能与常数进行计算；该指令的语法要求所有操作数必须是寄存器，而非直接数值；mul r0, r0, r1将r0寄存器的值与r1寄存器的值进行相乘，并将结果放入r0寄存器中；@ 数据类型：操作数默认视为 无符号整数（若需有符号乘法，需使用 SMULL 指令） @ 结果范围：结果为 32 位整数，若乘积超过 32 位，高位会被丢弃 @ 目标寄存器（如 R0）和源寄存器（如 R0、R1）必须为 通用寄存器（R0-R12） @ 目标寄存器不能与第一个源寄存器相同（例如 MUL R0, R0, R0 非法） 需通过临时寄存器实现Ⅱ MLA乘加指令；mla r0, r1, r2, r3将两个寄存器的值相乘，再将乘积与第三个寄存器的值相加，最终将结果存入目标寄存器;MLA R0, R1, R2, R3 @ R0 = R1×R2 + R3 @ 目标寄存器（Rd） 和 乘数寄存器…

安卓逆向-ARM可执行文件生成过程
type: item
date: 2025-10-23T14:11:00+08:00
description: ARM可执行文件生成过程资料：https://developer.android.google.cn/ndk/guides/other_build_systems?hl=zh-cn1. 过程简述ARM可执行文件的生成过程大概是四步：预处理、编译、汇编、链接；执行完这四步后就会得到一个可执行文件，就可以在手机上运行；需要借助于clang来实现这个过程，在android ndk会有这个工具；可在如下路径找到它；F:\android_sdk\ndk\27.1.12297006\toolchains\llvm\prebuilt\windows-x86_64\bin进入此路径可以查看clang的版本；将它加入到环境变量中，即可在任意位置使用它；在进行编译之前，写一个c代码；#include <stdio.h> int main(int argc, char const *argv[]) { printf("hello sana"); return 0; }以它为例进行演示；2. 预处理-E首先看看clang的基础命令；-E Only run the preprocessor 仅运行预处理器 用法：#clang -E hello.c -o hello.i 作用：将hello.c预处理输出hello.i文件我们需要借助这个参数来进行预处理这一步，在文件目录打开终端输入以下命令；target是指定平台；clang -target armv7a-linux-androideabi27 -E sana.c -o sana.i会得到一个 .i 文件，这时候其实与平台是没关系的，它是把头文件展开之后，处理源代码中的宏定义、文件包含和条件编译指令；3. 编译-S命令如下：-S Only run preprocess and compilation steps 仅运行预处理和编译步骤clang -target armv7a-linux-androideabi27 -S sana.i -o sana.s这里会生成一个汇编文件，这里就与平台是相关的了；4. 汇编-c命令如下：-c Only run preprocess, compile, and assemble steps 仅运行预处理、编译和组装步骤 clang -target armv7a-linux-androideabi27 -c sana.s -o sana.o 他生成的其实就是ELF格式，将汇编代码转换为机器码（二进制指令），需通过链接器进一步处理；可以判断一下它的类型；这里的file命令是装了插件，可以推到手机上用file命令看，但是此时也是不能够执行的，需要执行最后一步；5. 链接-o命令：clang -target armv7a-linux-androideabi27 sana.o -o hello此时再查看文件格式：去测试一下是否是可执行的；这里可以看到是输出了我们写的那句代码，证明就没问题；另外，如需一步到位也是执行-o命令；clang -target armv7a-linux-androideabi27 sana.c -o hello

安卓逆向-ChaCha20算法
type: item
date: 2025-10-20T09:52:00+08:00
description: chacha20算法1. 概述ChaCha20是一种高效、安全且快速的流加密算法，由 Daniel J. Bernstein 在 2008 年基于 Salsa20算法改进而成，主要用于加密数据流；ChaCha20 将密钥（256 位）、随机数（Nonce，96 位）和计数器（Counter，32 位）经过一系列混合运算生成密钥流（Key Stream），再与明文进行按位异或得到密文ChaCha20 的解密过程与加密完全相同，因为异或操作是可逆的，类似的算法还有RC4；输入长度随意且没有填充，密文和明文长度一样；2. 与RC4的区别Chacha20和RC4都是流密码算法，但它们的加密原理有以下几个区别：密钥长度：Chacha20的密钥长度为256位（32字节） ，而RC4的密钥长度为1到256字节不等；S盒：Chacha20没有S盒，而RC4使用一个256字节的S盒来生成密钥流；Chacha20是基于置换的加密算法，它使用固定的置换来生成密钥流；加密过程：在Chacha20中，通过对输入的明文数据块进行置换和加密操作，生成密文输出；每次加密需要根据密钥和计数器生成一个新的置换和密钥流；而RC4是在密钥生成完毕后，直接用密钥流对输入的明文进行异或操作得到密文输出；区别主要就是在于密钥流的生成方式；3. 算法核心3.1 初始化构建一个固定的 4x4 矩阵（64字节）；这个矩阵由四部分填充：固定的128位魔数（16字节），ASCII编码（小端序）：0x61707865, 0x3320646e, 0x79622d32, 0x6b206574；这个是特征点；256位密钥 key（32字节）；96位 Nonce（12字节）；32位计数器 counter（4字节）；其中魔数常量是："expand 32-byte k"；它的初始排列可以看作下面的矩阵；[ 常量 ][ 常量 ][ 常量 ][ 常量 ] [ key0 ][ key1 ][ key2 ][ key3 ] [ key4 ][ key5 ][ key6 ][ key7 ] [counter][nonce0][nonce1][nonce2]每一次加密生成 64 字节的密钥流块；矩阵位置 (索引)内容长度 (32位字)说明与示例0-3固定常数4固定为字符串 "expand 32-byte k"的ASCII编码（小端序）：0x61707865, 0x3320646e, 0x79622d32, 0x6b2065744-11256位密钥8用户提供的256位（32字节）密钥，被划分为8个32位字12块计数器132位计数器，标识当前正在处理的数据块。通常初始值为1（为Poly1305认证码预留0值）13-1596位随机数3每次加密必须唯一的96位（12字节）随机数（Nonce）3.2 密钥流生成chacha20算法的重点其实就在于秘钥的扩展，通过ChaCha20的伪随机函数对初始状态进行20轮(或者说10轮)的混淆操作，生成512位(64字节)的密钥流；秘钥扩展的算法如下：# 进行 20 轮运算（10 次双轮，每次包括列轮和对角线轮） for _ in range(10): # 列轮 quarter_round(working_state, 0, 4, 8, 12) quarter_round(working_state, 1, 5, 9, 13) quarter_round(working_state, 2, 6, 10, 14) quarter_round(working_state, 3, 7, 11, 15) # 对角线轮 quarter_round(working_state, 0, 5, 10, 15) quarter_round(working_state, 1, 6, 11, 12) quarter_round(working_state, 2, 7, 8, 13) quarter_round(working_state, 3, 4, 9, 14) # 最终将原始状态与运算结果相加 for i in range(16): working_state[i] = (working_state[i] + state[i]) & 0xffffffff对 16×32bit 状态做 20 轮运算，也可以说是10轮，20轮 = 10次 column+diagonal；最终还需要将初始状态相加，也就是 out = in + 轮后状态；3.3 加密将16个32位字按小端序序列化为 64字节的密钥流块（key_stream_block），将当前64字节明文（plaintext_block）与密钥流块进行逐字节异或（XOR）；def chacha20_encrypt(key, nonce, counter,…

安卓逆向-unidbg文件读写
type: item
date: 2025-10-17T17:20:00+08:00
description: unidbg文件读写1. 前言本篇分析的是自写demo，主要是为了熟悉unidbg对于文件读写的处理，会涉及到：Sharedpreference 读写；Assets 读写；文件 读写；在模拟执行之前需要手动的编写好目标应用的代码；2. demo1设计首先，了解一下Sharedpreferences的基本概念：Sharedpreferences是Android平台上常用的存储方式，用来保存应用程序的各种配置信息，其本质是一个以“键-值”对的方式保存数据的xml文件，其文件保存在/data/data/selfPackage/shared_prefs目录下；在APP刚启动时，我们新建两个Sharedpreference 文件，分别填入两个键值对，name-sana和location-china，首先先看基本的业务代码：package com.unidbg.demo1; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.unidbg.demo1.databinding.ActivityMainBinding; public class MainActivity extends AppCompatActivity { static { System.loadLibrary("demo1"); } private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 步骤1：创建SharedPreferences对象 SharedPreferences sharedPreferences1= getSharedPreferences("one", Context.MODE_PRIVATE); SharedPreferences sharedPreferences2= getSharedPreferences("two", Context.MODE_PRIVATE); // 步骤2： 实例化SharedPreferences.Editor对象 SharedPreferences.Editor editor1 = sharedPreferences1.edit(); SharedPreferences.Editor editor2 = sharedPreferences2.edit(); // 步骤3：将获取过来的值放入文件 editor1.putString("name", "xiayutian"); editor2.putString("location", "china"); // 步骤4：提交 editor1.apply(); editor2.apply(); TextView tv = findViewById(R.id.sample_text); Button btn = findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String string_data = stringFromJNI(getApplicationContext()); Log.i("sanaTag", string_data); tv.setText(string_data); } }); } public native String stringFromJNI(Context applicationContext); }接下来的重点就是 stringFromJNI 这个native方法了，先不着急，看看是否有目标的文件存在了；我们的目标就是来读取它俩，stringFromJNI 的主要逻辑就是干这事，但是实现的方式选取以下两种：读取one.xml时，使用JNI去调用 java 层对SharedPreferen…

安卓逆向-Des算法详解
type: item
date: 2025-10-14T17:34:00+08:00
description: DES算法详解1. 简介DES（Data Encryption Standard，数据加密标准）是一种对称加密算法，由IBM在20世纪70年代初期研发，并在1977年被美国国家标准局（现为NIST）采纳为联邦信息处理标准；总的来说，DES加密的过程就是通过一系列置换、异或、扩展等运算，将明文分成若干个小块，然后根据主密钥生成一系列的轮密钥，利用轮密钥对每个小块进行加密，最终将加密结果重新组合成一个整体，得到密文；DES 的功能是：给定一个 64 位的明文和一个 64 位的密钥，输出一个 64 位的密文，这个密文可以用相同的密钥解密；所谓“64位的密钥”，其实里面只有54位在起作用；剩余的位可以直接丢弃，或者当作奇偶校验位；虽然 DES 一次只能加密 8 个字节，但我们只需要把明文划分成每 8 个字节一组的块，就可以实现任意长度明文的加密；如果明文长度不是 8 个字节的倍数，还得进行填充；现在流行的填充方式是 PKCS7 / PKCS5，都是很简单的思路，用于把任意长度的文本填充成 8 字节的倍数长，也能方便地恢复原文，这里不再赘述；此外，独立地对每个块加密，最后直接拼起来是不行的（这种方式称为“电子密码本”，ECB 模式；它会导致明文中重复的块，加密结果也重复，这对于图片之类的数据来说几乎是致命的）；分组相关信息：输入：64比特(8个字节、16个16进制数)；key： 64比特(8个字节、16个16进制数)；结果：64比特(8个字节、16个16进制数)；DES的key必须是8个字节；2. 流程图算法的流程图如下：关于其中一些术语的解释：IP表：明文初始置换IP-1表：明文结束置换PC-1表：Key初始置换PC-2表：K0-K16置换E表：扩展Rn-1与Kn进行异或3. 算法流程还可以参考：https://www.yuque.com/nanren-w8l2z/xgu63m/owxzxq7qn0b5o53xDES分两部分：明文的处理以及密钥的编排二进制流处理，这里以下方的例子来描述整个算法：输入：0123456789abcdef (hex) key：133457799bbcdff1(hex) 输出：85e813540f0ab405 (hex) 模式: ECB3.1 密钥编排首先，密钥的二进制表现形式为（64位）：00010011 00110100 01010111 01111001 10011011 10111100 11011111 11110001接下来，经过PC1表(置换选择表1)重新排列，这里的结果是56位的，也就是说，最开始传进来的秘钥⼀共64位，但是马上就会经过一轮置换去掉8位，只有56位会被使用；PC1表如下：PC_1 = [57, 49, 41, 33, 25, 17, 9, 1, 58, 50, 42, 34, 26, 18, 10, 2, 59, 51, 43, 35, 27, 19, 11, 3, 60, 52, 44, 36, 63, 55, 47, 39, 31, 23, 15, 7, 62, 54, 46, 38, 30, 22, 14, 6, 61, 53, 45, 37, 29, 21, 13, 5, 28, 20, 12, 4] # 长度：56它的置换规则非常简单，就是按照PC1表，将密钥的二进制的第57位，放在第一位，将49位放在第二位，以此类推，其中没有 8，16，24，32，40，48，56，64；注意，这里索引是1开始，没记错的话只有S盒替换是从0开始的；也就意味着以下是等效的 DES多个Key对应同一个加密结果：00010011 00110100 01010111 01111001 10011011 10111100 11011111 11110001 等效 0001001 0011010 0101011 0111100 1001101 1011110 1101111 1111000 0001001* 0011010* 0101011* 0111100* 1001101* 1011110* 1101111* 1111000*注：数的时候从1开始不是0, 看PC表的顺序为从左到右然后从上至下 ；置换后的结果为（56位）：1111000 0110011 0010101 0101111 0101010 1011001 1001111 0001111随后，将这56比特长的密钥分成左右两部分，这里称为Left0和Right0(只拆分一次)；Left0: 1111000 0110011 0010101 0101111 Right0: 0101010 1011001 1001111 0001111 然后进行循环左移，进行⼀共16轮运算（SHIFT数组共16位）；SHIFT = [1, 1, 2, 2,…

安卓逆向-unidbg补系统调用(一)
type: item
date: 2025-10-14T11:36:50+08:00
description: 补系统调用(一)之得物apk：得物版本：4.94.0目标方法：com.shizhuang.stone.main.SzSdk.lf目标方法实现：libszstone.so方法实现如下：public static native byte[] lf(String str, int i2, int i3);1. 基本框架基本的jni环境如下，这里不讨论；package com.kanxue2w.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import java.util.ArrayList; import java.util.List; public class dewu extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public dewu() { emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.shizhuang.duapp") .build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/kanxue2w/du/du.apk")); vm.setJni(this); // vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary("szstone", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public void call_lf() { List<Object> list = new ArrayList<>(10); // jnienv list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, "awt0bapt/data/user/0/com.shizhuang.duapp/filesbav4.94.0bavn486bcndewubc"))); list.add(1); list.add(0); Number number = module.callFunction(emulator, 0x302fd, list.toArray()); String res = (String) vm.getObject(number.intValue()).getValue(); System.out.println("call_lf-->> " + res); } @Override public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { switch (signature) { case "android/app…

安卓逆向-快手去花指令
type: item
date: 2025-10-14T10:09:00+08:00
description: 快手去花指令1. 简述学习过程的记录，仅代表个人浅显理解，有错误烦请纠正；Ⅰ 什么是花指令？花指令是由开发者设计，通过某种方式插入程序，用于阻碍反汇编器和代码分析者对程序控制流的认识的汇编代码片段；它是由开发者设计的，希望使反汇编出错，让破解者无法清楚正确的理解程序逻辑的一种技术；2. 确认是否有花一般来说，插花都会在比较重要的函数做，或者说重要的函数被插花的概率更大，所以我们在做这一步的时候可以优先查看JNI_OnLoad这种函数来看是否被插花；这算是比较明显的特征，或者看上面的导航条，出现了大段的红色，以及对应的机器码部分也是红色，这就属于未被识别为函数的特征，所以可以确认这个样本存在花指令；3. 修复JNI_OnLoad2.1 手动修复样本存在大量的花指令，所以不可能一个一个单独修复，肯定是需要借助ida脚本来做这件事，但在批量处理之前我们可以先看一下单个的处理，先看看对应的汇编代码；.text:00000000000462C4 STP X0, X1, [SP,#var_20]! .text:00000000000462C8 STP X2, X30, [SP,#0x20+var_10] .text:00000000000462CC ADR X1, dword_462EC .text:00000000000462D0 SUBS X1, X1, #0xC .text:00000000000462D4 MOV X0, X1 .text:00000000000462D8 ADDS X0, X0, #0x3C ; '<' .text:00000000000462DC STR X0, [SP,#0x20+var_8] .text:00000000000462E0 LDP X2, X9, [SP,#0x20+var_10] .text:00000000000462E4 LDP X0, X1, [SP+0x20+var_20],#0x20 .text:00000000000462E8 BR X9可以看到最后它是跳转到了X9寄存器的地址，而ida其实已经识别到这个地址了，或者说他计算出了这个地址，就是JUMPOUT位置的值，0x4631，那么这个值在哪里？可以看到，它们隔的其实其实不远，只是中间有一些数据，我们按下D可以将他们转换一下；这里其实ida已经将该跳转的地方识别为了函数，只是它没有创建，因为它处在数据里面，ida无法百分百确定它是代码段，所以选择了较为稳妥的方案，将选择权交给用户，那我们可以尝试按下P将它创建为一个函数，这里注意光标位置放置准确；可以发现成功被创建成了一个函数，按下tab即可看伪c；此时点击JNI_OnLoad函数里的地址，它也会顺利的跳到这个新创建的sub_4631C函数处；2.2 keypatch修复接下来需要去分析这些汇编代码具体都有什么含义，依次来看，以jni_onload这个函数为例，这里可以按下h将一些数据转换成十六进制，方便我们分析，先看完整的汇编代码：.text:00000000000462C4 ; jint JNI_OnLoad(JavaVM *vm, void *reserved) .text:00000000000462C4 EXPORT JNI_OnLoad .text:00000000000462C4 JNI_OnLoad ; DATA XREF: LOAD:0000000000003AD0↑o .text:00000000000462C4 .text:00000000000462C4 var_20 = -0x20 .text:00000000000462C4 var_18 = -0x18 .text:00000000000462C4 var_10 = -0x10 .text:00000000000462C4 var_8 = -8 .text:00000000000462C4 .text:00000000000462C4 E0 07 BE A9 STP X0, X1, [SP,#-0x20]! .text:00000000000462C8 E2 7B 01 A9 STP X2, X30, [SP,#0x10] .text:00000000000462CC 01 01 00 10 ADR X1, 0x462EC .text:00000000000462D0 21 30 00 F1 SUBS X1, X1, #0xC .text:00000000000462D4 E0 03 01 AA MOV X0, X1 .text:00000000000462D8 00 F0 00 B1 ADDS X0, X0, #0x3C ; '<' .text:00000000000462DC E0 0F 00 F…

安卓逆向-Anti Unidbg之JNI层检测
type: item
date: 2025-07-15T14:48:00+08:00
description: JNI层检测1. Findclass的anti新建一个项目，写一个简单的findclass程序；extern "C" JNIEXPORT jstring JNICALL Java_com_sana_antifindclass_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; jclass MainActivity = env->FindClass("com/sana/antifindclass/MainActivity2"); bool exception = env->ExceptionCheck(); if (exception) { hello.append("Exception!!"); env->ExceptionClear(); }else { hello.append("No Exception"); } return env->NewStringUTF(hello.c_str()); }build一下，在真机和unidbg分别跑起来这个方法；真机：Unidbg：可以很明显的发现，走了不同的分支；这里的原理是通过找一个不存在的类，以此来产生不同的差异；来看看产生这个差异的原因，去看unidbg如何实现fiadclass的，位置：DalvikVM64.javaPointer _FindClass = svcMemory.registerSvc(new Arm64Svc() { @Override public long handle(Emulator<?> emulator) { RegisterContext context = emulator.getContext(); Pointer env = context.getPointerArg(0); Pointer className = context.getPointerArg(1); String name = className.getString(0); boolean notFound = notFoundClassSet.contains(name); if (verbose) { if (notFound) { System.out.printf("JNIEnv->FindNoClass(%s) was called from %s%n", name, context.getLRPointer()); } else { System.out.printf("JNIEnv->FindClass(%s) was called from %s%n", name, context.getLRPointer()); } } if (notFound) { throwable = resolveClass("java/lang/NoClassDefFoundError").newObject(name); return 0; } DvmClass dvmClass = resolveClass(name); long hash = dvmClass.hashCode() & 0xffffffffL; if (log.isDebugEnabled()) { log.debug("FindClass env=" + env + ", className=" + name + ", hash=0x" + Long.toHexString(hash)); } return hash; } });有几个关键点，由于unidbg对于java层是没有感知的，所以我们看不到关于类存不存在的代码，我们发现，不管怎样，他都会执行到DvmClass dvmClass = resolveClass(name)，这就是产生差异的根本原因；当然也有其他转机，这里有一个notFoundClassSet，他是一个集合，目的就是规避掉这种检测，使用方式如下：private void stringFromJNI() { vm.addNotFoundClass("com/sana/antifindclass/MainActivity2"); DvmObject<?> obj = vm.resolveClass("com/sana/antifindclass/MainActivity").newObject(null); DvmObject<?> dvmObject = obj.callJniMethodObject(emulator, "stringFromJNI()Ljava/lang/String;"); Object value = dv…