challenge 0x08

image.png

image.png

button.setOnClickListener(new View.OnClickListener() { // from class: com.ad2001.frida0x8.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View v) {
String ip = MainActivity.this.edt.getText().toString();
int res = MainActivity.this.cmpstr(ip);
if (res == 1) {
Toast.makeText(MainActivity.this, "YEY YOU GOT THE FLAG " + ip, 1).show();
} else {
Toast.makeText(MainActivity.this, "TRY AGAIN", 1).show();
}
}
});
  • 首先onClick函数,将用户输入作为参数,传入了cmpstr 方法中,并返回了一个整数值
  • 如果返回值为1,则输出用户输入的文本内容,否则返回 “TRY AGAIN”

cmpstr 方法为native中定义的,函数接受一个字符串作为参数并返回一个整数

image.png

直接解压apk文件,或使用apktool等反编译工具,在/resources/lib/目录下存放了对应的.so文件

image.png

大多数物理设备都基于 ARM64 架构,这块使用模拟器打开apk,所以使用 x86 库。但不会有太大区别。

image.png

可以使用IDA等逆向汇编工具打开so文件,这块使用一个新工具:Ghidra

具体使用教程可参考:https://www.youtube.com/watch?v=fTGTnrgjuGA

1、将文件导入,单击 Yes 并等待分析完成。

点击Export查看导出函数,找到了cmpstr 函数:

image.png

2、这时候可以看到对应的汇编和反汇编代码了,代码能力不行,直接AI即可

bool Java_com_ad2001_frida0x8_MainActivity_cmpstr(_JNIEnv **env, undefined8 param_2, undefined8 str)
{
int iVar1;
char *__s1;
ulong uVar2;
long lVar3;
long in_FS_OFFSET;
int local_c4;
char local_78 [104];
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28); // 保存一个安全检查的值,用于检测栈溢出
__s1 = (char *)_JNIEnv::GetStringUTFChars(*env, str, 0); // 从JNI环境获取Java字符串的UTF-8表示
local_c4 = 0; // 初始化循环计数器
while( true ) { // 无限循环
uVar2 = __strlen_chk("GSJEB|OBUJWF`MBOE~", 0xffffffffffffffff); // 获取一个硬编码字符串的长度
if (uVar2 <= (ulong)(long)local_c4) break; // 如果已经复制了所有字符,则退出循环
local_78[local_c4] = "GSJEB|OBUJWF`MBOE~"[local_c4] + -1; // 将硬编码字符串的每个字符减1后复制到local_78数组
local_c4 = local_c4 + 1; // 增加计数器
}
lVar3 = __strlen_chk("GSJEB|OBUJWF`MBOE~", 0xffffffffffffffff); // 再次获取硬编码字符串的长度
local_78[lVar3] = '\0'; // 在字符串末尾添加空字符以结束字符串
iVar1 = strcmp(__s1, local_78); // 比较输入的字符串和转换后的硬编码字符串
__android_log_print(3, "input ", &DAT_001006b0, __s1); // 将输入的字符串记录到Android日志
__android_log_print(3, "Password", &DAT_001006b0, local_78); // 将转换后的字符串记录到Android日志
_JNIEnv::ReleaseStringUTFChars(*env, str, __s1); // 释放JNI字符串资源
if (*(long *)(in_FS_OFFSET + 0x28) == local_10) { // 检查栈是否溢出
return iVar1 == 0; // 如果字符串匹配,返回true,否则返回false
}
/* WARNING: Subroutine does not return */
__stack_chk_fail(); // 如果检测到栈溢出,则调用失败处理函数
}

简而言之,这个函数的作用是:

  1. 从JNI环境获取一个Java字符串的UTF-8表示。
  2. 将一个硬编码的字符串每个字符减1后存储到本地数组。
  3. 比较输入的字符串和转换后的字符串。
  4. 将输入的字符串和转换后的字符串记录到Android日志。
  5. 释放JNI字符串资源。
  6. 如果检测到栈溢出,则调用失败处理函数。

返回值是布尔类型,如果输入的字符串和转换后的字符串匹配,则返回true,否则返回false。这里的字符串比较看起来像是一种简单的加密或编码过程,其中硬编码的字符串被每个字符减1后用于比较。

3、下面提供本机函数的源代码以便于解释。

#include <jni.h>
#include <string.h>
#include <cstdio>
#include <android/log.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_ad2001_frida0x8_MainActivity_cmpstr(JNIEnv *env, jobject thiz, jstring str) {
const char *inputStr = env->GetStringUTFChars(str, 0);
const char *hardcoded = "GSJEB|OBUJWF`MBOE~";
char password[100];

for (int i = 0; i < strlen(hardcoded) ; i++) {

password[i] = (char)(hardcoded[i] - 1);
}

password[strlen(hardcoded)] = '\0';
int result = strcmp(inputStr, password);
__android_log_print(ANDROID_LOG_DEBUG, "input ", "%s",inputStr);
__android_log_print(ANDROID_LOG_DEBUG, "Password", "%s",password);
env->ReleaseStringUTFChars(str, inputStr);

// Returning result: 1 if equal, 0 if not equal
return (result == 0) ? 1 : 0;
}

下面针对代码逐句解释下:

这声明了一个名为 cmpstr 的 JNI(Java 本机接口)函数。它从 Java 代码 ( Java_com_ad2001_frida0x8_MainActivity_cmpstr ) 中调用。

有三个参数: env 表示 JNI 环境, thiz 表示 Java 对象, str 表示 Java 字符串。

extern "C" JNIEXPORT jint JNICALL
Java_com_ad2001_frida0x8_MainActivity_cmpstr(JNIEnv *env, jobject thiz, jstring str)

从 Java 字符串 ( jstring ) 检索输入字符串并将其转换为 c 样式字符串 ( const char* )。

const char *inputStr = env->GetStringUTFChars(str, 0);

变量 hardcoded 包含一个硬编码值,并且还声明了一个数组 password 。

该循环通过减 1 来转换 hardcoded 中的每个字符,并将结果存储在 password 数组中。

const char *hardcoded = "GSJEB|OBUJWF`MBOE~";
char password[100];
for (int i = 0; i < strlen(hardcoded); i++) {
password[i] = (char)(hardcoded[i] - 1);
}

使用 strcmp 将用户输入 ( inputStr ) 与调整后的密码 ( password ) 进行比较。

结果存储在变量 result 中。

int result = strcmp(inputStr, password);

释放与输入字符串关联的资源。

env->ReleaseStringUTFChars(str, inputStr);

如果字符串相等则返回 1,如果不相等则返回 0。

return (result == 0) ? 1 : 0;

4、所以,这块要得到用户正确的输入,有两种办法:

  • 方法一:写脚本,对硬编码字符串进行循环操作后,得到正确输入
  • 方法二:直接hook函数 strcmp ,获取它的第二个参数,得到正确输入

方法一:Python脚本

str1 = "GSJEB|OBUJWF`MBOE~"
str2 = ""
for i in range(len(str1)):
s = chr(ord(str1[i]) - 1)
str2 = str2+s

print(str2)

image.png

image.png

方法二:hook函数 strcmp

常见API介绍

要Hook Native层的函数,有对应的Frida API,如下所示:

Interceptor.attach(targetAddress, {
onEnter: function (args) {
console.log('Entering ' + functionName);
// Modify or log arguments if needed
},
onLeave: function (retval) {
console.log('Leaving ' + functionName);
// Modify or log return value if needed
}
});

Interceptor.attach :将回调附加到指定的函数地址。 targetAddress 应该是我们要挂钩的本机函数的地址。

onEnter :进入挂钩函数时调用此回调。它提供对函数参数( args )的访问。

onLeave :当挂钩函数即将退出时调用此回调。它提供对返回值 ( retval ) 的访问。

同时,获取Native中特定函数的地址,也可以调用Frida API:

(1)Module.enumerateExports()

获取 libfrida0x8.so 的所有导出函数

image.png

(2)Module.getExportByName()

函数从模块(共享库)中检索具有给定名称的导出符号的地址

image.png

(3)Module.findExportByName()

它与 Module.getExportByName() 相同。唯一的区别是,如果未找到导出, Module.getExportByName() 会引发异常,而如果未找到导出, Module.findExportByName() 将返回 null 。

image.png

(4)Module.getBaseAddress()

有时,如果上述 API 不起作用,我们可以依靠 Module.getBaseAddress() ,该 API 返回给定模块的基地址。让我们找到 libfrida0x8.so 库的基地址。

image.png

如果我们想找到特定函数的地址,我们只需添加偏移量即可。为了找到偏移量,我们可以使用 ghidra。我们用这种方式找到 cmpstr 的地址。

image.png

偏移量为 0x8c0 。 将 0x8c0 添加到 0x7f59bbbf6000 得到 0x7f59bbbf68c0 。

img

(5)Module.enumerateImports()

枚举全部的导入函数

image.png

Hook操作

通过Module.enumerateImports()可以枚举全部的导入函数,在里面可以看到strcmp 函数

img

下面开始写脚本如下:

var strcmp_adr = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp_adr, {
onEnter: function (args) {
var arg0 = Memory.readUtf8String(args[0]); // first argument
var flag = Memory.readUtf8String(args[1]); // second argument
if (arg0.includes("Hello")) {

console.log("Hookin the strcmp function");
console.log("Input " + arg0);
console.log("The flag is "+ flag);

}
},
onLeave: function (retval) {
// Modify or log return value if needed
}
});

img

image.png

challenge 0x09

image.png

image.png

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

public void onClick(View v) {
if (MainActivity.this.check_flag() == 1337) {
try {
Cipher cipher = Cipher.getInstance("AES");
try {
cipher.init(2, new SecretKeySpec("3000300030003003".getBytes(), "AES"));
try {
Toast.makeText(MainActivity.this.getApplicationContext(), "You won " + new String(cipher.doFinal(Base64.getDecoder().decode("hBCKKAqgxVhJMVTQS8JADelBUPUPyDiyO9dLSS3zho0="))), 1).show();
}
}
}
}

JNI函数名为:Java_com_ad2001_a0x9_MainActivity_check_1flag

image.png

这块函数默认返回值为1,但是在java层中要求返回值为 1337,才会继续向下执行。这块直接hook函数返回值为1337即可:

function main() {
Java.perform(function() {
var check_flag = Module.enumerateExports("liba0x9.so")[0]['address'];
console.log("check_flag: " + check_flag);
Interceptor.attach(check_flag, {
onEnter: function(args) {
console.log("check_flag called");
},
onLeave: function(retval) {
console.log("check_flag returned: " + retval);
retval.replace(1337); //【注意】这块需要使用retval.replace来修改返回值
console.log("check_flag returned: " + retval);
}
});
})
}

setImmediate(main);

image.png