0%

逆向学习 0x0A frida labs练习

frida0x1

image-20241121204217780

先进行反编译,找到 com.ad2001.frida0x1.MainActivity 方法了

解读一下代码

有一个生成100以内的随机数的方法,还有个一个check方法

内涵一个判断,先hook产生随机数的方法,固定这个i 的值,然后输入正确的i2值

1
if ((i * 2) + 4 == i2)

image-20241121204208922

先hook一下 check 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java.perform(function() {
// 输出加载的类名
// var classes = Java.enumerateLoadedClassesSync();
// for (var i = 0; i < classes.length; i++) {
// var className = classes[i];
// console.log("className: " + className);
// }



let MainActivity = Java.use("com.ad2001.frida0x1.MainActivity");

MainActivity["get_random"].implementation = function () {
console.log(`MainActivity.get_random is called`);
return 1;
};

MainActivity["check"].implementation = function (i, i2) {
console.log(`MainActivity.check is called: i=${i}, i2=${i2}`);
this["check"](i, i2);
};
});

看输出 i 的值为17,返回1之后还是17,说明这个方法没有执行,需要重启hook,问题不大,现在输入38 (17*2+4)就完成这个了,得到flag

1
FRIDA{BABY_HOOK_OX1}

image-20241121205923154

猜测正确,每次重启会随机一个数,把这个数hook了,就固定了密码

image-20241121210327953

再来

frida0x2

反编译 AES加密突脸了

key是 “HILLBILLWILLBINN” .getByte() 这个字节数组

iv值是0字节数组,是个空数组

2 表示解密方法

解密的数据是 “q7mBQegjhpfIAr0OgfLvH0t/D0Xi0ieG0vd+8ZVW+b4=” 进行base64解码之后

image-20241121210612960

难说,直接薄纱了

HILLBILLWILLBINN 用 hex编码一下,iv写32位0,解了

image-20241121213348207

再用JS实现一下

Base64解密之后是原始的数据格式,在网站上使用Raw来接收,在cryptoJS中选择 WordArray 这个类似于字节数组的格式来接收参数,解密成功,得到flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cryptoJS = require('crypto-js');

// AES解密
// key值为字节数组
var key = cryptoJS.enc.Hex.parse(Buffer.from("HILLBILLWILLBINN", "utf8").toString("hex"))
// iv值是0字节数组
var iv = cryptoJS.enc.Hex.parse("00000000000000000000000000000000")
// 待解密的密文
var decodeBase64 = Buffer.from("q7mBQegjhpfIAr0OgfLvH0t/D0Xi0ieG0vd+8ZVW+b4=", "base64")
var encrypted = cryptoJS.lib.WordArray.create(decodeBase64)

var cfg = {
iv: iv,
mode: cryptoJS.mode.CBC,
padding: cryptoJS.pad.Pkcs7
}

var decrypted = cryptoJS.AES.decrypt({ciphertext: encrypted}, key, cfg);

console.log(decrypted.toString(cryptoJS.enc.Utf8));

image-20241122102209105

使用frida的主动调用来实现,这个才是正题

image-20241122102745827

因为这个方法找不到实现,现在主动调用这个方法

1
2
3
4
5
Java.perform(function() {
// 主动调用
var a = Java.use("com.ad2001.frida0x2.MainActivity");
a.get_flag(4919)
});

这种主动调用的方式很神奇啊,只有这样可以

image-20241122110030899

我试了其他的方法

这个方法不能够使用 -f 的方式,要使用 -F 直接调用

在程序启动之后再主动调用方法,防止调用完,程序启动重新覆盖显示

1
2
3
4
Java.perform(function() {
var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity");
MainActivity.get_flag(4919);
});
1
2
3
4
5
6
7
8
9
10
Java.perform(function() {
//主动调用
Java.choose("com.ad2001.frida0x2.MainActivity", {
onMatch: function(instance) {
var a = 4919;
instance.get_flag(a);
},
onComplete: function() {}
})
});

image-20241122111056484

frida0x3

看代码,一个点击方法,判断code是否等于512,但是increase方法没有被调用,说明光点是肯定不行的,hook将数值改成512,然后通过解密方法获取flag

image-20241122113325199

主动更改数值之后,一点就出

1
2
3
4
Java.perform(function() {
let Checker = Java.use("com.ad2001.frida0x3.Checker");
Checker.code.value = 512;
})

image-20241122113847909

还可以看源码去网站解密

glass123 进行hex解密

没有添加iv值,更改位ECB模式,base64解码之后的数据类型为Raw

good

image-20241122113253739

就不用JS复现了,需要其他的库,流程和之前的AES解密差不多

frida0x4

image-20241122165516120

主动调用这个方法,这里踩了坑了,我试了两种主动调用的方法均没有输出

第一、Java.use指定已经加载的类,Check.get_flag(1337); 这个形式只能主动调用静态方法,没有实例化的对象需要先实例化对象

第二、使用 Java.choose 时,确保该类在当前上下文中有实例存在。如果当前没有可用的实例,onMatch 函数将不会被调用,从而没有输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java.perform(function() {
// let Check = Java.use("com.ad2001.frida0x4.Check");
// var result = Check.get_flag(1337);
// console.log(result);


// Java.choose("com.ad2001.frida0x4.Check", {
// onMatch: function(instance) {
// console.log("Found instance: " + instance);
// var a = 1337;
// var result = instance.get_flag(a);
// console.log(result);
// },
// onComplete: function() {}
// })

var MyClass = Java.use('com.ad2001.frida0x4.Check');
// 创建类的实例
var myClassInstance = MyClass.$new();
var result = myClassInstance.get_flag(1337);
console.log(result.toString());
})

先创建实例再主动调用,拿下flag

FRIDA{XORED_INSTANCE}

image-20241122170222663

打算用JS复现一下,先解读代码,创建一个byte数组,长度为44

循环数组,对 I]FKNtW@]JKPFA\\[NALJr 这个字符串转字节数组的每一个元素和15(1111)进行异或操作,得到结果再使用hex解码返回字符串

异或,相同为0 ,不同为1

1
2
3
4
5
6
7
8
9
10
11
12
function decodeString(input) {
const hexBytes = new Array(input.length);
for (let i = 0; i < input.length; i++) {
// 将输入的字符串转成Unicode编码,异或操作后转换成字符
hexBytes[i] = String.fromCharCode(input.charCodeAt(i) ^ 15);
}
// 将字符合并成字符串返回
return hexBytes.join('');
}
const input = "I]FKNtW@]JKPFA\\[NALJr"
const output = decodeString(input);
console.log(output);

image-20241122171748312

frida0x5

反编译一下,真难绷

image-20241122172459141

又是一个主动调用的,直接解了算了

image-20241122172440823

JS

跟frida0x2的解密代码一模一样,就改一下key和密文

image-20241122172648168

frida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function() {
// var MainActivity = Java.use('com.ad2001.frida0x5.MainActivity');
// var myClass = MainActivity.$new();
// myClass.flag(1337);

// var MainActivity = Java.use('com.ad2001.frida0x5.MainActivity');
// MainActivity.flag(1337);

Java.choose("com.ad2001.frida0x5.MainActivity", {
onMatch: function(instance) {
var a = 1337;
instance.flag(a);
console.log("success");
},
onComplete: function() {}
})
})

image-20241122173739595

问题

这个时候我产生了一个问题,为什么同样是主动调用,在apk4中就不能使用 java.choose 要使用 $new(),而apk5中就不能使用 $new() ,而是使用Java.choose 呢

这个和是否在MainActivity类中和是否是静态方法有关,比如apk4,要主动调用的方法是非MainActivity类的非静态方法,这样的方法在Java中要调用还得new一下呢

1
2
Check ch = new Check();
String flag = ch.get_flag(1337);

那么就不难想象,frida的调用方法了

1
2
3
var Check = Java.use("com.ad2001.frida0x4.Check");
var ch = Check.$new();
var flag = ch.get_flag(1337);

使用Java.choose方法来调用的话,没有这个实例化对象的,因为这个类就完全是没有调用加载的类,没有任何一个地方调用他了,根本就没有创建,不存在这个实例,既不输出也不执行。

而面对MainActivity类中的非静态方法就不能使用 $new() 的方式了,会导致系统崩溃

6b6145653543c69ae5d635cd59f44e6

因为andriod的生命周期和线程规则,Android组件,如 Activity 的子类,依赖于应用程序上下文进行正确运行。在Frida中,您可能缺少必要的上下文。Android UI组件通常需要具有关联Looper的特定线程。如果涉及UI任务,请确保在具有活动Looper的主线程上执行。活动是较大的Android应用程序生命周期的一部分。创建MainActivity的实例可能需要应用处于特定状态,并且通过Frida管理整个生命周期可能并不直接。

所以说不能直接来 new MainActivity 这个类,因为在Android程序启动的时候,系统就会创建 MainActivity 的实例,这个时候就可以直接使用 Java.choose 来指定Java的实例来进行主动调用了

这种方式一般在非 MainActivity 类的,且有实例化对象的时候调用

9ea9386ea48d3a12881d6cdc6f95cda

这个时候是应该使用 Java.choose 的

1
2
3
4
5
6
7
Java.choose("", {
onMatch: function(instance) {
var result = instance.get_flag(1337);
console.log(result);
},
onComplete: function() {}
})

frida0x6

反编译看方法

image-20241124115843696

这个主动调用需要中西结合,使用 $new 来获取Checker 类的实例化对象,更改num1和num2的值,再用Java.choose 来获取MainActivity类的实例化对象,来主动调用 get_flag 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function() {
var Check = Java.use('com.ad2001.frida0x6.Checker');
var Checker = Check.$new();
Checker.num1.value = 1234;
Checker.num2.value = 4321;
var MainActivity = Java.use('com.ad2001.frida0x6.MainActivity');
Java.choose("com.ad2001.frida0x6.MainActivity", {
onMatch: function(instance) {
instance.get_flag(Checker);
},
onComplete: function() {}
})
})

image-20241124115829499

看代码是是一个AES的ECB加密

image-20241124120618975

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const cryptoJS = require('crypto-js');

function get_flag() {
var key = cryptoJS.enc.Hex.parse(Buffer.from("MySecureKey12345", "utf8").toString("hex"))
var decodeBase64 = Buffer.from("QQzMj/JNaTblEHnIzgJAQkvWJV2oK9G2/UmrCs85fog=", "base64")
var encrypted = cryptoJS.lib.WordArray.create(decodeBase64)
var cfg = {
mode: cryptoJS.mode.ECB,
padding: cryptoJS.pad.Pkcs7
}
var decrypted = cryptoJS.AES.decrypt({ciphertext: encrypted}, key, cfg);
console.log(decrypted.toString(cryptoJS.enc.Utf8));
}

get_flag();

image-20241124120926563

frida0x7

这个和上面的区别就是,这里不需要主动调用了,这里初始化了Checker这个类,传入了两个值,然后调用了 flag这个方法,现在需要hook Checker 这个类的构建方法,将传入值改变即可

image-20241124121316633image-20241124200221798

1
2
3
4
5
6
7
8
Java.perform(function() {
var Check = Java.use('com.ad2001.frida0x7.Checker');
Check.$init.overload('int', 'int').implementation = function(a, b) {
var a1 = 513;
var a2 = 513;
return this.$init(a1, a2);
};
})

出结果

image-20241124200209234

JS解密方法和0x6一模一样

image-20241124200351743

frida0x8

难度飙升了,进so层了,可以看到关键的一个方法放到了SO层,找so文件反编译

image-20241124200843929

找到这个方法

image-20241124202410190

因为的对so层也了解不多,就逐行解读一下,就当加深印象了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_BOOL8 __fastcall Java_com_ad2001_frida0x8_MainActivity_cmpstr(__int64 a1, __int64 a2, __int64 a3)
{
int v4; // [rsp+30h] [rbp-C0h]
int i; // [rsp+34h] [rbp-BCh]
char *s1; // [rsp+40h] [rbp-B0h]
char s2[104]; // [rsp+80h] [rbp-70h] BYREF
unsigned __int64 v9; // [rsp+E8h] [rbp-8h]

v9 = __readfsqword('(');
s1 = (char *)_JNIEnv::GetStringUTFChars(a1, a3, 0LL);
for ( i = 0; i < (unsigned __int64)__strlen_chk("GSJEB|OBUJWF`MBOE~", -1LL); ++i )
s2[i] = aGsjebObujwfMbo[i] - 1;
s2[__strlen_chk("GSJEB|OBUJWF`MBOE~", -1LL)] = 0;
v4 = strcmp(s1, s2);
__android_log_print(3LL, "input ", &unk_6B0, s1);
__android_log_print(3LL, "Password", &unk_6B0, s2);
_JNIEnv::ReleaseStringUTFChars(a1, a3, s1);
return v4 == 0;
}

GetStringUTFChars 是JNI中的一个API(可以这样理解),这个 从 Java 字符串中获取 UTF-8 编码的字符数组。,也就是说 s1 存储的是我们输入框输入的信息

for ( i = 0; i < (unsigned __int64)__strlen_chk(“GSJEB|OBUJWF`MBOE~”, -1LL); ++i )
s2[i] = aGsjebObujwfMbo[i] - 1;

这里是利用了一个神秘的方法 aGsjebObujwfMbo 来处理出flag ,将flag赋值给S2

使用 strcmp 方法比较两个字符串,输出两句,然后将s1转换成Java的字符串,然后返回0 ,无论是否正确都返回0 ,这个时候就需要hook strcmp 方法了

沉淀一下,继续hook

hook一下 strcmp 方法,这个是系统方法,通过计算地址值进行hook

1
2
3
4
5
6
7
8
9
Interceptor.attach(Module.findBaseAddress('libfrida0x8.so').add(0x9DD), {
onEnter: function(args) {
console.log('onEnter: ', hexdump(args[0]))
console.log('onEnter: ', hexdump(args[1]))
},
onLeave: function(retval) {
console.log('onLeave: ', retval)
}
});

tab键查看地址

image-20250322222804674

直接打印出来 flag 了

image-20250322222712335

1
FRIDA{NATIVE_LAND}

frida0x9

image-20250322223611434

貌似只需要让这个so函数的返回值为1337即可,根本不需要so层的hook,直接Java层,改一下return结果即可

1
2
3
4
5
6
Java.perform(function () {
let MainActivity = Java.use("com.ad2001.a0x9.MainActivity");
MainActivity["check_flag"].implementation = function () {
return 1337;
};
})

image-20250322223545537

1
FRIDA{NATIVE_LAND_0X2}

静态分析直接梭也行

image-20250323085023620

1
2
3
4
5
6
7
8
9
10
11
12
const CryptoJS = require("crypto-js");
function AES_encrypt() {
let key = CryptoJS.enc.Utf8.parse("3000300030003003");
let data = CryptoJS.enc.Base64.parse("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0=")

var decrypted = CryptoJS.AES.decrypt({ciphertext: data}, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
}).toString(CryptoJS.enc.Utf8);
console.log(decrypted);
return decrypted;
}

frida0xA

进so看看

image-20250323091841320

好像也没有什么东西,这个so方法就是将这个东西设置到桌面显示上,返回值就是这个 hello hackers

image-20250323092759751

翻了翻还有一个get_flag 方法,主动调用这个方法

image-20250323091825842

so层的主动调用超纲了,百度一下

1
2
3
4
5
6
7
Java.perform(function () {
var addr = Module.findExportByName("libfrida0xa.so", "_Z8get_flagii")
console.log(`addr=${addr}`)
let get_flag_pointer = new NativePointer(addr)
let get_flag = new NativeFunction(get_flag_pointer, 'int', ['int', 'int'])
get_flag(2,1)
})

再看一下代码,是输出成日志的形式,不是直接显示的,还需要查看一下日志

因为使用了 flag 标签,过滤一下

1
adb logcat -s "FLAG"

结果如下

1
FRIDA{DONT_CALL_ME}

image-20250323095014516

也可以根据这个代码直接解密,源代码是将每个字符偏移,将字符转换成ASCCII码的形式进行计算还原

1
2
3
原始字符:F P E > 9 q 8 A > B K - ) 2 0 A - # Y
偏移计算:0,2,4,6,8,...(每个字符的偏移量为 2*i)
最终结果:F+0, P+2, E+4, >+6, 9+8, q+10, 8+12, A+14, >+16, B+18, K+20, -+22, )+24, 2+26, 0+28, A+30, -+32, #+34, Y+36

frida 0xB

java层没东西,直接进so。end

没找到,找博客:https://www.cnblogs.com/WXjzc/p/18231528

直接进去没东西,原因是这里有一个永假判断,跳转到其他地址了,感觉比较偏CTF风格了

image-20250323100554837

这里先将 var_24 赋值成 0xDEADBEEF

然后再判断 var_24 是否等于 0x539

不相等跳转到错误分支 loc_171A6 所有啥也没有

image-20250323100756720

在IDA中右键jnz设置成nop,先看一下代码

image-20250323111108075

这个时候就有两种修改方式了,第一种是将 jnz loc_171A6 修改成 jz loc_171A6

jnz 是结果为0(否)跳转指令,jz 是结果为1(是)跳转指令,改成jz就不会出现跳转

或者是将这个指令Nop掉,不进行跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.perform(function() {
var funcAddr = Module.findExportByName("libfrida0xb.so", 'Java_com_ad2001_frida0xb_MainActivity_getFlag');
var opaddr = funcAddr.add(0x1E)

// 模拟器行为,如果是真机应该是 Arm64Writer
var writer = new X86Writer(opaddr);
Memory.protect(opaddr, 0x10, 'rwx');
try {
writer.puNop();
writer.flush();
} catch (error) {
writer.dispose();
}
})

image-20250323125423912

jnz -> jz

jnz的机器码是 0F 85,jz的机器码是 0F 84

需要将这个改为 0F 84

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function() {
var funcAddr = Module.findBaseAddress("libfrida0xb.so");
var opaddr = funcAddr.add(0x170CE)

console.log(opaddr);
var writer = new X86Writer(opaddr);
Memory.protect(opaddr, 0x1000, 'rwx');
try {
writer.putU8(0x0F)
writer.putU8(0x84)
writer.flush();
} catch (error) {
console.log( "Error:",error);
} finally {
writer.dispose();
}
})

image-20250323130802111

1
FRIDA{NATIVE_HACKER}

第二种就是令 var_24 等于 0x539 ,使跳转指令不成立

C7 45 DC EF BE AD DE

后四个机器码是赋值的小端字节序,将这个改成 39 05 00 00,就可以使下方判断成立

image-20250323131921727

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function() {
var funcAddr = Module.findBaseAddress("libfrida0xb.so");
var opaddr = funcAddr.add(0x170CA)

console.log(opaddr);
var writer = new X86Writer(opaddr);
Memory.protect(opaddr, 0x1000, 'rwx');
try {
writer.putU8(0xEF)
writer.putU8(0xBE)
writer.putU8(0xAD)
writer.putU8(0xDE)
writer.flush();
} catch (error) {
console.log( "Error:",error);
} finally {
writer.dispose();
}
})

image-20250323133958878

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function() {
var funcAddr = Module.findBaseAddress("libfrida0xb.so");
var opaddr = funcAddr.add(0x170C3)

console.log(opaddr);
var writer = new X86Writer(opaddr);
Memory.protect(opaddr, 0x1000, 'rwx');
try {
writer.putU8(0x39)
writer.putU8(0x05)
writer.putU8(0x00)
writer.putU8(0x00)
writer.flush();
} catch (error) {
console.log( "Error:",error);
} finally {
writer.dispose();
}
})

image-20250323134124190

END