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