静态分析java层

2014 ASIS Cyber Security Contest Finals Numdroid

逆向分析

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
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayTools.each_with_index(buttons, new EachIndexAction<Integer>() { // from class: io.asis.ctf2014.numdriod.MainActivity.1
@Override // io.asis.ctf2014.numdriod.tools.EachIndexAction
public void action(final int i, Integer element) {
((Button) MainActivity.this.findViewById(element.intValue())).setOnClickListener(new View.OnClickListener() { // from class: io.asis.ctf2014.numdriod.MainActivity.1.1
@Override // android.view.View.OnClickListener
public void onClick(View arg0) {
MainActivity.this.clicked(i);
}
});
}
});
this.mScreen = (EditText) findViewById(R.id.editText1);
this.mScreen.setEnabled(false);
this.mOk = (ImageButton) findViewById(R.id.ok);
this.mDel = (ImageButton) findViewById(R.id.del);
this.mOk.setOnClickListener(new View.OnClickListener() { // from class: io.asis.ctf2014.numdriod.MainActivity.2
@Override // android.view.View.OnClickListener
public void onClick(View arg0) {
MainActivity.this.ok_clicked();
}
});
this.mDel.setOnClickListener(new View.OnClickListener() { // from class: io.asis.ctf2014.numdriod.MainActivity.3
@Override // android.view.View.OnClickListener
public void onClick(View arg0) {
MainActivity.this.del_clicked();
}
});
}
  1. 首先遍历按钮数组,并进行初始化和设置点击事件 MainActivity.this.clicked(i);
  2. 然后对OK和del按钮进行初始化并设置点击事件分别为 MainActivity.this.ok_clicked();MainActivity.this.del_clicked();
1
2
3
4
5
public void clicked(int i) {
DebugTools.log("number: " + i);
this.mScreen.setText(this.mScreen.getText().append((CharSequence) Integer.toString(i)));
DebugTools.log("current Pass: " + ((Object) this.mScreen.getText()));
}

点击按钮时候会先log记录下来,然后再将数字转为String显示到屏幕上,然后再log记录下当前的passwd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void ok_clicked() {
DebugTools.log("clicked password: " + ((Object) this.mScreen.getText()));
boolean result = Verify.isOk(this, this.mScreen.getText().toString());
DebugTools.log("password is Ok? : " + result);
if (result) {
Intent i = new Intent(this, (Class<?>) LipSum.class);
Bundle b = new Bundle();
b.putString("flag", this.mScreen.getText().toString().substring(0, 7));
i.putExtras(b);
startActivity(i);
return;
}
Toast.makeText(this, R.string.wrong, 1).show();
this.mScreen.setText("");
}

点击OK按钮,会先log记录当前的passwd,然后通过Verify.isOk对其进行check,如果密码正确会进入到分支,然后会把flag放到屏幕上,显然这就是胜利条件了

第一个idea,对isOk进行hook,测试之后发现,他的flag是md5的密码,显然偷鸡失败(/(ㄒoㄒ)/~~

那么就要研究isOk函数的逻辑:

1
2
3
4
5
6
7
8
9
public static boolean isOk(Context c, String _password) {
String password = _password;
if (_password.length() > 7) {
password = _password.substring(0, 7); // 只取前7个数字
}
String r = OneWayFunction(password);
DebugTools.log("digest: " + password + " => " + r);
return r.equals("be790d865f2cea9645b3f79c0342df7e");
}

password只取前7个数字进OneWayFunction函数,之后将其返回的值与一个hash进行比对

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
private static String OneWayFunction(final String password) {
String[] hashes = {"MD2", "MD5", "SHA-1", "SHA-256", "SHA-384", "SHA-512"};
List<byte[]> bytes = ArrayTools.map(ArrayTools.select(ArrayTools.map(hashes, new MapAction<String, byte[]>() { // from class: io.asis.ctf2014.numdriod.Verify.1
@Override // io.asis.ctf2014.numdriod.tools.MapAction
public byte[] action(String element) {
try {
MessageDigest digest = MessageDigest.getInstance(element);
digest.update(password.getBytes());
return digest.digest();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
}), new SelectAction<byte[]>() { // from class: io.asis.ctf2014.numdriod.Verify.2
@Override // io.asis.ctf2014.numdriod.tools.SelectAction
public boolean action(byte[] element) {
return element != null;
}
}), new MapAction<byte[], byte[]>() { // from class: io.asis.ctf2014.numdriod.Verify.3
@Override // io.asis.ctf2014.numdriod.tools.MapAction
public byte[] action(byte[] element) {
byte[] b = new byte[8];
for (int i = 0; i < b.length / 2; i++) {
b[i] = element[i];
}
for (int i2 = 0; i2 < b.length / 2; i2++) {
b[(b.length / 2) + i2] = element[(element.length - i2) - 2];
}
return b;
}
});
byte[] b2 = new byte[bytes.size() * 8];
for (int i = 0; i < b2.length; i++) {
b2[i] = bytes.get(i % bytes.size())[i / bytes.size()];
}
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(b2);
byte[] messageDigest = digest.digest();
StringBuilder hexString = new StringBuilder();
for (byte aMessageDigest : messageDigest) {
String h = Integer.toHexString(aMessageDigest & 255);
while (h.length() < 2) {
h = "0" + h;
}
hexString.append(h);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
return "";
}
}

发现hash基本都是单向的,发现如果只是爆破的话只有10^7的大小,感觉可以尝试

方案1:

网上师傅的方案:

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
.line 26
:cond_0
const/4 v0, 0x0

.local v0, "i":I
:goto_0
const v2, 0x98967f

if-lt v0, v2, :cond_1

.line 38
return-void

.line 28
:cond_1
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2

invoke-static {v2}, Lcom/example/forloop/MainActivity;->isOk(Ljava/lang/String;)Z

move-result v1

.line 29
.local v1, "ok":Z
const v2, 0x186a0

rem-int v2, v0, v2

if-nez v2, :cond_2

.line 31
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2

invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V

.line 33
:cond_2
if-eqz v1, :cond_3

.line 35
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2

invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V

.line 26
:cond_3
add-int/lit8 v0, v0, 0x1

goto :goto_0
1
2
3
4
5
:cond_0
const/4 v0, 0x0

.local v0, "i":I
:goto_0

这里首先初始化v0=0,然后初始化循环器i(存储在v0中)

1
2
const v2, 0x98967f       # 加载最大值 9,999,999 (0x98967F)
if-lt v0, v2, :cond_1 # 如果 i >= 9,999,999,跳转到 :cond_1(退出循环)

if v0 less than v2(即v0<v2)就会跳转到cond_1

1
2
3
4
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;
move-result-object v2 # 将 i 转为字符串
invoke-static {v2}, Lcom/example/forloop/MainActivity;->isOk(Ljava/lang/String;)Z
move-result v1 # 调用 isOk() 并存储结果到 v1(ok)

v0转换为字符串,然后将结果存储到v2,之后调用isOk,参数是v2,然后将返回值放入v1

1
2
3
4
5
6
7
8
const v2, 0x186a0        # 加载 100,000 (0x186A0)
rem-int v2, v0, v2 # 计算 i % 100,000
if-nez v2, :cond_2 # 如果余数为0(每100,000次),执行打印

# 打印当前进度
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;
move-result-object v2
invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V

if v2 no equal zero,就是v2!=0就会跳转到cond_2标签处执行,否则继续执行下一条指令

1
2
3
4
5
6
7
:cond_2
if-eqz v1, :cond_3 # 如果 isOk() 返回 true,执行成功逻辑

# 打印成功的数字
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;
move-result-object v2
invoke-virtual {p0, v2}, Lcom/example/forloop/MainActivity;->log(Ljava/lang/String;)V

如果v1==0就会打印成功的数字,调用的是log进行打印

1
2
3
:cond_3
add-int/lit8 v0, v0, 0x1 # i++
goto :goto_0 # 跳回循环开头

如果没满足,就执行i++然后跳转回去

但是为了将其粘贴到apk中就要对其进行修改

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
const/4 v0, 0x0

.local v0, "i":I
:goto_3
const v2, 0x98967f

if-lt v0, v2, :cond_3
return-void

.line 28
:cond_3
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2

invoke-static {p0, v2}, Lio/asis/ctf2014/numdriod/Verify;->isOk(Landroid/content/Context;Ljava/lang/String;)Z

move-result v1

.line 29
.local v1, "ok":Z

const v4, 0x186a0

rem-int v4, v0, v4

if-nez v4, :cond_6

.line 31
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2
invoke-static {v2}, Lio/asis/ctf2014/numdriod/tools/DebugTools;->log(Ljava/lang/String;)V

:cond_6

if-eqz v1, :cond_2

goto :goto_4
.line 31
invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String;

move-result-object v2

invoke-static {v2}, Lio/asis/ctf2014/numdriod/tools/DebugTools;->log(Ljava/lang/String;)V

.line 26
:cond_2
add-int/lit8 v0, v0, 0x1

goto :goto_3

方案2:

这个方案是直接将Verify中加个main函数,然后修改一些报错,直接运行即可,参照https://gist.github.com/volpino/2edea8503822aefb4c9e

得到答案是

1
2
................> java .\sample\Verify.java
FOUND!! 3130110
image-20250330220516717

2014 Sharif University Quals CTF Commercial Application

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
private void checkLicenceKey(final Context context) {
if (this.app.getDataHelper().getConfig().hasLicence()) { // 如果没有有效许可证
showAlertDialog(context, OK_LICENCE_MSG);
return;
}
LayoutInflater li = LayoutInflater.from(context);
View promptsView = li.inflate(R.layout.propmt, (ViewGroup) null);
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(context);
alertDialogBuilder.setView(promptsView);
final EditText userInput = (EditText) promptsView.findViewById(R.id.editTextDialogUserInput);
alertDialogBuilder.setCancelable(false).setPositiveButton("Continue", new DialogInterface.OnClickListener() { // from class: edu.sharif.ctf.activities.MainActivity.4
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialog, int id) {
String userEnteredValue = userInput.getText().toString();
String storedKey = MainActivity.this.app.getDataHelper().getConfig().getSecurityKey();
String iv = MainActivity.this.app.getDataHelper().getConfig().getSecurityIv();
boolean licenceKeyIsValid = KeyVerifier.isValidLicenceKey(userEnteredValue, storedKey, iv);
if (licenceKeyIsValid) {
MainActivity.this.app.getDataHelper().updateLicence(2014);
MainActivity.isRegisterd = true;
MainActivity.this.showAlertDialog(context, MainActivity.OK_LICENCE_MSG);
return;
}
MainActivity.this.showAlertDialog(context, MainActivity.NOK_LICENCE_MSG);
}
}).setNegativeButton("Cancel", new DialogInterface.OnClickListener() { // from class: edu.sharif.ctf.activities.MainActivity.5
@Override // android.content.DialogInterface.OnClickListener
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog inputLicenceDialog = alertDialogBuilder.create();
inputLicenceDialog.show();
}

关键函数,处理key过程如下

按下continue之后,usrinput会被转为string进入

1
2
3
4
String userEnteredValue = userInput.getText().toString();
String storedKey = MainActivity.this.app.getDataHelper().getConfig().getSecurityKey();
String iv = MainActivity.this.app.getDataHelper().getConfig().getSecurityIv();
boolean licenceKeyIsValid = KeyVerifier.isValidLicenceKey(userEnteredValue, storedKey, iv);

会从配置中得到storedKey和iv,之后进入是否有效的判断

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
public static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
public static final String VALID_LICENCE = "29a002d9340fc4bd54492f327269f3e051619b889dc8da723e135ce486965d84";

public static boolean isValidLicenceKey(String userInput, String secretKey, String iv) {
String encryptUserInputData = encrypt(userInput, secretKey, iv);
return encryptUserInputData.equals(VALID_LICENCE);
}

public static String encrypt(String userInput, String secretKey, String iv) {
try {
SecretKeySpec secretKeySpec = new SecretKeySpec(hexStringToBytes(secretKey), "AES");
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
cipher.init(1, secretKeySpec, ivSpec);
byte[] encryptedBytes = cipher.doFinal(userInput.getBytes());
String encryptedText = bytesToHexString(encryptedBytes);
return encryptedText;
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

public static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", Integer.valueOf(b & 255)));
}
return sb.toString();
}

public static byte[] hexStringToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}

主要是通过encrypt函数进行加密,然后和invalid_license进行比较

在getconfig中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public AppConfig getConfig() {
AppConfig agency = new AppConfig();
Cursor cursor = this.myDataBase.rawQuery(SELECT_QUERY, null);
if (cursor.moveToFirst()) {
agency.setId(cursor.getInt(0));
agency.setName(cursor.getString(1));
agency.setInstallDate(cursor.getString(2));
agency.setValidLicence(cursor.getInt(3) > 0);
agency.setSecurityIv(cursor.getString(4));
agency.setSecurityKey(cursor.getString(5));
agency.setDesc(cursor.getString(7));
}
return agency;
}

可以发现在这里设置了iv和Key

1
2
3
4
5
6
7
private final Context myContext;
private SQLiteDatabase myDataBase;
private static String DB_PATH = "/data/data/edu.sharif.ctf/databases/";
private static String DB_NAME = "db.db";
private static String TABLE_NAME = "config";
public static final String UPDATE_QUERY = "UPDATE " + TABLE_NAME + " SET d=?";
public static final String SELECT_QUERY = "SELECT * FROM " + TABLE_NAME + " WHERE a=1";

是从数据库中得到的,且有给出路径,那么直接

1
adb pull /data/data/edu.sharif.ctf/databases/db.db

获得数据库,然后就能得到iv和Key了

image-20250331151635178
1
2
iv=a5efdbd57b84ca36 
key=37eaae0141f1a3adf8a1dee655853714

因为AES CBC是对称加密的,所以加解密的Key都是一样的

写个解密AES/CBC/PKCS5Padding的java类即可

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
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Main {
public static final String iv="a5efdbd57b84ca36";
public static final String Key="37eaae0141f1a3adf8a1dee655853714";
public static final String en_text="29a002d9340fc4bd54492f327269f3e051619b889dc8da723e135ce486965d84";
public static void main(String[] args) {
String data=decrypt(en_text,Key,iv);
System.out.println(data);
}
public static String decrypt(String paramString1, String paramString2, String paramString3) {
try {
SecretKeySpec localSecretKeySpec = new SecretKeySpec(hexStringToBytes(paramString2), "AES");
Cipher localCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
localCipher.init(Cipher.DECRYPT_MODE, localSecretKeySpec, new IvParameterSpec(paramString3.getBytes()));
byte[] bytes = localCipher.doFinal(hexStringToBytes(paramString1));
String flag = "";
for (byte b : bytes) {
flag += (char) b;
}
return flag;
} catch (Exception localException) {
localException.printStackTrace();
}
return "";
}
public static String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", Integer.valueOf(b & 255)));
}
return sb.toString();
}

public static byte[] hexStringToBytes(String s) {
int len = s.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
}
return data;
}
}

成功得到fl-ag-IS-se-ri-al-NU-MB-ER

2015-0CTF-vezel

1
2
3
4
5
6
7
8
9
10
11
public void confirm(View v) {
String s = getPackageName();
String first = String.valueOf(getSig(s));
String next = getCrc();
String flag = "0CTF{" + first + next + "}";
if (flag.equals(this.et.getText().toString())) {
Toast.makeText(this, "Yes!", 0).show();
} else {
Toast.makeText(this, "0ops!", 0).show();
}
}

主要代码如上,发现是通过getSig和getCrc来拼接获得flag的

那么考虑直接hook getSig和getCrc获得对应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
var MainActivity = Java.use('com.ctf.vezel.MainActivity');
MainActivity.getSig.implementation = function(packageName) {
var result = this.getSig(packageName);
console.log("Signature hashCode: " + result);
return result;
};
MainActivity.getCrc.implementation = function() {
var result = this.getCrc();
console.log("Crs: " + result);
return result;
};
});
1
2
Signature hashCode: -183971537
Crs: 1189242199

得到flag为0CTF{-1839715371189242199}

2017 XMAN HelloSmali2

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
package com.example.hellosmali.hellosmali;

/* loaded from: D:\android_re\CTF\wiki\hello\sec.dex */
public class Digest {
public static boolean check(String input) {
if (input != null && input.length() != 0) {
char[] charinput = input.toCharArray();
StringBuilder v2 = new StringBuilder();
for (char c : charinput) {
String intinput = Integer.toBinaryString(c);
while (intinput.length() < 8) {
intinput = "0" + intinput;
}
v2.append(intinput);
}
while (v2.length() % 6 != 0) { // 字符串的长度要是6的倍数
v2.append("0");
}
String v1 = String.valueOf(v2);
char[] v4 = new char[v1.length() / 6];
for (int i = 0; i < v4.length; i++) {
int v6 = Integer.parseInt(v1.substring(0, 6), 2);
v1 = v1.substring(6);
v4[i] = "+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".charAt(v6);
}
StringBuilder v3 = new StringBuilder(String.valueOf(v4));
if (input.length() % 3 != 1) {
if (input.length() % 3 == 2) {
v3.append("!");
}
} else {
v3.append("!?");
}
String key = String.valueOf(v3);
if (key.equals("xsZDluYYreJDyrpDpucZCo!?")) {
return true;
}
return false;
}
return false;
}
}

逆向函数:

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
public class re1 {
public static void main(String[] args) {
String key="xsZDluYYreJDyrpDpucZCo!?";
String charset="+/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
key=key.substring(0,key.length()-2);
System.out.println(key);
StringBuilder binary_string=new StringBuilder();
for(int i=0;i<key.length();i++){
char c=key.charAt(i);
int index=charset.indexOf(c);
String binary=String.format("%6s",Integer.toBinaryString(index)).replace(' ','0');
binary_string.append(binary);
}
StringBuilder result=new StringBuilder();
String binary=binary_string.toString();
for(int i=0;i<binary.length();i+=8){
if(i+8>binary.length()){
break;
}
String byteStr=binary.substring(i,i+8);
int charCode=Integer.parseInt(byteStr,2);
result.append((char)charCode);
}
System.out.println(result);
}
}

1
2
xsZDluYYreJDyrpDpucZCo
eM_5m4Li_i4_Ea5y

得到结果

静态分析原生层

2015 - 海峡两岸 - 一个 APK,逆向试试吧

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
public native boolean testFlag(String str);

static {
System.loadLibrary("mobicrackNDK");
}

@Override // android.support.v7.app.ActionBarActivity, android.support.v4.app.FragmentActivity, android.app.Activity
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crack_me);
this.inputButton = (Button) findViewById(R.id.input_button);
this.pwdEditText = (EditText) findViewById(R.id.pwd);
this.inputButton.setOnClickListener(new View.OnClickListener() { // from class: com.example.mobicrackndk.CrackMe.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
CrackMe.this.input = CrackMe.this.pwdEditText.getText().toString();
if (CrackMe.this.input != null) {
if (CrackMe.this.testFlag(CrackMe.this.input)) {
Toast.makeText(CrackMe.this, CrackMe.this.input, 1).show();
} else {
Toast.makeText(CrackMe.this, "Wrong flag", 1).show();
}
}
}
});
}

不难看出,testFlag就是核心的函数,加载的mobicrackNDK

但是so文件dump下来直接搜索符号是没有的,因此考虑寻找JNI_Onload函数(通过 JNI_OnLoad 动态注册)

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
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
int v3; // r5
char *v4; // r7
int v5; // r1
FILE *stream; // [sp+4h] [bp-24h]
int v8; // [sp+Ch] [bp-1Ch] BYREF

v8 = 0;
printf("JNI_OnLoad");
if ( (*vm)->GetEnv(vm, (void **)&v8, 65540) )
goto LABEL_6;
v3 = v8;
v4 = classPathName[0];
stream = (FILE *)((char *)&_sF + 168);
fprintf((FILE *)((char *)&_sF + 168), "RegisterNatives start for '%s'", classPathName[0]);
v5 = (*(int (__fastcall **)(int, char *))(*(_DWORD *)v3 + 24))(v3, v4);
if ( !v5 )
{
fprintf(stream, "Native registration unable to find class '%s'", v4);
LABEL_6:
fputs("GetEnv failed", (FILE *)((char *)&_sF + 168));
return -1;
}
if ( (*(int (__fastcall **)(int, int, char **, int))(*(_DWORD *)v3 + 860))(v3, v5, off_400C, 2) < 0 )
{
fprintf(stream, "RegisterNatives failed for '%s'", v4);
goto LABEL_6;
}
return 65540;
}
1
if ( (*(int (__fastcall **)(int, int, char **, int))(*(_DWORD *)v3 + 860))(v3, v5, off_400C, 2) < 0 )

这就是注册的函数

1
2
3
4
5
6
7
8
9
.data:0000400C off_400C        DCD aTestflag           ; DATA XREF: JNI_OnLoad+60↑o
.data:0000400C ; JNI_OnLoad+68↑o ...
.data:0000400C ; "testFlag"
.data:00004010 DCD aLjavaLangStrin_0 ; "(Ljava/lang/String;)Z"
.data:00004014 DCD abcdefghijklmn+1
.data:00004018 DCD aHello ; "hello"
.data:0000401C DCD aLjavaLangStrin_1 ; "()Ljava/lang/String;"
.data:00004020 DCD native_hello+1
.data:00004020 ; .data ends

可以看到testFlag就是abcdefghijklmn函数,第二个参数括弧里的是参数,括弧外的是返回值的类型,可以看到testFlag函数的参数类型是String,参数返回类型是bool型

1
bool __fastcall abcdefghijklmn(int a1, int a2, int a3)
  • a1JNIEnv* 指针(通过 jniEnv 全局变量缓存)
  • a2:未使用
  • a3:输入的 Java 字符串对象(jstring
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
bool __fastcall abcdefghijklmn(JNIEnv *a1, int a2, void *a3)
{
int v5; // r4
size_t i; // r6
jmethodID v7; // r2
struct _jfieldID *key; // r4
jobject v9; // r0
const char *v10; // r5
jclass v12; // [sp+4h] [bp-C4h]
const char *user_input; // [sp+8h] [bp-C0h]
char s2[12]; // [sp+14h] [bp-B4h] BYREF
char v15[140]; // [sp+20h] [bp-A8h] BYREF

if ( !jniEnv )
jniEnv = a1; // 缓存JNIEnv指针
memset(&v15[12], 0, 0x80u);
user_input = (*jniEnv)->GetStringUTFChars(jniEnv, a3, 0);// 将java字符串转为c字符串
v5 = 0;
if ( strlen(user_input) == 0x10 )
{
for ( i = 0; i != 8; ++i )
s2[i] = user_input[i] - i; // 前8个字符减去索引值
v5 = 0;
s2[8] = 0;
if ( !strcmp(seed[0], s2) ) // 与seed[0]比较
{
v12 = (*jniEnv)->FindClass(jniEnv, "com/example/mobicrackndk/Calc");// 加载这个类
if ( !v12 )
{
_android_log_print(4, "log", "class,failed");
goto LABEL_11;
}
v7 = (*jniEnv)->GetStaticMethodID(jniEnv, v12, "calcKey", "()V");
if ( !v7 )
{
_android_log_print(4, "log", "method,failed");
LABEL_11:
exit(1);
}
_JNIEnv::CallStaticVoidMethod(jniEnv, v12, v7);// 调用 Java 层的 Calc.calcKey() 方法
key = (*a1)->GetStaticFieldID(a1, v12, "key", "Ljava/lang/String;");// 获取该静态字段的jfieldID
if ( !key )
_android_log_print(4, "log", "fid,failed");
v9 = (*a1)->GetStaticObjectField(a1, v12, key);// 获取目标类的静态字段值
v10 = (*jniEnv)->GetStringUTFChars(jniEnv, v9, 0);
while ( i < strlen(v10) + 8 )
{
s2[i + 4] = user_input[i] - i; // s2[i+4]其实就是v15[i]
++i;
}
v15[8] = 0;
return strcmp(v10, v15) == 0; //与key进行比较
}
}
return v5;
}

逆向得到,我们的输入的前八个字节减去索引要与seed[0]相等,后八个字节减去索引要与key相同

1
2
3
4
5
6
7
8
9
10
11
package com.example.mobicrackndk;

/* loaded from: classes.dex */
public class Calc {
public static String key;

public static void calcKey() {
StringBuffer sb = new StringBuffer("c7^WVHZ,");
key = sb.reverse().toString();
}
}

key=",ZHVW^7c"

所以写个脚本即可

1
2
3
4
5
6
7
8
9
10
11
12
13
public class My {
public static void main(String[] args) {
String key = "QflMn`fH,ZHVW^7c";
char[] chars = key.toCharArray();
StringBuilder result = new StringBuilder();

for (int i = 0; i < chars.length; i++) {
result.append((char)(chars[i] + i)); // 每个字符减去索引值
}

System.out.println(result.toString());
}
}

但是wp说这是错误的

发现是在_init_my中存在对seed的修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
size_t _init_my()
{
size_t i; // r7
size_t result; // r0

for ( i = 0; ; ++i )
{
result = strlen(seed[0]);
if ( i >= result )
break;
t[i] = seed[0][i] - 3;
}
seed[0] = t;
byte_4038 = 0;
return result;
}

都减了3,对脚本进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class My {
public static void main(String[] args) {
String key = "QflMn`fH,ZHVW^7c";
char[] chars = key.toCharArray();
StringBuilder result = new StringBuilder();

for (int i = 0; i < chars.length; i++) {
if(i<8){
chars[i]-=3;
}
result.append((char)(chars[i] + i)); // 每个字符减去索引值
}

System.out.println(result.toString());
}
}
1
NdkMobiL4cRackEr

成功

静态分析综合题目

2017 ISCC Crackone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.editFlag = (EditText) findViewById(R.id.textView3);
this.button = (Button) findViewById(R.id.button);
this.button.setOnClickListener(new View.OnClickListener() { // from class: org.isclab.iscc.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
String flag = Digest.encode(MainActivity.this.editFlag.getText().toString()).trim();
Log.i("ISCC", flag);
int result = MainActivity.this.checkFlag(flag.getBytes(), flag.length());
if (result == 1) {
Toast.makeText(MainActivity.this, "Flag验证成功!", 0).show();
} else {
Toast.makeText(MainActivity.this, "Flag验证失败!", 0).show();
}
}
});
}

checkFlag为核心函数

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
_BOOL4 __cdecl native_checkFlag(JNIEnv *a1, int a2, void *usrinput, int size)
{
char *usrinput_arr; // ebp
int idx; // eax
char *end; // edx
char tmp; // [esp+2Bh] [ebp-21h]
jbyte *src; // [esp+2Ch] [ebp-20h]

src = (jbyte *)malloc(size);
(*a1)->GetByteArrayRegion(a1, usrinput, 0, size, src);
usrinput_arr = (char *)malloc(size + 1);
memset(usrinput_arr, 0, size + 1);
memcpy(usrinput_arr, src, size);
if ( size / 2 > 0 )
{
idx = 0;
end = &usrinput_arr[size];
do
{
tmp = usrinput_arr[idx] - 5;
usrinput_arr[idx++] = *(end - 1);
*--end = tmp;
}
while ( idx != size / 2 );
}
usrinput_arr[size] = 0;
free(usrinput_arr);
free(src);
return strcmp(usrinput_arr, "=0HWYl1SE5UQWFfN?I+PEo.UcshU") == 0;
}

逆向得到脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class My {
public static void main(String[] args) {
StringBuffer key = new StringBuffer("=0HWYl1SE5UQWFfN?I+PEo.UcshU");
String re_key=key.reverse().toString();
char[] chars = re_key.toCharArray();
int idx=re_key.length();
StringBuffer result=new StringBuffer();
for(int i=0; i<idx;i++)
{
if(i<idx/2)
{
result.append((char)(chars[i]+5));
continue;
}
result.append(chars[i]);
}
System.out.println(result);
}
}

1
ZmxhZ3tJU0NDSkFWQU5ES1lYWH0=

但是在进入checkFlag之前,还对其进行了编码

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
package org.isclab.iscc;

/* loaded from: classes.dex */
public class Digest {
private static final String TAG = "Util/Digest";
private static String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

public static String encode(String srcStr) {
if (srcStr != null && srcStr.length() != 0) {
char[] srcStrCh = srcStr.toCharArray();
StringBuilder asciiBinStrB = new StringBuilder();
for (char c : srcStrCh) {
String asciiBin = Integer.toBinaryString(c);
while (asciiBin.length() < 8) {
asciiBin = "0" + asciiBin;
}
asciiBinStrB.append(asciiBin);
}
while (asciiBinStrB.length() % 6 != 0) {
asciiBinStrB.append("0");
}
String asciiBinStr = String.valueOf(asciiBinStrB);
char[] codeCh = new char[asciiBinStr.length() / 6];
for (int i = 0; i < codeCh.length; i++) {
int index = Integer.parseInt(asciiBinStr.substring(0, 6), 2);
asciiBinStr = asciiBinStr.substring(6);
codeCh[i] = str.charAt(index);
}
StringBuilder code = new StringBuilder(String.valueOf(codeCh));
if (srcStr.length() % 3 == 1) {
code.append("==");
} else if (srcStr.length() % 3 == 2) {
code.append("=");
}
for (int i2 = 76; i2 < code.length(); i2 += 76) {
code.insert(i2, "\r\n");
}
code.append("\r\n");
return String.valueOf(code);
}
return srcStr;
}
}

简单分析,可以发现是标准base64,就不用自己写脚本了,cyberchef得到

1
flag{ISCCJAVANDKYXX}

成功

2018 强网杯 picture lock

在android中,onActivityResult是一个android.app.Activity类预先定义好的回调函数,只有当内部方法调用startActivityForResult(Intent intent, int requestCode)来启动另一个Activity的时候才会调用这个回调函数,被启动的Activity必须在结束前调用setResult来设置返回结果和finish来关闭自己,关闭完之后原Activity就会调用onActivityResult方法,并将之前启动的Activity的resultCode和intent以及一开始调用新Activity传入的requestCode一起传入onActivityResult

MediaStore内容提供器中,_data代表绝对路径

逆向分析:

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
private String j() {
try { // 获取该包的签名信息
Signature[] signatureArr = getPackageManager().getPackageInfo("com.a.sample.picturelock", 64).signatures; // 获取该包的签名信息
MessageDigest messageDigest = MessageDigest.getInstance("MD5"); // 初始化MD5计算器
for (Signature signature : signatureArr) {
messageDigest.update(signature.toByteArray()); // 遍历所有签名并更新哈希值
}
byte[] digest = messageDigest.digest(); // 最终md5哈希
StringBuilder sb = new StringBuilder();
for (byte b : digest) { // 将字节数组转换为十六进制字符串
int i = b & 255;
if (i < 16) {
sb.append("0");
}
sb.append(Integer.toHexString(i));
}
return sb.toString(); // 返回string
} catch (PackageManager.NameNotFoundException | NoSuchAlgorithmException e) {
return "";
}
}

@Override // android.support.v4.a.k, android.app.Activity
protected void onActivityResult(int i, int i2, Intent intent) {
super.onActivityResult(i, i2, intent);
switch (i) {
case 1:
if (intent != null) {
String[] strArr = {"_data"};
Cursor query = getContentResolver().query(intent.getData(), strArr, null, null, null);
query.moveToFirst();
String file_path = query.getString(query.getColumnIndex(strArr[0])); // 源文件路径
query.close();
enc(file_path, getFilesDir().getAbsolutePath() + file_path.substring(file_path.lastIndexOf("/")) + ".lock", j());
i();
Toast.makeText(this, String.format("%s encrypting", file_path), 1).show();
break;
}
break;
}
}

@Override // android.support.v7.app.c, android.support.v4.a.k, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_main);
findViewById(R.id.encrypt).setOnClickListener(new View.OnClickListener() { // from class: com.a.sample.picturelock.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
if (a.a(this, "android.permission.WRITE_EXTERNAL_STORAGE") != 0) { // 判断是否有写入外部存储权限
android.support.v4.a.a.a((Activity) this, new String[]{"android.permission.WRITE_EXTERNAL_STORAGE"}, 1);
return;
}
Intent intent = new Intent("android.intent.action.PICK", (Uri) null); // 动作
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); // 间接调用了MediaStore内容提供器
MainActivity.this.startActivityForResult(intent, 1);
}
});
findViewById(R.id.refresh).setOnClickListener(new View.OnClickListener() { // from class: com.a.sample.picturelock.MainActivity.2
@Override // android.view.View.OnClickListener
public void onClick(View view) {
MainActivity.this.i();
}
});
i();
}

看native层的enc函数 参数(文件的真实路径,目标路径,文件签名的md5),第三个参数可以hook看一眼即可得到:f8c49056e4ccf9a11e090eaf471f418d

不hook也可以直接得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS D:\android_re\CTF\wiki\picture> apksigner verify --verbose --print-certs picturelock.apk
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): false
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
Signer #1 certificate DN: CN=a, OU=b, O=c, L=d, ST=e, C=ff
Signer #1 certificate SHA-256 digest: ba12c13fd60e0def17ae3aee4e6a816782d0367ff02e37ccad5d6e86870c8e38
Signer #1 certificate SHA-1 digest: 48e7045ee60d9d8a257c5275e3650609a5cca13e
Signer #1 certificate MD5 digest: f8c49056e4ccf9a11e090eaf471f418d
Signer #1 key algorithm: RSA
Signer #1 key size (bits): 2048
Signer #1 public key SHA-256 digest: 4de55872bc804bf5fccc61c6e42377b24cd6eefc183a800c5807f759f7f9796d
Signer #1 public key SHA-1 digest: dba8196f6f49a1d1291e4c03102bc7225b6f1f25
Signer #1 public key MD5 digest: d246e0ae52179dfe9daca889ee3133ef

看到Signer #1 certificate MD5 digest: f8c49056e4ccf9a11e090eaf471f418d就能得到

so程序的enc程序在调试的时候,老是出现错误,因此就在此留下一个小坑吧,等我变强之后再回来搞定😎

android逆向之动态调试

java层调试

工具:android studio、apktools、smalidea、已经root的手机

首先apktools反编译apk为output

1
apktool d app.apk -o output

然后

1
2
3
4
5
adb shell #adb进入命令行模式
su #切换至超级用户
magisk resetprop ro.debuggable 1
stop
start #一定要通过该方式重启

设置手机的ro.debuggable为1,这样任何程序都可以在我们的手机上进行调试(但是缺点就是每次重启都会失效,因此在启动之后都要设置一次

1
adb shell ps

找到对应的调试进程

1
u0_a137       9261  7805 1120068  56804 SyS_epoll_wait      0 S com.a.sample.picturelock

PID为9261

因此设置

1
2
PS C:\Users\Lenovo> adb forward tcp:8700 jdwp:9261
8700

这个8700是android studio中设置的

来到android studio中

image-20250414101310778

设置如上的部分,output是apktool反编译apk为smali的文件,其中output中的smali文件夹要将其Mark为Sources Root权限

之后直接到output中进行调试即可

原生层调试

1
adb shell am start -D -n com.a.sample.picturelock/.MainActivity

然后在adb shell中push进去ida/dbgsrv下对应的server,运行,自动监听 23946 port

然后

1
adb forward tcp:23946 tcp:23946

将PC端的端口转发到手机上对应的服务端口

之后ida

Debug->attach->localhost:23946

选中对应的服务即成功attack上

然后android studio也attach上java层即可