Bugku Android逆向合集

记录一下在Bugku做的一些Moblie题。

1. signin(Java hook)

运行一下程序。

加载进jeb分析:

那如何获取到flag呢?可以用hook。hook一个方法需要知道:

  1. 方法的包名和类名:re.sdnisc2018.sdnisc_apk1.MainActivity
  2. 方法名:getFlag
  3. 方法的参数类型:无

另外还要知道程序的包名:re.sdnisc2018.sdnisc_apk1

Logcat返回getFlag的返回值:

随便找个可以逆序文字的网站,再拿去Base64解码,得到flag:

1
2
3
4
5
#逆序后
ZmxhZ3tIZXIzX2k1X3kwdXJfZjFhZ18zOWZiY199

#Base64解码后
flag{Her3_i5_y0ur_f1ag_39fbc_}

2. mobile1(算法分析)

运行程序。

载入jeb分析Java伪代码:

1
2
3
4
5
#Tenshine的MD5加密
b9c77224ff234f27ac6badf83b855c76

#取MD5的奇数位
flag{bc72f242a6af3857}

3. mobile2(XML文件)

APK逆向-2,mobile2下载下来是一个ZIP文件,一样的。

4. Timer(算法分析&Smali文件修改)

拿去AK看了一下,没有允许动态调试。一般CTF的题如果没有动态调试,说明这道题不是考动态调试so文件的,而且这道题没有输入,断也不知道在哪里断下来,断下来干嘛。那如果so文件拿去IDA看,没有Log语句的话也挺难静态分析的。所以这道题很有可能是直接修改smali文件。

先jeb查看Java源码,因为重点不是so文件,所以可以暂时忽略它了:

因为最后打印flag是将k作为实参传入native方法的,而k是用is2方法求出来的,所以将is2方法运行一下,求出k的值。

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
public class demo{
public static boolean is2(int arg4) {
boolean v1 = true;
if(arg4 > 3) {
if(arg4 % 2 != 0 && arg4 % 3 != 0) {
int v0 = 5;
while(true) {
if(v0 * v0 <= arg4) {
if(arg4 % v0 != 0 && arg4 % (v0 + 2) != 0) {
v0 += 6;
continue;
}
return false;
}
else {
return v1;
}
}
}
v1 = false;
}
else if(arg4 <= 1) {
v1 = false;
}
return v1;
}
public static void main(String args[]){
int time=200000;
int k=0;
while(time>0){
if(is2(time)){
k+=100;
}else{
k--;
}
time--;
}
System.out.println(k);
}
}
/**
1616384
*/

在smali文件修改传入native方法的k的值为1616384,还要将输出flag的条件语句修改为小于0。

保存,编译,安装,运行。

1
2
alictf{Y0vAr3TimerMa3te7}
flag{Y0vAr3TimerMa3te7}

5. First_Mobile(算法分析)

在模拟器上运行不了,可能是架构的问题,但这题不需要在页面上显示什么,所以运行不了也没事。

载入jeb分析:

调整一下encode类:

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
class encode {
private static byte[] b;

static {
encode.b = new byte[]{23, 22, 26, 26, 25, 25, 25, 26, 27, 28, 30, 30, 29, 30, 32, 32};
}

public encode() {
super();
}

public static void check(String arg7) {
int v6 = 16;
byte[] v1 = arg7.getBytes();
byte[] v3 = new byte[v6];
int i,j;
byte[] v5 = new byte[v6];
for(j=0; j<v1.length; j++)
{
for(i = 0; i < v6; ++i) {
v3[i] = ((byte)((v1[j] + encode.b[i]) % 61));
v3[i] = ((byte)(v3[i] * 2 - i));
if (v3[i] == v1[j])
{
v5[i] = v3[i];

}
}
}
String str = new String (v5);
System.out.println(str);
}
}
public class AES_128
{

public static void main(String[] args)
{
encode enc = new encode();
String arg7 = "ABCDEFGHJIKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890{}_";
enc.check(arg7);
}
}
/**
LOHILMNMLKHILKHI
*/

所以flag为:

1
XMAN{LOHILMNMLKHILKHI}

6. easy_100(AES加密)

虽然在模拟器不能运行,但没root的手机也可以从电脑上安装app。

在AK中看到没有so文件,也不用动态调试。jeb分析,因为有混淆,所以看起来还挺费力。

将图片用Winhex打开,144=90h,所以v=”this_is_the_key.”。

密文与数组比较。现在理一下思路:key经过小端存储作为密钥,输入的文本串经AES_ECB模式和密钥加密后得到需要比较的数组。所以我们现在想逆向回去找文本串,需要将key转换成密钥,再利用ECB解密数组,得到flag。编写脚本:

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
package v5le0n9;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

public class demo {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
StringBuilder v1 = new StringBuilder();
int v0_1;
String v0_2;
String arg4 = "this_is_the_key."; //在winhex中发现
for(v0_1 = 0; v0_1 < arg4.length(); v0_1 += 2) { //对KEY进行二次变换,这是有类c中的函数分析得到的
v1.append(arg4.charAt(v0_1 + 1));
v1.append(arg4.charAt(v0_1));
}
v0_2 = v1.toString();
byte key[] = v0_2.getBytes(); //转换成Bytes数组
SecretKeySpec a;
Cipher b;
a = new SecretKeySpec(key, "AES");
b = Cipher.getInstance("AES/ECB/PKCS5Padding");
byte flag[] = {21, -93, -68, -94, 86, 117, -19, -68,
-92, 33, 50, 118, 16, 13, 1, -15, -13, 3, 4, 103, -18, 81, 30, 68, 54, -93, 44, -23,
93, 98, 5, 59};
b.init(Cipher.DECRYPT_MODE,a); //这里注意前面的参数,选择相应的解密参数

String AES_decode = new String(b.doFinal(flag),"utf-8");
System.out.println(AES_decode);
}
}
/**
LCTF{1t's_rea1ly_an_ea3y_ap4}
*/

7. HelloSmali2(送分)

下载下来一个smali文件和一个java文件,将java文件运行一下就有答案。所以smali文件并没有什么用?

1
2
javac XMan.java//生成XMan.class文件
java XMan//执行class文件
1
2
eM_5m4Li_i4_Ea5y
XMAN{eM_5m4Li_i4_Ea5y}

8. LoopAndLoop(算法分析)

顺着它的逻辑编写代码,循环几次后发现规律,最后得到flag。

1
alictf{Jan6N100p3r}

9. SafeBox(算法分析)

这有两个Activity,两个Activity的算法有点差别,将两个算法都算一遍就知道谁是正确flag了。

而且题目描述说明flag内容只包含[a-zA-Z0-9],所以可以轻易得出正确flag。

10. baby unity3d(unity & Native hook)

unity3d手游逆向

Android逆向笔记-Unity3D逆向一般思路(静态分析)

根据bugku baby unity3d分析思路,在Magisk中启动Zygisk,使用Zygisk-Il2CppDumper将生成的ZIP包刷入Magisk后重启,启动游戏,得到dump.cs文件。

CS文件其实是C#源文件,可以使用任何编辑器打开。dump.cs中有上万行代码,需要在这些代码中寻找关键函数并不容易。可以根据Assembly-CSharp.dll的index进行定位。

libil2cpp.so载入IDA,定位到相关虚拟地址。将IDA中的函数名修改为dump.cs中的函数名以便理解。

AESEncrypt()的password和iv,op_Equality()的dword_69B7F0都很难在IDA中得到,可以通过Native hook将这些参数打印出来,点击程序中的Check按钮就会调用到这两个函数。

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
function hook_flag(){
var soAddr = Module.findBaseAddress("libil2cpp.so");
console.log("soAddr => " + soAddr);

//根据函数偏移调用函数
var AESEncryptAddr = soAddr.add(0x518b54);
console.log("AESEncryptAddr => " + AESEncryptAddr);

Interceptor.attach(AESEncryptAddr, {
onEnter:function(args){
console.log("AESEncrypt addr => ", args[0]);
console.log("text => ", hexdump(args[1]));
console.log("password => ", hexdump(args[2]));
console.log("iv => ", hexdump(args[3]));
},onLeave:function(retval){
console.log("AESEncrypt retval => ",hexdump(retval));
return retval;
}
})

var op_EqualityAddr = soAddr.add(0x7d644);
console.log("op_EqualityAddr => " + op_EqualityAddr);

Interceptor.attach(op_EqualityAddr, {
onEnter:function(args){
console.log("op_Equality addr => ", args[0]);
console.log("AESEncrypt retval => ", hexdump(args[1]));
console.log("dword_69B7F0 => ", hexdump(args[2]));
},onLeave:function(retval){
console.log("op_Equality retval => ",retval);
return retval;
}
})
}
setImmediate(hook_flag)

可以得到password为91c775fa0f6a1cba,iv为58f3a445939aeb79。dword_69B7F0为w0ZyUZAHhn16/MRWie63lK+PuVpZObu/NpQ/E/ucplc=,应该是将密文再进行了一次Base64编码。

1
N1CTF{h4ppy_W1TH_1l2cpp}

如果不清楚AESEncrypt()中还进行了什么奇怪的算法,使用在线解密工具解密不出时,可以主动调用libil2cpp.so中的AESDecrypt()。但我貌似没这能耐啊不会弄。

11. easyeasy-200(模拟器检测)

check()方法中调用了Native方法checkEmulator(),看名字就知道是模拟器检测,还调用了checkPasswd(),这个才是核心函数。

将So文件载入IDA,查看.init_array

查看JNI_OnLoad()

相关检测可以看Tide安全团队——动态防护技术Android模拟器检测及对抗方法Android逆向之旅—运行时修改内存中的Dalvik指令来改变代码逻辑Android常见风控检测位置

最后看Native层函数:

.init_array中的secret是pass逆序后的Base64编码,拿去解码逆序回来即可。由于Base64字符表中没有“.”,猜测是占位符。解出没有前后缀的flag,加上前后缀提交。

1
2
3
iwantashellbecauseidonthaveitttt

flag{iwantashellbecauseidonthaveitttt}

12. exaycrack-100(算法分析 & RC4)

进入So文件查看parseText()函数逻辑:

1
2
3
It_s_a_easyCrack_for_beginners

flag{It_s_a_easyCrack_for_beginners}

13. 汤姆的苹果

AsyncTask解析

aVar这个异步任务有两个处理器(Handler),并在onClick()函数中完成赋值。

对于输入的字符串,作为b.b.a.b()的参数传入:

再回到MainActivity,执行aVar.execute("99");。但在执行这条语句前,异步任务会先执行doInBackground()方法,其中调用了b.b.a.b.a()方法,使用到了经过变换后的输入。

继而再执行onPostExecute(),传递消息内容为999,把整个处理器c传过去,由处理器f728b发送。

对每个字符进行匹配,如果全都匹配成功,由处理器c发送内容99,否则发送内容97。当发送的消息内容为99时,显示“Right”。

点击按钮后,将b.b.a.b类的静态成员变量hook出来。

由于输入字符串长度为4,且f729a的可见字符长度也是4。可以根据cArr推测输入字符串长度为24。

使用3个反编译器,代码都不同,而且改来改去都不对,真是服了。

14. LittleRotatorGame

全部逻辑都在So层中。进行了OLLVM保护。