菜逼!!啥都记录一点

ISCC-2025

整好借着打ISCC,顺便写两道WP

ISCC2025-whereisflag

APK直接打开看一眼,直接看activity里面的MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.example.whereisflag;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.whereisflag.databinding.ActivityMainBinding;

/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
private EditText flagEditText;
private Button submitButton;

static {
System.loadLibrary("whereisflag");
}///加载本地的动态链接库,在windows就是dll,在Linux就是so

@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle); //初始化activity

ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
this.binding = inflate;//使用View Binding功能将XML中的控件绑定到Java中
setContentView(inflate.getRoot());//加载UI界面

this.flagEditText = this.binding.editTextFlag;
Button button = this.binding.button;
this.submitButton = button;//把EditText(输入flag的地方)和Button(提交按钮)分别绑定到flagEditText和submitButton成员变量。

button.setOnClickListener(new View.OnClickListener() { // from class: com.example.whereisflag.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
String obj = MainActivity.this.flagEditText.getText().toString();
//给按钮设置点击事件
//当点击按钮时,取出用户在EditText中输入的文本obj

if (obj.length() != 16) {
Toast.makeText(MainActivity.this, "flag长度错误,请继续寻找", 0).show();
//设置判断长度
} else if (new a().b(obj)) {
Toast.makeText(MainActivity.this, "恭喜你找到了正确的flag", 1).show();
//调用a的方法b(obj)来校验flag是否正确

} else {
Toast.makeText(MainActivity.this, "flag错误,请继续寻找", 0).show();
//如果都不满足就输出失败
}
}
});
}
}

很明显,最简单的,看一下调用a的方法b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.whereisflag;  //表明这个类属于com.example.whereisflag包

/* loaded from: classes.dex */
public class a {
public native String compute(String str);
//compute是一个native方法,它的实现不在java中,而是在C/C++的本地代码中,通常是.so的动态链接库

public boolean b(String str) {
if (str.startsWith("ISCC{") && str.endsWith("}")) {
return compute(str.substring(5, 15)).equals("iB3A7kSISR");
}
//下面就是一个校验逻辑了,我们最后的形式必须是iB3A7kSISR!
return false;
}
}

显而易见,这就是我们需要的flag了

flag的包装头ISCC{xxxxxx}

肯定是有加密的,让我们简单分析一下

分析完毕,我们拆包去lib中找我们需要的加密逻辑。

image-20250522200206677

简单逆向分析一下咯,直接看encrypt函数

喂给ai

image-20250522201623078

没有ai我怎么活呢?,很明显了首先有个字符串反转,然后有个换表,这里说错了,只是进行了凯撒加密,只不过对应的表换了,并不是我们常见的A-C这样,然后进行了凯撒加密,偏移是3

先找一下表吧

image-20250522202232842

找到了捏,WHEReISFLAGBCDJKMNOPQTUVXYZabcdfghijklmnopqrstuvwxyz0123456789

逆向的话,仔细看你就会发现它其实是对着表进行偏移的,就是上面的表

也就是我们只需要偏移回去在倒转即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 加密映射表(来自 C++ 中的 qword_4ED70)
ABC = "WHEReISFLAGBCDJKMNOPQTUVXYZabcdefghijklmnopqrstuvwxyz0123456789"

# 解密函数(偏移 -2 后再反转)
def decode(encrypted: str) -> str:
return ''.join([ABC[(ABC.index(ch) - 2) % len(ABC)] for ch in encrypted])[::-1]

if __name__ == "__main__":
# 将 compute() 返回的密文粘贴到这里:
secret = "iB3A7kSISR" # 示例密文,来自你的 Java 代码 b(str) 中的比较值

# 解密出 flag 中间部分
content = decode(secret)

# 拼出完整 flag
result = f"ISCC{{{content}}}"
print("解密结果:", result)
##ISCC{HeRei5F1Ag}

之后得到了flag。

然后就完事了,唉,怎么说呢!

还是比较简单的

image-20250522203409814

怎么说呢?现在准备还是一天一道CTF吧。(感觉做不到)

攻防世界

Android2.0

2025.5.25

怎么说,JADX打开,简单来说是直接看Activity

image-20250525125615163

基本来说,加密逻辑就在MainActivity里了

直接看Activity的生命周期

onCreate()–>onStart()–>onResume()–>onPause()–>onStop()–>onDestory()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.example.test.ctf03;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
Button button;
EditText pwd;
TextView textView;

@Override // android.support.v7.app.AppCompatActivity, android.support.v4.app.FragmentActivity, android.support.v4.app.BaseFragmentActivityGingerbread, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);//设置布局文件activity_main.xml
init();//简单来说就是初始化的
this.button.setOnClickListener(new View.OnClickListener() { // from class: com.example.test.ctf03.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {

///主要的逻辑在这
String str = MainActivity.this.pwd.getText().toString();
///获取用户输入的字符串str
int result = JNI.getResult(str);
///调用本地JNI方法JNI.getResult(str),传入输入进行验证

MainActivity.this.Show(result); //显示结果的没啥说的
}
});
}

public void init() {
this.pwd = (EditText) findViewById(R.id.pwd); //输入框
this.button = (Button) findViewById(R.id.button); //按钮
this.textView = (TextView) findViewById(R.id.result); //文本显示区域
}

public void Show(int type) {
switch (type) {
case 0:
this.textView.setText("Wrong");
break;
case 1:
this.textView.setText("Great");
break;
}
}
}

所以找到核心的验证加密逻辑JNI.getResult(str),点进去看看

image-20250525131214322

加密逻辑一看就在Native层了,拆包看一下。正好lib中也只有一个.so文件(动态链接库)

点进去看见是ARM-32位的。

我点进去是先看的函数栏,找加密逻辑喽

image-20250525131849300

看着像分成了三段

其实感觉就是一个简单的拼接

image-20250525132430026

这个第二段和第三段在一块

image-20250525132537627

感觉不太对,转不成ASCLL码

先拼接一下试试,不太对

第一部分:LN^dl

第二部分: 5-

第三部分:AFBo}

想太多

我们直接看Java_com_example_test_ctf03_JNI_getResult这个函数,直接喂给ai看反汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
int Java_com_example_test_ctf03_JNI_getResult(JNIEnv *env, jobject thiz, jstring input) {
const char *user_input = (*env)->GetStringUTFChars(env, input, 0);
int result = 0;

if (strlen(user_input) != 15) {
return 0;
}

void Init(char *out1, char *out2, char *out3, const char *in, int len) {
if (len < 1) {
out1[0] = 0;
out2[0] = 0;
out3[0] = 0;
return;
}

// 定义用于模拟取模操作的魔数
uint32_t div3_magic = 0x55555556; // 1/3的乘法倒数(右移可得余数)
uint32_t div3_magic_2 = 0xAAAAAAAB; // 类似用于除法操作

for (int i = 0; i < len; i++) {
int mod = i % 3;

switch (mod) {
case 0: { // out1
uint32_t idx = (i * div3_magic_2) >> 1; // 等效于除以2
out1[idx] = in[i];
break;
}
case 1: { // out2
uint32_t idx = (i * div3_magic_2) >> 1;
out2[idx] = in[i];
break;
}
case 2: { // out3
uint32_t idx = (i * div3_magic_2) >> 1;
out3[idx] = in[i];
break;
}
}
}

// 添加结束符
out1[len / 3] = 0;
out2[len / 3] = 0;
out3[len / 3] = 0;
}

int First(char *buf) {
int i = 0;
unsigned char xor_key = 0x80;

// 解码前4个字节
while (i < 4) {
buf[i] = (xor_key ^ (buf[i] << 1)) & 0xFF; // 等价于 EOR.W R3, R2, R3,LSL#1
i++;
}

// 比较处理后的 buf 是否等于 "LN^dl"
if (strcmp(buf, "LN^dl") == 0)
return 1;
return 0;
}



// 分配内存
char *buf1 = malloc(1);
char *buf2 = malloc(1);
char *buf3 = malloc(1);

int len = 15;
Init(buf1, buf2, buf3, user_input, len); // 模糊处理,具体实现未知




// 如果 First(buf1) 返回非 0
if (First(buf1)) {
// buf2[i] ^= buf1[i] for i in 0..3
for (int i = 0; i < 4; i++) {
buf2[i] ^= buf1[i];
}

// 比较 buf2 和 " 5-"
if (strcmp(buf2, " 5-") == 0) {
for (int i = 0; i < 4; i++) {
buf3[i] ^= buf2[i];
}

// 比较 buf3 和 "AFBo}"
if (strcmp(buf3, "AFBo}") == 0) {
result = 1;
}
}
}

return result;
}



怎么说,ai还是强大的

这里直接建议用ida9打开就好了

image-20250527201725689

image-20250527201737145

首先知道我们要的flag长度是15,init函数把我们的字符串分成三段。first函数的加密逻辑(xor_key ^ (buf[i] << 1)) & 0xFF;,异或0x80并且左移一位,然后和LN^dl比较。buf2是异或buf1,buf3是异或buf2,分别与 5-AFBo}比较。

所以直接进行逆向解密

part1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src=list("LN^dl")
for i in range(5):
print(chr(0x80^(ord(src[i])>>1),end='')



dst = list("LN^dl")
for i in range(4):
after = ord(dst[i])
for c in range(256):
if (0x80 ^ (c << 1) & 0xFF) == after:
print(chr(c), end='')
break
###fgor
###注意这里只对前4个数进行了操作,所以第五个数不用变
1
fgorl

这里笔者进行逆向是发现不对,只能暴力破解为好

image-20250525142434958

part2

1
2
3
4
5
6
7
buf1="LN^dl"
buf2=[0x20, 0x35, 0x2D, 0x16, 0x61]
for i in range(4):
print(chr(buf2[i]^ord(buf1[i])),end='')
#l{sr

###我去,这里是对加密后的buf1进行操作的

看反编译代码只对前四个进行了操作,则第五个不动吗????

所以第五个还是a

1
l{sra

注意buf2肯定也还是五字节的

image-20250525150821692

只不过有些无法转成ASCLL而已

part3

1
2
3
4
5
buf3="AFBo}"
buf2=[0x20, 0x35, 0x2D, 0x16, 0x61]
for i in range(4):
print(chr(buf2[i]^ord(buf3[i])),end='')
#asoy

加上第五个字符

1
asoy}

最终

1
2
3
4
5
6
7
8
buf1="fgorl"
buf2="l{sra"
buf3="asoy}"

for i in range(5):
print(buf1[i]+buf2[i]+buf3[i],end='')

#flag{sosorryla}

总结:

我去我去,注意笔者在part1的时候没有注意只对前4个字节进行了操作,所以踩坑了。part2的时候没注意是对加密后的buf1进行操作的又踩坑了,最终的拼接是要flag形状的。

怎么说呢!题出的还是非常好的

APK逆向

—-2025.5.27

老样子直接拖进JADX里面看看,直接看activity里的MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.example.crackme; //包名

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
//导入的模块


/* loaded from: classes.dex */
public class MainActivity extends Activity {
private Button btn_register; //注册按钮
private EditText edit_sn; //输入框
String edit_userName;//用户名

@Override // android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //调用父类的onCreate方法,必不可少
setContentView(R.layout.activity_main); //设置页面布局的
setTitle(R.string.unregister); //设置窗口标题为"未注册"
this.edit_userName = "Tenshine";//写死用户名"Tenshine"
this.edit_sn = (EditText) findViewById(R.id.edit_sn);//通过ID找到界面的输入框
this.btn_register = (Button) findViewById(R.id.button_register);//通过ID找到界面的注册按钮
this.btn_register.setOnClickListener(new View.OnClickListener() { // from class: 监听的 com.example.crackme.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) { //点击按钮执行函数
if (!MainActivity.this.checkSN(MainActivity.this.edit_userName.trim(), MainActivity.this.edit_sn.getText().toString().trim()))
//checkSN()方法验证用户名和序列号是否匹配
//trim()是去除空格
{
Toast.makeText(MainActivity.this, R.string.unsuccessed, 0).show();
//注册不成功
return;
}
Toast.makeText(MainActivity.this, R.string.successed, 0).show();
//注册成功
MainActivity.this.btn_register.setEnabled(false);
//禁用按钮
MainActivity.this.setTitle(R.string.registered);
//更改注册表为已注册
}
});
}

@Override // android.app.Activity
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return true;
}
//创建菜单的

然后我们可以雷电模拟运行一下这个APP

image-20250527182215452

看主要的校验逻辑吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    /* JADX INFO: Access modifiers changed from: private */
public boolean checkSN(String userName, String sn) {
if (userName == null) {
return false;
}
try {
if (userName.length() == 0 || sn == null || sn.length() != 22) {
return false;
//限制长度和和序列号
}
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.reset();
digest.update(userName.getBytes());
byte[] bytes = digest.digest();
//创建MD5摘要器
//将用户名转为字节数组进行MD5哈希计算
//bytes是MD5结果

String hexstr = toHexString(bytes, "");
StringBuilder sb = new StringBuilder();
for (int i = 0; i < hexstr.length(); i += 2) {
sb.append(hexstr.charAt(i));
}
//每个一个字符取一个,拼接出新的字符串userSN
String userSN = sb.toString();
//得到最终部分
return new StringBuilder().append("flag{").append(userSN).append("}").toString().equalsIgnoreCase(sn);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return false; //拼接flag{userSN},并和用户输入的sn比较
}
}

private static String toHexString(byte[] bytes, String separator) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = Integer.toHexString(b & 255);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex).append(separator);
}
return hexString.toString();
}
}
//把一个字节数组转换为十六进制字符串。
//例如 0x1f 会变成 "1f",0x03 会变成 "03"。
//separator 参数用来控制是否在每两个字符中间插入分隔符(此程序传入 "",即不加分隔)。
//没啥用辅助功能

现在我们知道的用户名就是Tenshine,我们简单的MD5加密一下B9C77224FF234F27AC6BADF83B855C76

1
2
3
4
5
6
7
src="B9C77224FF234F27AC6BADF83B855C76"

for i in range(0, len(src), 2):
print(src[i],end="")

#BC72F242A6AF3857
flag{BC72F242A6AF3857}

然后试一下

image-20250527184249386

额,行吧,交的时候注意换成32小写即可

1
bc72f242a6af3857

像这道题就是直接在Java层中找加密逻辑

APK中的结构

层级 说明 示例
💡 应用层(App Layer) 你写的代码,比如 MainActivity.java com.example.crackme.MainActivity
🔧 Framework 层(Java Framework) Android 提供的类库,比如 Activity, Button, Toast android.app.Activity
⚙️ Native 层(Native Libraries) 用 C/C++ 编写的库,NDK、libc、libcrypto 等 可能用来做更隐蔽的加密
🧩 HAL / Kernel 层 驱动层,硬件访问层 与 CrackMe 无关

人民的名义-抓捕赵德汉1-200

2025.5.28

这次下载完是jar后缀

不到是啥意思,直接使用JADX打开

image-20250528133009826

感觉主要的加密逻辑就在这了

喂给ai,这是一个动态加载类并验证密码的实列过程,它把真正验证密码的类加密后嵌入资源中,运行时再动态加载并调用。

怎么说?frida hook一下吗?

image-20250528134629882

我先看看idea能进行打个断点调试吗?

没源码吗?这是打不开反正

这里也是直接看WP了

第一次发现jar包还能直接运行呢

1
Java -jar jar包名

然后就能运行了

java版本太高运行不起来是正常的

换个低版本即可

没啥说的

1
2
3
fa3733c647dca53a66cf8df953c2d539
md5解密得到flag
monkey99

这题出的不好

XCTF_MOBILE15_人民的名义-抓捕赵德汉1-200-CSDN博客

ill-intentions

2025.5.31

JADX打开,老样子直接进行看MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.application;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.TextView;
import com.example.hellojni.Manifest;

/* loaded from: classes.dex */
public class MainActivity extends Activity {
@Override // android.app.Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(getApplicationContext());
tv.setText("Select the activity you wish to interact with.To-Do: Add buttons to select activity, for now use Send_to_Activity");
setContentView(tv);
IntentFilter filter = new IntentFilter();
filter.addAction("com.ctf.INCOMING_INTENT");
BroadcastReceiver receiver = new Send_to_Activity();
registerReceiver(receiver, filter, Manifest.permission._MSG, null);
}
}

简单翻译一下这句英文

image-20250531131524639

简单模拟一下这个APK看一下

image-20250531131709886

简单ai了一下MainActivity主要是显示一段引导文字,提醒用户暂时通过广播与指定的Activity交互,注册了一个广播接收器Send_to_Activity,监听 com.ctf.INCOMING_INTENT

然后我们主要看Send_to_Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.application;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

/* loaded from: classes.dex */
public class Send_to_Activity extends BroadcastReceiver {
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
String msgText = intent.getStringExtra("msg");
if (msgText.equalsIgnoreCase("ThisIsTheRealOne")) {
Intent outIntent = new Intent(context, (Class<?>) ThisIsTheRealOne.class);
context.startActivity(outIntent);
} else if (msgText.equalsIgnoreCase("IsThisTheRealOne")) {
Intent outIntent2 = new Intent(context, (Class<?>) IsThisTheRealOne.class);
context.startActivity(outIntent2);
} else if (msgText.equalsIgnoreCase("DefinitelyNotThisOne")) {
Intent outIntent3 = new Intent(context, (Class<?>) DefinitelyNotThisOne.class);
context.startActivity(outIntent3);
} else {
Toast.makeText(context, "Which Activity do you wish to interact with?", 1).show();
}
}
}

监听”msg”字段的广播,根据其值启动不同的Activity

  • "ThisIsTheRealOne" → 启动 ThisIsTheRealOne 活动

  • "IsThisTheRealOne" → 启动 IsThisTheRealOne 活动

  • "DefinitelyNotThisOne" → 启动 DefinitelyNotThisOne 活动

看一下这三个Activity会发现它的每个类都加载了一个名为hello-jni的本地库。每个类都定义了若干native方法,每个类在界面中提供了一个按钮,点击后会发送一个广播com.ctf.OUTGOING_INTENT,其中附带了一段加密/处理过的msg

这下简单了,直接进行拆包,看动态加载库即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
__int64 __fastcall Java_com_example_application_IsThisTheRealOne_perhapsThis(
__int64 a1,
__int64 a2,
__int64 a3,
__int64 a4,
__int64 a5)
{
__int64 v5; // r15
const char *v8; // r12
const char *v9; // rbp
const char *v10; // rbx
char *v11; // rax
char v12; // si
char v14[80]; // [rsp+0h] [rbp-188h] BYREF
char v15[80]; // [rsp+50h] [rbp-138h] BYREF
char dest[80]; // [rsp+A0h] [rbp-E8h] BYREF
char v17[88]; // [rsp+F0h] [rbp-98h] BYREF

v5 = 0LL;
v8 = (*(*a1 + 1352LL))(a1, a3, 0LL);
v9 = (*(*a1 + 1352LL))(a1, a4, 0LL);
v10 = (*(*a1 + 1352LL))(a1, a5, 0LL);
v11 = &v8[strlen(v8)];
*v11 = '{barGwnw';
strcpy(v11 + 16, "tfqm}");
*(v11 + 1) = 'Cr\x7FhbtuO';
strncpy(dest, v8, 'L');
strncpy(v14, v9, 'L');
strncpy(v15, v10, 'L');
dest[76] = 0;
v15[76] = 0;
v14[76] = 0;
do
{
v12 = dest[v5] ^ v14[v5] ^ v15[v5];
v17[v5++] = v12;
printf("%c\n", v12);
}
while ( v5 != 76 );
v17[76] = 0;
printf("Here is your Reply: %s", v17);
return (*(*a1 + 1336LL))(a1, v17);
}



__int64 __fastcall Java_com_example_application_ThisIsTheRealOne_orThat(
__int64 a1,
__int64 a2,
__int64 a3,
__int64 a4,
__int64 a5)
{
__int64 v5; // r15
const char *v8; // r12
const char *v9; // rbp
const char *v10; // rbx
char *v11; // rax
char v12; // si
char v14[80]; // [rsp+0h] [rbp-188h] BYREF
char v15[80]; // [rsp+50h] [rbp-138h] BYREF
char dest[80]; // [rsp+A0h] [rbp-E8h] BYREF
char v17[88]; // [rsp+F0h] [rbp-98h] BYREF

v5 = 0LL;
v8 = (*(*a1 + 1352LL))(a1, a3, 0LL);
v9 = (*(*a1 + 1352LL))(a1, a4, 0LL);
v10 = (*(*a1 + 1352LL))(a1, a5, 0LL);
v11 = &v8[strlen(v8)];
*v11 = '[\x7FLElYIJ';
*(v11 + 1) = 'n`xuz`jx';
*(v11 + 2) = 'AOfz\x7FvO\x7F';
strcpy(v11 + 24, "RkiPkYmbFftKqusfn}flhQfsqxr\\TeaSgnmdc!");
strncpy(dest, v8, 'L');
strncpy(v14, v9, 'L');
strncpy(v15, v10, 'L');
dest[76] = 0;
v15[76] = 0;
v14[76] = 0;
do
{
v12 = dest[v5] ^ v14[v5] ^ v15[v5];
v17[v5++] = v12;
printf("%c\n", v12);
}
while ( v5 != 76 );
v17[76] = 0;
printf("Here is your Reply: %s", v17);
return (*(*a1 + 1336LL))(a1, v17);
}




__int64 __fastcall Java_com_example_application_DefinitelyNotThisOne_definitelyNotThis(
__int64 a1,
__int64 a2,
__int64 a3,
__int64 a4)
{
(*(*a1 + 1352LL))(a1, a3, 0LL);
(*(*a1 + 1352LL))(a1, a4, 0LL);
return (*(*a1 + 1336LL))(a1, "Told you so!");
}

这三段代码里ThisIsTheRealOne是我们真正想要的,本来是想要动态调试一下这个.so文件的,有报错没调试起来。上网找了找

image-20250531150045938

照着文章整了整,但是把你会发现报错,我们调试android_server64时他会报错Segmentation fault,然后呢我们换成android x64 server进行调试的时候,你又会发现我们ida又会报错

image-20250531150404086

SO,笔者的建议还是使用frida进行hook好了,但是笔者这里不会搓脚本,然后也是使用ai编写的了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Interceptor.attach(Module.getExportByName(null, "dlopen"), {
onEnter: function (args) {
this.path = Memory.readCString(args[0]);
},
onLeave: function (retval) {
if (this.path.indexOf("libhello-jni.so") !== -1) {
console.log("[*] libhello-jni.so loaded");

var base = Module.findBaseAddress("libhello-jni.so");
console.log("[*] Base address:", base);

// 这里根据你 readelf 得到的 offset 修改
var target = base.add(0x6c0);
console.log("[*] Hook target function at", target);

Interceptor.attach(target, {
onEnter: function (args) {
console.log("[*] Function called");
console.log("Arg0: " + args[0]);
console.log("Arg1: " + args[1]);
},
onLeave: function (retval) {
console.log("[*] Return value:", retval);
}
});
}
}
});

不咋会用frida

image-20250531151801026

hook之后应该做什么哇?这里直接也是看WP了

下面直接贴hook脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34


function main() {
Java.perform(function() {
var DefinitelyNotThisOneHandler = Java.use('com.example.application.DefinitelyNotThisOne')
DefinitelyNotThisOneHandler.definitelyNotThis.implementation = function(arg0, arg1) {
console.log('DefinitelyNotThisOneHandler called: ' + arg0 + " \n" + arg1)
var ret = this.definitelyNotThis(arg0, arg1)
console.log('DefinitelyNotThisOneHandler ret: ' + ret )
return ret
}

var ThisIsTheRealOneHandler = Java.use('com.example.application.ThisIsTheRealOne')
ThisIsTheRealOneHandler.orThat.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(arg0, arg1, arg2) {
console.log('ThisIsTheRealOneHandler called: ' + arg0 + " \n" + arg1 + " \n" + arg2)
var ret = this.orThat(arg0, arg1, arg2)
console.log('ThisIsTheRealOneHandler ret: ' + ret )
return ret
}

var IsThisTheRealOneHandler = Java.use('com.example.application.IsThisTheRealOne')
IsThisTheRealOneHandler.perhapsThis.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function(arg0, arg1, arg2) {
console.log('IsThisTheRealOneHandler called: ' + arg0 + " \n" + arg1 + " \n" + arg2)
var ret = this.perhapsThis(arg0, arg1, arg2)
console.log('IsThisTheRealOneHandler ret: ' + ret )
return ret
}
})
}

setImmediate(main)

//firda-ps -U
//frida -U -n "CTF Application" -l hook.js

步骤就不说了,不会hook的,简单去学一下吧,或者看一下笔者的另一篇文章。

image-20250531161101809

hook上之后,要在模拟器那边简单触发一下。

这里讲一下objection

它是基于frida的命令工具,主要用于对 Android 和 iOS 应用的动态分析,尤其适合逆向工程、安全测试和 CTF 挖洞。

常见的功能如下:

功能 说明
explore 进入应用交互控制台
android hooking Hook Java 方法、类和 native 函数
android intent 启动 Activity,查看 intent 内容
android memory 查看内存中的字符串、对象等
android keystore 与系统 Keystore 交互
android file download 下载应用内部的文件
android sslpinning disable 绕过 SSL Pinning(HTTPS 拦截)
android misc 常用的辅助操作,如截图、剪贴板查看等

命令

1
2
3
4
5
6
>objection -g com.example.hellojni explore


>>>CLI中输入
>android intent launch_activity com.example.application.IsThisTheRealOne
>android hooking watch class_method android.content.Intent.putExtra --dump-return --dump-args --dump-backtrace

然后我们hook的时候就可以看见把拦截的信息打印下来了

image-20250531160841428

bananaship师傅说还是很好用的,简单了解一下吧

总结:

学的东西还是挺多的,至少ida的对安卓设备的调试会了,虽说没有用上。hook也是学上了,对frida的了解又加深了。对hook脚本的编写希望能在进一些。

boomshakalaka-3

2025.6.2

直接JADX打开,哦?没有MainActivtiy

雷电模拟器打开一下这个文件,然后玩了一下

image-20250602123002391

哈哈彩笔

image-20250602123427666

so,直接找Activity入口点喽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.example.plane;

import android.os.Bundle;
import org.cocos2dx.lib.Cocos2dxActivity;
import org.cocos2dx.lib.Cocos2dxGLSurfaceView;

/* loaded from: classes.dex */
public class FirstTest extends Cocos2dxActivity {
@Override // org.cocos2dx.lib.Cocos2dxActivity, android.app.Activity
//FirstTest是游戏的主Activity
//继承自Cocos2dxActivity,这意味着它是一个基于Cocos2d-x 的 OpenGL 游戏应用。

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//初始化不说啥了

a haha = new a(this, "flag");
haha.d("YmF6aW5nYWFhYQ==");
//创建了一个a类型的对象haha,传入上下文和"flag"
//然后调用d()方法,传入的是一个base64编码的字符串
//bazingaaaa
a hehe = new a(this, "Cocos2dxPrefsFile");
hehe.d("N0");
//创建了另一个a类的对象hehe,这个传入的名字是Cocos2d-x 默认的配置文件名。
//调用d("N0")
}

@Override // org.cocos2dx.lib.Cocos2dxActivity
public Cocos2dxGLSurfaceView onCreateView() {
Cocos2dxGLSurfaceView glSurfaceView = new Cocos2dxGLSurfaceView(this);
//创建并返回一个用于 OpenGL 渲染的视图对象(游戏画面在此绘制)。
a hehe = new a(this, "Cocos2dxPrefsFile");
hehe.d("MG");
//再次调用 a.d() 方法,传入 "MG",行为类似 onCreate(),可能控制状态流或做数据处理。
glSurfaceView.setEGLConfigChooser(5, 6, 5, 0, 16, 8);
return glSurfaceView;
//设置 OpenGL 渲染配置:颜色通道、深度缓冲区等参数。

}

static {
System.loadLibrary("cocos2dcpp");
//加载本地的倒台链接库
}
}

base64解码

image-20250602123715023

abcd的几个方法我也看了

image-20250602124734041

没啥用,感觉还是Native层的加密,我先把bazingaaaa套上flag试试,不对

这里直接进行拆包然后看动态链接库,ARM架构的,直接使用ida9打开

打开是打开了,但是感觉找不到明显的加密逻辑,唯一有个还是像是base64换表

image-20250602130654692

哦?还得用hook吗?

这里起adb服务的时候有点问题

Android Studio启动虚拟器后出现emulator-5554 offline-CSDN博客

结果不是hook

看了一眼wp感觉还挺抽象的

首先我们看动态链接库是没问题的,找不到加密方法也是正常的,因为题就不是这么做的。

因为更具题目提示,它是跟分数有关的,找一下有关分数的这个函数

image-20250602150340219

image-20250602145413487

大概就是你获得分数后,字符串会写回xml中。

并且看java层a方法的调用

image-20250602150615314

它使用了SharedPreference存储数据,SharedPreferences是Android平台上一个轻量级的存储类,主要是保存一些常用的配置比如窗口状态。这意味着我们可以在/data/data/{package名}SharedPrefs中可能会找到需要的数据。

image-20250602150706682

扔进厨子

image-20250602150939406

得到前半段,并且注意我们可以看见它一直是以dz99结尾的

1
2
MGN0ZntDMGNvUzJkX0FuRHJvMW
//0ctf{C0coS2d_AnDro1

然后我们看.so文件的几个字符串

image-20250602151153908

1
MWRfRzBtRV9Zb1VfS24w

然后拼接一下

image-20250602150028510

反正挺抽象的0ctf{C0coS2d_AnDro1d_G0mE_YoU_Kn0w?}

easy-app

老样子直接JADX打开,直接看MainActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.myapplication;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
public native String check(String str);

static {
System.loadLibrary("native-lib");
}

@Override // androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
final TextView textView = (TextView) findViewById(R.id.editTextTextPersonName);
((Button) findViewById(R.id.button)).setOnClickListener(new View.OnClickListener() { // from class: com.example.myapplication.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
MainActivity mainActivity = MainActivity.this;
Toast.makeText(mainActivity, mainActivity.check(textView.getText().toString()), 0).show();
}
});
}
}

没啥说的,就是一个check,具体的check机制在Native层。

直接进行拆包进行反编译动态链接库

看一下函数栏

image-20250608134043201

找一下加密逻辑

image-20250608134704275

没啥说的TEA加密

image-20250608134735286

base64编码

找到密文

image-20250608134806231

1
e)n*pNe%PQy!^oS(@HtkUu+Cd$#hmmK&ieytiWwYkIA=

看ai咋说

image-20250608134907650

差不多那就正常逆向的话就是base64解码,然后TEA解密呗

在往上看的时候有CheckM::CheckMCheckM::check1两个函数,可能也会对加密有点影响,但是先不管了

找一下密钥

image-20250608140347177

但是base64解码可能不对,应该存在一个换表

image-20250608140428569

但是看不见,嘶这是为什么,直接静态不让分析呗就

那就动态调试呗,上一道题有讲怎么样调试

image-20250608150547605

找是找到了,不过为什么我反编译不了呢

image-20250608150649687

也就是说找不到这个函数呗

1
2
3
4
5
#adb端口转发
adb forward tcp:23946 tcp:23946

#查找动态链接库
libnative-lib.so

怎么说呢加密分为三个部分吧

  • 首先将前十六位的高四位和后十六位的低四位组合存放到后十六位,将后十六位的高四位和前十六位的低四位组合存放到前十六位。
  • 然后进行了一个TEA加密。密钥是变了的,动态调试的时候取出,但是笔者这里不知道为什么不能反编译
  • 然后进行了base64变表和一个每三位循环向左移动,第四位做分隔符不变

笔者这里没有取出密钥

正确应该是0x42,0x37,0x2c,0x21。测不知道为什么

然后就是照着脚本抄即可

题非常好~

[原创]2020太湖杯物联网安全大赛easy-app解题wp-软件逆向-看雪-安全社区|安全招聘|kanxue.com

攻防世界 XCTF 【Mobile】easy-app 题解正常先下载附件,解压后,先拖到 JADX-gui 中去放编译一 - 掘金 (juejin.cn)