逆向工程 --- lab3
https://github.com/lucideus-repo/cybergym/tree/master/CyberGym%202/mobile/lab3
我们来逐行分析这些 Android 应用程序的 Java 代码。
这些代码片段属于一个名为 com.moksh.lab3 的 Android 应用,它似乎是一个旨在通过一些挑战来找到“flag”的 CTF (Capture The Flag) 类型应用。
文件 1:
这是应用的主要活动,是用户交互的界面。
生成的 java
package com.moksh.lab3; // 定义包名
import android.support.v7.app.AppCompatActivity; // 导入 AppCompatActivity 类,用于向后兼容的 Activity
import android.os.Bundle; // 导入 Bundle 类,用于在 Activity 状态之间传递数据
import android.view.View; // 导入 View 类,UI 组件的基类
import android.widget.Button; // 导入 Button 组件
import android.widget.TextView; // 导入 TextView 组件
import android.widget.Toast; // 导入 Toast 类,用于显示简短的提示信息
import org.json.JSONObject; // 导入 JSONObject 类,用于处理 JSON 对象
public class MainActivity extends AppCompatActivity { // 定义主活动类,继承自 AppCompatActivity
TextView txt1; // 声明一个 TextView 变量
Button bt1; // 声明一个 Button 变量
JSONObject jso = new JSONObject(); // 创建一个新的 JSONObject 实例
@Override
protected void onCreate(Bundle savedInstanceState) { // Activity 创建时调用的方法
super.onCreate(savedInstanceState); // 调用父类的 onCreate 方法
setContentView(R.layout.activity_main); // 设置当前活动的布局文件
bt1 = findViewById(R.id.button); // 通过 ID 初始化按钮
txt1 = findViewById(R.id.txtValue); // 通过 ID 初始化 TextView
int d1 = b.checkAppSignature(this); // 调用 b 类中的 checkAppSignature 方法检查应用签名
if(d1 < 1){ // 如果签名检查失败(返回值小于1)
Toast.makeText(this,"Application Tampered",Toast.LENGTH_LONG).show(); // 显示一个提示信息,内容为 "Application Tampered" (应用被篡改)
this.finishAffinity(); // 关闭所有相关的 Activity,退出应用
}
try{
jso.put("algo","SHA256"); // 向 JSON 对象中添加一个键值对 "algo": "SHA256"
jso.put("challenge","lab3"); // 向 JSON 对象中添加一个键值对 "challenge": "lab3"
}catch(Exception e){ // 捕获可能发生的异常
e.printStackTrace(); // 打印异常的堆栈跟踪信息
}
bt1.setOnClickListener(new View.OnClickListener() { // 为按钮设置一个点击事件监听器
@Override
public void onClick(View v) { // 当按钮被点击时执行
//Toast.makeText(MainActivity.this,jso.toString(),Toast.LENGTH_SHORT).show(); // 这行被注释掉了,原本用于显示 JSON 对象的内容
try{
// 调用 t 类实例的 z() 方法,获取一个经过 Base64 编码的哈希值,并替换掉所有的换行符
jso.put("flag",new t().z().replaceAll("\\n",""));
//Toast.makeText(MainActivity.this,jso.toString(),Toast.LENGTH_SHORT).show(); // 这行也被注释掉了,原本用于显示更新后的 JSON 内容
txt1.setText("Request sent: "+jso.toString()); // 在 TextView 中显示 "Request sent: " 和 JSON 对象的字符串形式
}catch(Exception e){ // 捕获可能发生的异常
e.printStackTrace(); // 打印异常的堆栈跟踪信息
}
}
});
}
}
文件 2:
这个类的主要功能是验证应用的数字签名,以防止应用被篡改。
生成的 java
package com.moksh.lab3; // 定义包名
import android.content.Context; // 导入 Context 类,提供关于应用环境的全局信息
import android.content.pm.PackageInfo; // 导入 PackageInfo 类,包含包的整体信息
import android.content.pm.PackageManager; // 导入 PackageManager 类,用于检索与当前设备上安装的应用包相关的信息
import android.content.pm.Signature; // 导入 Signature 类,表示一个应用的签名
import android.util.Base64; // 导入 Base64 类,用于 Base64 编码
import android.util.Log; // 导入 Log 类,用于日志记录
import android.widget.Toast; // 导入 Toast 类
import java.security.MessageDigest; // 导入 MessageDigest 类,用于计算消息摘要(哈希)
public class b { // 定义 b 类
public static final String p0 = "<Add signature here>"; // 定义一个常量字符串,应该用于存放正确的应用签名哈希值,但目前是占位符
public static int checkAppSignature(Context context){ // 定义一个静态方法,用于检查应用签名
try{
// 获取当前应用的包信息,并指定获取签名信息
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES);
for (Signature signature : packageInfo.signatures){ // 遍历所有的签名
byte[] signatureBytes = signature.toByteArray(); // 将签名对象转换为字节数组
MessageDigest md = MessageDigest.getInstance("SHA"); // 获取一个 SHA 哈希算法的实例
md.update(signature.toByteArray()); // 使用签名的字节来更新摘要
final String currentSignature = Base64.encodeToString(md.digest(), Base64.DEFAULT); // 计算哈希值并进行 Base64 编码
//Log.e("Sherlock","Sig: "+currentSignature); // 这行被注释掉了,原本用于在日志中打印当前应用的签名哈希
/*if(p0.trim().equalsIgnoreCase(currentSignature.trim())){ // 这部分代码被注释掉了
return 1; // 如果预设的签名与当前签名匹配,返回 1
}else{
return 0; // 否则返回 0
}*/
// return true for now
return 1; // 无论签名是否匹配,目前都直接返回 1,意味着签名检查实际上被绕过了
}
}catch(Exception e){ // 捕获可能发生的异常
return 0; // 如果发生异常,返回 0
}
return 0; // 如果没有签名信息,返回 0
}
}
文件 3:
这个类包含了核心的逻辑,通过调用本地 C/C++ 代码 (.so 文件) 来获取关键数据,然后进行处理以生成最终的 "flag"。
生成的 java
package com.moksh.lab3; // 定义包名
import android.util.Base64; // 导入 Base64 类
import android.util.Log; // 导入 Log 类
import android.widget.Toast; // 导入 Toast 类
import java.security.MessageDigest; // 导入 MessageDigest 类
public class t { // 定义 t 类
static { // 静态代码块,在类加载时执行
System.loadLibrary("local"); // 加载名为 "local" 的本地共享库 (liblocal.so)
}
private String y(byte[] t1, byte[] t2){ // 定义一个私有方法 y
// t1 is false, t2 will contain the flag value (注释:t1 是假的,t2 将包含 flag 值)
//Log.e("Keyy: ",new String(t2)); // 这行被注释掉了,原本用于打印 t2 的内容(即真正的 flag)
try{
MessageDigest co = null; // 声明一个 MessageDigest 变量
co = MessageDigest.getInstance("SHA-256"); // 获取一个 SHA-256 哈希算法的实例
t1 = co.digest(t2); // 对 t2 (真正的 flag) 进行 SHA-256 哈希计算,结果存入 t1
return Base64.encodeToString(t1,0); // 将哈希结果进行 Base64 编码并返回
}catch(Exception e){ // 捕获可能发生的异常
e.printStackTrace(); // 打印异常堆栈信息
return "123"; // 发生异常时返回 "123"
}
}
public String z(){ // 定义一个公共方法 z
String result = new String(); // 初始化一个空字符串 result
char[] charArray = k1().toCharArray(); // 调用本地方法 k1() 获取一个字符串,并转换为字符数组
for(int i = 0; i < charArray.length; i=i+2) { // 每两个字符遍历一次
String st = ""+charArray[i]+""+charArray[i+1]; // 将两个字符拼接成一个字符串
char ch = (char)Integer.parseInt(st, 16); // 将这个两位十六进制字符串转换为一个字符
result = result + ch; // 将转换后的字符拼接到 result 字符串
}
String result2 = new String(); // 初始化一个空字符串 result2
char[] charArray2 = k2().toCharArray(); // 调用本地方法 k2() 获取另一个字符串,并转换为字符数组
for(int i = 0; i < charArray2.length; i=i+2) { // 同样进行十六进制到字符的转换
String st = ""+charArray2[i]+""+charArray2[i+1];
char ch2 = (char)Integer.parseInt(st, 16);
result2 = result2 + ch2;
}
result = "cygym3{"+result+"}"; // 将解码后的 result 字符串用 "cygym3{}" 包裹起来,这似乎是一个假的 flag
result2= "cygym3{"+result2+"}"; // 将解码后的 result2 字符串用 "cygym3{}" 包裹起来,这应该是真正的 flag
try{
// 调用 y 方法,用 result2 (真 flag) 的字节数组作为哈希内容,result (假 flag) 的字节数组被忽略
return y(result.getBytes("UTF-8"),result2.getBytes("UTF-8"));
}catch(Exception e){ // 捕获可能发生的异常
e.printStackTrace(); // 打印异常堆栈信息
return "123"; // 发生异常时返回 "123"
}
}
public native String k1(); // 声明一个本地方法 k1(),返回一个字符串 (注释表明是假 flag)
public native String k2(); // 声明一个本地方法 k2(),返回一个字符串 (注释表明是真 key)
}
总结:
-
启动流程: 应用启动时,MainActivity 会首先检查应用的签名。但目前这个检查被硬编码为总是通过。
-
JSON 准备: MainActivity 初始化一个 JSON 对象,并放入一些基本信息。
-
点击事件: 当用户点击按钮时,应用会调用 t 类的 z 方法。
-
本地代码调用: z 方法会调用两个本地 C/C++ 函数 k1() 和 k2(),这两个函数(定义在 liblocal.so 文件中)会返回两个十六进制编码的字符串。
-
解码: z 方法将这两个十六进制字符串解码成常规字符串。
-
Flag 构造: 解码后的字符串分别被 cygym3{} 包裹,形成两个 flag。根据注释,k2() 返回的是真正的 key。
-
哈希计算: z 方法将真正的 flag (result2) 进行 SHA-256 哈希计算,然后进行 Base64 编码。
-
显示结果: MainActivity 将这个最终的 Base64 编码的哈希值作为 "flag" 字段的值添加到 JSON 对象中,并显示在屏幕上。
要找到真正的 flag,需要逆向分析 liblocal.so 文件以找出 k1() 和 k2() 函数返回的内容,或者在运行时动态调试 t.java,在 y 方法被调用前,检查 result2 变量的值。
栈内存布局 (从高地址到低地址)
═══════════════════════════════════════════════════════════════
高地址
┌─────────────────────────────────────────────────────────────┐
│ 调用者栈帧 │
├─────────────────────────────────────────────────────────────┤
│ EBP+0x8: param_1 (JNIEnv* 指针) │ ← 函数参数
│ EBP+0x4: 返回地址 │
│ EBP+0x0: 保存的旧EBP │ ← EBP指向这里
├─────────────────────────────────────────────────────────────┤
│ EBP-0x4: 保存的EBX │
│ EBP-0x8: 保存的EDI │
│ EBP-0xC: 保存的ESI │ ← LEA ESP,[EBP-0xC] 恢复点
├─────────────────────────────────────────────────────────────┤
│ 16字节对齐填充 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ESI指向的栈空间 (0x30字节) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ESI+0x28: Stack Canary (金丝雀值) │ │ ← local_18
│ │ ESI+0x24: 未使用 │ │
│ │ ESI+0x20: 未使用 │ │
│ │ ESI+0x1C: 未使用 │ │
│ │ ESI+0x18: 未使用 │ │
│ │ ESI+0x14: 未使用 │ │
│ │ ESI+0x11: 构建的23字节字符串开始 │ │ ← 字符串缓冲区
│ │ ESI+0x10: │ │ │
│ │ ESI+0x0F: │ │ │
│ │ ESI+0x0E: │ │ │
│ │ ESI+0x0D: │ │ │
│ │ ESI+0x0C: local_34 (新分配缓冲区地址) │ │
│ │ ESI+0x08: local_38 (字符串指针) │ │
│ │ ESI+0x04: 未使用 │ │
│ │ ESI+0x00: 未使用 │ │ ← ESI指向这里
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ │
│ 动态分配的缓冲区 (0x30字节) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 处理后的字符串存储区域 │ │ ← EDX指向这里
│ │ (每个字符转换为2字节) │ │ (local_34保存)
│ │ ... │ │
│ │ ... │ │
│ │ 最后添加NULL终止符 │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 函数调用栈空间 │
│ (为snprintf和JNI调用分配) │
├─────────────────────────────────────────────────────────────┤
│ ESP │ ← 当前栈顶
└─────────────────────────────────────────────────────────────┘
低地址
数据段内存布局
═══════════════════════════════════════════════════════════════
全局数据段 (EBX指向基地址)
┌─────────────────────────────────────────────────────────────┐
│ 跳转表 (Switch语句) │
│ EBX + 0xffffe79c: Case 0 地址 │
│ EBX + 0xffffe7a0: Case 1 地址 │
│ EBX + 0xffffe7a4: Case 2 地址 │
│ ... │
│ EBX + 0xffffe7bc: Case 9 地址 │
├─────────────────────────────────────────────────────────────┤
│ 字符串常量区域 │
│ s_ghYsLZomn1_z5: "ghYsLZomn1_z5..." (23字节) │
│ s_2aLbOr4VJQYdJ: "2aLbOr4VJQYdJ..." (23字节) │
│ s_6TlskU63WW72e: "6TlskU63WW72e..." (23字节) │
│ s_4T3fO0ea6x0th: "4T3fO0ea6x0th..." (23字节) │
│ s_dSVd_bMfE787C: "dSVd_bMfE787C..." (23字节) │
│ s_7BLL3aqav6SsY: "7BLL3aqav6SsY..." (23字节) │
│ s_fQfLz1d4NBgBe: "fQfLz1d4NBgBe..." (23字节) │
│ s_LaIe1GZiFShXt: "LaIe1GZiFShXt..." (23字节) │
│ s_8IViRf2Lrr10a: "8IViRf2Lrr10a..." (23字节) │
│ s_VRXFWu34uFIs4: "VRXFWu34uFIs4..." (23字节) │
└─────────────────────────────────────────────────────────────┘
寄存器使用情况
═══════════════════════════════════════════════════════════════
EBP: 栈帧基址指针
ESP: 栈顶指针 (动态变化)
ESI: 指向主要工作缓冲区 (0x30字节)
EBX: 指向全局数据段基地址 (PIC)
EDI: 临时使用 (跳转地址、计数器等)
EAX: 函数返回值、临时计算
ECX: Switch计数器 (0-9循环)、字符串指针
EDX: 主循环计数器 (0-22)、缓冲区地址
内存操作流程
═══════════════════════════════════════════════════════════════
1. 栈帧设置 + 16字节对齐
2. 分配0x30字节工作空间 (ESI指向)
3. 设置栈保护金丝雀
4. 构建23字节字符串 (通过Switch从10个源字符串选择)
5. 字符串反转
6. 再分配0x30字节处理缓冲区 (EDX指向)
7. 字符处理 (每个字符调用格式化函数)
8. JNI调用创建Java字符串
9. 栈保护检查
10. 栈帧清理和返回
; *************************************************************
; * 函数信息 *
; *************************************************************
;
; undefined Java_com_moksh_lab3_t_k2(undefined4 param_1)
;
; 栈帧布局:
; EBP+0x8: param_1 (从Java层传入的第一个参数, 通常是 JNIEnv* 指针)
; EBP-0x18: local_18 (用于存储栈保护金丝雀值)
; EBP-0x2e: local_2e (作为字符串处理时的指针)
; ... 其他局部变量
;
; 函数入口点: Java_com_moksh_lab3_t_k2
;------------------------------------------------------------------------------------
; 函数序言: 设置栈帧
000105a0 55 PUSH EBP ; 保存旧的栈基址指针 (EBP)
000105a1 89 e5 MOV EBP, ESP ; 设置新的栈基址, EBP 指向当前栈顶
000105a3 53 PUSH EBX ; 保存 EBX 寄存器 (被调用者保存)
000105a4 57 PUSH EDI ; 保存 EDI 寄存器 (被调用者保存)
000105a5 56 PUSH ESI ; 保存 ESI 寄存器 (被调用者保存)
000105a6 83 e4 f0 AND ESP, 0xfffffff0 ; 将栈指针 (ESP) 按16字节对齐
000105a9 83 ec 30 SUB ESP, 0x30 ; 在栈上分配 0x30 (48) 字节的局部变量空间
000105ac 89 e6 MOV ESI, ESP ; ESI 指向新分配的栈空间基地址
; 位置无关代码 (PIC) 设置: 获取数据段的基地址
000105ae e8 00 00 CALL LAB_000105b3 ; 调用下一条指令, 将返回地址 (0x000105b3) 压栈
00 00
LAB_000105b3: ; 标签
000105b3 5b POP EBX ; 将返回地址 (0x000105b3) 弹出到 EBX
000105b4 81 c3 31 ADD EBX, 0x1a31 ; EBX 加上一个偏移量, 使其指向全局数据段的基地址
1a 00 00
; 栈保护: 设置金丝雀 (Stack Canary)
000105ba 65 a1 14 MOV EAX, GS:[0x14] ; 从线程本地存储 (TLS) 中读取金丝雀值到 EAX
00 00 00
000105c0 89 46 28 MOV dword ptr [ESI + 0x28], EAX ; 将金丝雀值存入栈中 (local_18)
; 初始化循环计数器
000105c3 31 d2 XOR EDX, EDX ; 清零 EDX (用作主循环计数器, 0-22)
000105c5 31 c9 XOR ECX, ECX ; 清零 ECX (用作 switch-case 计数器, 0-9)
000105c7 90 NOP ; 空操作, 用于对齐或调试
; -- 开始构建一个23字节的字符串 --
LAB_000105d0: ; 主循环开始
000105d0 83 f9 0a CMP ECX, 0xa ; 比较 ECX 是否等于 10
000105d3 b8 00 00 MOV EAX, 0x0 ; 将 0 放入 EAX
00 00
000105d8 0f 44 c8 CMOVZ ECX, EAX ; 如果 ECX 等于 10 (Zero Flag=1), 则将 ECX 重置为 0. 实现 0-9 循环
000105db 83 f9 09 CMP ECX, 0x9 ; 检查 ECX 是否在 0-9 的范围内
000105de 77 65 JA LAB_00010645 ; 如果大于 9, 则跳转 (实际上在前一步已处理, 此为防御性代码)
; Switch 语句的实现
000105e0 8b bc 8b MOV EDI, dword ptr [EBX + ECX*4 + 0xffffe79c] ; 根据 ECX 的值 (0-9) 从跳转表中查找地址
9c e7 ff
ff
000105e7 01 df ADD EDI, EBX ; 将相对地址转换为绝对地址
000105e9 ff e7 JMP EDI ; 跳转到对应的 case 块
; -- Switch 的 Case 分支 --
; 每个 case 从一个不同的字符串中, 根据主循环计数器 EDX 的值取出一个字符
switchD_000105e9::caseD_0:
000105eb 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_ghYsLZomn1_z5] ; Case 0: 从字符串 "ghYsLZ..." 中取第 EDX 个字符
000105f3 eb 5b JMP LAB_00010650 ; 跳转到字符存储部分
switchD_000105e9::caseD_4:
000105f5 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_dSVd_bMfE787C] ; Case 4: 从字符串 "dSVd_b..." 中取第 EDX 个字符
000105fd eb 51 JMP LAB_00010650
switchD_000105e9::caseD_9:
000105ff 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_VRXFWu34uFIs4] ; Case 9: 从字符串 "VRXFWu..." 中取第 EDX 个字符
00010607 eb 47 JMP LAB_00010650
switchD_000105e9::caseD_2:
00010609 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_6TlskU63WW72e] ; Case 2: 从字符串 "6TlskU..." 中取第 EDX 个字符
00010611 eb 3d JMP LAB_00010650
switchD_000105e9::caseD_3:
00010613 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_4T3fO0ea6x0th] ; Case 3: 从字符串 "4T3fO0..." 中取第 EDX 个字符
0001061b eb 33 JMP LAB_00010650
switchD_000105e9::caseD_7:
0001061d 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_LaIe1GZiFShXt] ; Case 7: 从字符串 "LaIe1G..." 中取第 EDX 个字符
00010625 eb 29 JMP LAB_00010650
switchD_000105e9::caseD_1:
00010627 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_2aLbOr4VJQYdJ] ; Case 1: 从字符串 "2aLbOr..." 中取第 EDX 个字符
0001062f eb 1f JMP LAB_00010650
switchD_000105e9::caseD_5:
00010631 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_7BLL3aqav6SsY] ; Case 5: 从字符串 "7BLL3a..." 中取第 EDX 个字符
00010639 eb 15 JMP LAB_00010650
switchD_000105e9::caseD_6:
0001063b 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_fQfLz1d4NBgBe] ; Case 6: 从字符串 "fQfLz1..." 中取第 EDX 个字符
00010643 eb 0b JMP LAB_00010650
LAB_00010645: ; case > 9 的跳转目标
00010645 42 INC EDX ; 增加主循环计数器 EDX
00010646 eb 0d JMP switchD_000105e9::caseD_a ; 跳转到循环的公共部分
switchD_000105e9::caseD_8:
00010648 0f b6 84 MOVZX EAX, byte ptr [EBX + EDX*1 + offset s_8IViRf2Lrr10a] ; Case 8: 从字符串 "8IViRf..." 中取第 EDX 个字符
LAB_00010650: ; 所有 case 分支的汇合点
00010650 88 44 16 MOV byte ptr [ESI + EDX*1 + 0x11], AL ; 将取出的字符 (在 AL 中) 存储到栈上的缓冲区 [ESI+0x11]
11
00010654 42 INC EDX ; 增加主循环计数器 EDX
switchD_000105e9::caseD_a: ; 循环继续点
00010655 41 INC ECX ; 增加 switch 计数器 ECX
00010656 83 fa 17 CMP EDX, 0x17 ; 比较 EDX 是否达到 23 (0x17)
00010659 0f 85 71 JNZ LAB_000105d0 ; 如果没到23, 继续循环
ff ff ff
; -- 字符串构建完成, 开始反转字符串 --
0001065f b8 16 00 MOV EAX, 0x16 ; EAX = 22 (字符串最后一个字符的索引)
00 00
00010664 b9 01 00 MOV ECX, 0x1 ; ECX = 1 (字符串第二个字符的索引, 用于交换)
00 00
LAB_00010670: ; 字符串反转循环开始
00010670 0f b6 54 MOVZX EDX, byte ptr [ESI + ECX + 0x10] ; 将 [ESI+ECX+0x10] (即 buffer[ECX-1]) 的字节存入 DL
0e 10
00010675 8a 74 06 MOV DH, byte ptr [ESI + EAX + 0x11] ; 将 [ESI+EAX+0x11] (即 buffer[EAX]) 的字节存入 DH
11
00010679 88 74 0e MOV byte ptr [ESI + ECX + 0x10], DH ; 将 DH 的内容写回 buffer[ECX-1]
10
0001067d 88 54 06 MOV byte ptr [ESI + EAX + 0x11], DL ; 将 DL 的内容写回 buffer[EAX]
11
00010681 48 DEC EAX ; EAX-- (从后往前)
00010682 39 c1 CMP ECX, EAX ; 比较头尾指针
00010684 8d 49 01 LEA ECX, [ECX + 1] ; ECX++ (从前往后)
00010687 7c e7 JL LAB_00010670 ; 如果 ECX < EAX, 继续循环交换
; -- 字符串反转完成, 开始进一步处理 --
00010689 89 e2 MOV EDX, ESP ; EDX = 当前栈指针
0001068b 83 c2 d0 ADD EDX, -0x30 ; EDX 指向一个新分配的缓冲区 (在当前栈帧之下)
0001068e 89 d4 MOV ESP, EDX ; 移动栈指针, 正式分配该空间
00010690 8a 46 11 MOV AL, byte ptr [ESI + 0x11] ; 读取反转后字符串的第一个字符
00010693 84 c0 TEST AL, AL ; 测试字符是否为 NULL
00010695 74 52 JZ LAB_000106e9 ; 如果是 NULL, 跳转到结尾部分
; 初始化另一个循环
00010697 8d 4e 12 LEA ECX, [ESI + 0x12] ; ECX 指向反转后字符串的第二个字符
0001069a 89 4e 08 MOV dword ptr [ESI + 0x8], ECX ; 将该指针存入栈中 (local_38)
0001069d 31 c9 XOR ECX, ECX ; 清零 ECX, 用作计数器
0001069f 89 56 0c MOV dword ptr [ESI + 0xc], EDX ; 将新分配的缓冲区地址存入栈中 (local_34)
LAB_000106b0: ; 字符处理循环开始
000106b0 0f be c0 MOVSX EAX, AL ; 将当前字符 (AL) 符号扩展到 EAX
000106b3 89 cf MOV EDI, ECX ; 保存计数器 ECX 到 EDI
000106b5 01 d1 ADD ECX, EDX ; 计算目标缓冲区的写入地址
000106b7 83 ec 10 SUB ESP, 0x10 ; 为函数调用分配栈空间
000106ba 89 44 24 MOV dword ptr [ESP + 0xc], EAX ; 将字符作为第3个参数压栈
0c
000106be 89 0c 24 MOV dword ptr [ESP], ECX ; 将目标地址作为第1个参数压栈
000106c1 c7 44 24 MOV dword ptr [ESP + 0x4], 0xffffffff ; 将 -1 作为第2个参数压栈
04 ff ff
ff ff
000106c9 e8 52 00 CALL FUN_00010720 ; 调用外部函数 (可能是某种格式化输出, 如 snprintf)
00 00
000106ce 8b 56 0c MOV EDX, dword ptr [ESI + 0xc] ; 重新加载缓冲区地址
000106d1 83 c4 10 ADD ESP, 0x10 ; 清理调用参数占用的栈空间
000106d4 83 c7 02 ADD EDI, 0x2 ; 计数器 EDI += 2
000106d7 89 f9 MOV ECX, EDI ; 更新主计数器 ECX
000106d9 8b 7e 08 MOV EDI, dword ptr [ESI + 0x8] ; 加载字符串指针 (local_38)
000106dc 0f b6 07 MOVZX EAX, byte ptr [EDI] ; 读取下一个字符到 AL
000106df 47 INC EDI ; 字符串指针++
000106e0 89 7e 08 MOV dword ptr [ESI + 0x8], EDI ; 保存更新后的字符串指针
000106e3 84 c0 TEST AL, AL ; 检查是否到达字符串末尾 (NULL)
000106e5 75 c9 JNZ LAB_000106b0 ; 如果不是 NULL, 继续循环
; -- 循环结束 --
000106e7 eb 02 JMP LAB_000106eb
LAB_000106e9: ; 空字符串的跳转路径
000106e9 31 c9 XOR ECX, ECX ; ECX = 0
LAB_000106eb:
000106eb c6 04 0a MOV byte ptr [EDX + ECX], 0x0 ; 在最终处理过的字符串末尾添加 NULL 终止符
00
; -- JNI 调用: 将C字符串转换为Java字符串 --
000106ef 8b 45 08 MOV EAX, dword ptr [EBP + 0x8] ; EAX = param_1 (JNIEnv*)
000106f2 89 c1 MOV ECX, EAX ; ECX = JNIEnv*
000106f4 8b 00 MOV EAX, dword ptr [EAX] ; EAX = JNIEnv->functions (指向 JNI 函数表的指针)
000106f6 83 ec 08 SUB ESP, 0x8 ; 为 JNI 调用参数分配栈空间
000106f9 52 PUSH EDX ; 压入第2个参数: 指向我们最终生成的C字符串
000106fa 51 PUSH ECX ; 压入第1个参数: JNIEnv*
000106fb ff 90 9c CALL dword ptr [EAX + 0x29c] ; 调用 JNI 函数 (env->NewStringUTF), 偏移 0x29c 对应 NewStringUTF
02 00 00
00010701 83 c4 10 ADD ESP, 0x10 ; 清理 JNI 调用的参数栈空间
; 栈保护: 检查金丝雀
00010704 65 8b 0d MOV ECX, dword ptr GS:[0x14] ; 再次读取金丝雀的值
14 00 00
00
0001070b 3b 4e 28 CMP ECX, dword ptr [ESI + 0x28] ; 与保存在栈上的原始金丝雀值比较
0001070e 75 08 JNZ LAB_00010718 ; 如果不相等 (说明栈被破坏), 跳转到失败处理
; 函数尾声: 正常返回
00010710 8d 65 f4 LEA ESP, [EBP - 0xc] ; 恢复栈指针 (释放局部变量)
00010713 5e POP ESI ; 恢复 ESI 寄存器
00010714 5f POP EDI ; 恢复 EDI 寄存器
00010715 5b POP EBX ; 恢复 EBX 寄存器
00010716 5d POP EBP ; 恢复旧的栈基址指针
00010717 c3 RET ; 返回 (NewStringUTF 的结果 jstring 在 EAX 中返回)
; 栈检查失败处理
LAB_00010718:
00010718 e8 63 fd CALL <EXTERNAL>::__stack_chk_fail ; 调用栈检查失败函数, 终止程序
ff ff
IDA加gemini解密k1和k2
#include <jni.h> #include <string> #include <vector> #include <algorithm> #include <cstdio> /** * 对应于反汇编中的 Java_com_moksh_lab3_t_k1 函数。 * 此函数直接从 .rodata 段加载一个硬编码的字符串,并通过 JNI 的 NewStringUTF 函数 * 将其作为 Java 字符串返回。汇编代码没有对此字符串进行任何解码或转换操作。 * * @param env JNI 环境指针。 * @param thiz JNI 对象引用。 * @return 一个包含硬编码内容的 Java 字符串。 */ extern "C" JNIEXPORT jstring JNICALL Java_com_moksh_lab3_t_k1(JNIEnv *env, jobject /* this */) { // .rodata:000007A8 处存储了硬编码的字符串 // .text:00000586 的 JNI 调用 (call dword ptr [ecx+29Ch]) 是 NewStringUTF const char* hardcoded_string = "546d463061585a6c4e57566a636d56305545427a63336377636d5178"; return env->NewStringUTF(hardcoded_string); } /** * 对应于反汇编中的 Java_com_moksh_lab3_t_k2 函数。 * 此函数执行一个确定的、无外部输入的算法,并返回结果的十六进制表示。 * 整个过程没有随机性或猜测性环节。 * * @param env JNI 环境指针。 * @param thiz JNI 对象引用。 * @return 一个经过确定性算法生成的十六进制 Java 字符串。 */ extern "C" JNIEXPORT jstring JNICALL Java_com_moksh_lab3_t_k2(JNIEnv *env, jobject /* this */) { // 阶段 1: 动态构建一个23字节的数组 // 此逻辑严格对应 .text:000005D0 到 .text:00000659 的循环和 switch 跳转。 // .rodata 段中的数据源 const char* data_sources[] = { "ghYsLZomn1_z5K3XPTB1r8ansI7k7", "2aLbOr4VJQYdJCj0zyvXRo0mm4Jev", "6TlskU63WW72eoQRKDnG5xMIcdLir", "4T3fO0ea6x0thvY97CmatF8TGo7rL", "dSVd_bMfE787C8lBcxqkX80v8fKxZ", "7BLL3aqav6SsYgEoPOhsqkuHt2jBw", "fQfLz1d4NBgBe87wsknpgYxZWSxEW", "LaIe1GZiFShXtRy7w_qH1BQKaXzP4", "8IViRf2Lrr10aMku3cyypcxitdRrZ", "VRXFWu34uFIs4dTWQ6TtmLYZCPXs" }; unsigned char generated_array[23]; // 循环23次 (edx 从 0 到 22)。 // 每次循环中,从 data_sources[i % 10] 的第 i 个位置取一个字节。 for (int i = 0; i < 23; ++i) { generated_array[i] = data_sources[i % 10][i]; } // 阶段 2: 原地反转生成的数组 // 此逻辑严格对应 .text:00000670 到 .text:00000687 的反转循环。 int left = 0; int right = 22; while (left < right) { unsigned char temp = generated_array[left]; generated_array[left] = generated_array[right]; generated_array[right] = temp; left++; right--; } // 阶段 3: 将最终的数组转换为十六进制字符串 // 此逻辑对应 .text:000006B0 开始的循环,该循环调用 .text:00000720 中的 // __vsprintf_chk 函数,使用 "%02X" 格式化字符串。 char final_hex_string[47] = {0}; // 23 字节 * 2 十六进制字符/字节 + 1 空终止符 for (int i = 0; i < 23; ++i) { sprintf(&final_hex_string[i * 2], "%02X", generated_array[i]); } // 通过 JNI 返回最终的十六进制字符串 return env->NewStringUTF(final_hex_string); } trae编写解密脚本 #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CTF解密脚本 - 基于ida2.cpp和MainActivity.java的逻辑 该脚本实现了完整的Android CTF flag生成和解密过程 """ import hashlib import base64 def k1_function(): """ 模拟ida2.cpp中的Java_com_moksh_lab3_t_k1函数 返回硬编码的十六进制字符串 """ return "546d463061585a6c4e57566a636d56305545427a63336377636d5178" def k2_function(): """ 模拟ida2.cpp中的Java_com_moksh_lab3_t_k2函数 实现复杂的字符串构建、反转和十六进制转换算法 """ # .rodata段中的数据源(修正版本,每个字符串28个字符) data_sources = [ "ghYsLZomn1_z5K3XPTB1r8ansI7k7", # case 0 "2aLbOr4VJQYdJCj0zyvXRo0mm4Jev", # case 1 "6TlskU63WW72eoQRKDnG5xMIcdLir", # case 2 "4T3fO0ea6x0thvY97CmatF8TGo7rL", # case 3 "dSVd_bMfE787C8lBcxqkX80v8fKxZ", # case 4 "7BLL3aqav6SsYgEoPOhsqkuHt2jBw", # case 5 "fQfLz1d4NBgBe87wsknpgYxZWSxEW", # case 6 "LaIe1GZiFShXtRy7w_qH1BQKaXzP4", # case 7 "8IViRf2Lrr10aMku3cyypcxitdRrZ", # case 8 "VRXFWu34uFIs4dTWQ6TtmLYZCPXs" # case 9 ] # 阶段1: 动态构建23字节数组 generated_array = [] for i in range(23): # 从data_sources[i % 10]的第i个位置取一个字节 source_index = i % 10 char_index = i if char_index < len(data_sources[source_index]): generated_array.append(ord(data_sources[source_index][char_index])) else: # 如果索引超出范围,使用0填充 generated_array.append(0) print(f"生成的数组(反转前): {[chr(x) for x in generated_array]}") print(f"生成的数组(字节): {generated_array}") # 阶段2: 原地反转数组 left = 0 right = 22 while left < right: generated_array[left], generated_array[right] = generated_array[right], generated_array[left] left += 1 right -= 1 print(f"反转后的数组: {[chr(x) for x in generated_array]}") # 阶段3: 转换为十六进制字符串 hex_string = "" for byte_val in generated_array: hex_string += f"{byte_val:02X}" return hex_string def generate_flag(): """ 根据MainActivity.java中的逻辑生成最终的CTF flag """ print("=== CTF解密过程 ===") # 获取k1和k2的十六进制字符串 k1_hex = k1_function() k2_hex = k2_function() print(f"k1十六进制: {k1_hex}") print(f"k2十六进制: {k2_hex}") # 将十六进制转换为ASCII字符 try: k1_chars = bytes.fromhex(k1_hex).decode('ascii') k2_chars = bytes.fromhex(k2_hex).decode('ascii') except ValueError as e: print(f"十六进制解码错误: {e}") return None print(f"k1解码: {k1_chars}") print(f"k2解码: {k2_chars}") # 构造最终字符串:cygym3{k1_chars + k2_chars} final_string = f"cygym3{{{k1_chars}{k2_chars}}}" print(f"最终字符串: {final_string}") # 根据MainActivity.java中的逻辑,计算SHA-256哈希并进行Base64编码 sha256_hash = hashlib.sha256(final_string.encode('utf-8')).digest() final_flag = base64.b64encode(sha256_hash).decode('ascii') return final_flag def main(): """ 主函数 - 执行完整的CTF解密流程 """ print("Android CTF解密脚本") print("基于ida2.cpp和MainActivity.java的逻辑\n") try: flag = generate_flag() if flag: print(f"\n=== 最终结果 ===") print(f"CTF Flag: {flag}") else: print("解密失败!") except Exception as e: print(f"解密过程中发生错误: {e}") import traceback traceback.print_exc() if __name__ == "__main__": main()

浙公网安备 33010602011771号