challenge 0x08
button.setOnClickListener(new View .OnClickListener() { @Override 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
中定义的,函数接受一个字符串作为参数并返回一个整数
直接解压apk文件,或使用apktool等反编译工具,在/resources/lib/目录下存放了对应的.so文件
大多数物理设备都基于 ARM64 架构,这块使用模拟器打开apk,所以使用 x86
库。但不会有太大区别。
可以使用IDA等逆向汇编工具打开so文件,这块使用一个新工具:Ghidra
具体使用教程可参考:https://www.youtube.com/watch?v=fTGTnrgjuGA
1、将文件导入,单击 Yes
并等待分析完成。
点击Export查看导出函数,找到了cmpstr
函数:
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 ); 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 ; 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_log_print(3 , "Password" , &DAT_001006b0, local_78); _JNIEnv::ReleaseStringUTFChars(*env, str, __s1); if (*(long *)(in_FS_OFFSET + 0x28 ) == local_10) { return iVar1 == 0 ; } __stack_chk_fail(); }
简而言之,这个函数的作用是:
从JNI环境获取一个Java字符串的UTF-8表示。
将一个硬编码的字符串每个字符减1后存储到本地数组。
比较输入的字符串和转换后的字符串。
将输入的字符串和转换后的字符串记录到Android日志。
释放JNI字符串资源。
如果检测到栈溢出,则调用失败处理函数。
返回值是布尔类型,如果输入的字符串和转换后的字符串匹配,则返回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); 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)
方法二:hook函数 strcmp 常见API介绍 要Hook Native层的函数,有对应的Frida API,如下所示:
Interceptor .attach (targetAddress, { onEnter : function (args ) { console .log ('Entering ' + functionName); }, onLeave : function (retval ) { console .log ('Leaving ' + functionName); } });
Interceptor.attach
:将回调附加到指定的函数地址。 targetAddress
应该是我们要挂钩的本机函数的地址。
onEnter
:进入挂钩函数时调用此回调。它提供对函数参数( args
)的访问。
onLeave
:当挂钩函数即将退出时调用此回调。它提供对返回值 ( retval
) 的访问。
同时,获取Native中特定函数的地址,也可以调用Frida API:
(1)Module.enumerateExports() 获取 libfrida0x8.so
的所有导出函数
(2)Module.getExportByName() 函数从模块(共享库)中检索具有给定名称的导出符号的地址
(3)Module.findExportByName() 它与 Module.getExportByName()
相同。唯一的区别是,如果未找到导出, Module.getExportByName()
会引发异常,而如果未找到导出, Module.findExportByName()
将返回 null
。
(4)Module.getBaseAddress() 有时,如果上述 API 不起作用,我们可以依靠 Module.getBaseAddress()
,该 API 返回给定模块的基地址。让我们找到 libfrida0x8.so
库的基地址。
如果我们想找到特定函数的地址,我们只需添加偏移量即可。为了找到偏移量,我们可以使用 ghidra。我们用这种方式找到 cmpstr
的地址。
偏移量为 0x8c0
。 将 0x8c0
添加到 0x7f59bbbf6000
得到 0x7f59bbbf68c0
。
(5)Module.enumerateImports() 枚举全部的导入函数
Hook操作 通过Module.enumerateImports()可以枚举全部的导入函数,在里面可以看到strcmp
函数
下面开始写脚本如下:
var strcmp_adr = Module .findExportByName ("libc.so" , "strcmp" );Interceptor .attach (strcmp_adr, { onEnter : function (args ) { var arg0 = Memory .readUtf8String (args[0 ]); var flag = Memory .readUtf8String (args[1 ]); if (arg0.includes ("Hello" )) { console .log ("Hookin the strcmp function" ); console .log ("Input " + arg0); console .log ("The flag is " + flag); } }, onLeave : function (retval ) { } });
challenge 0x09
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
这块函数默认返回值为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 ); console .log ("check_flag returned: " + retval); } }); }) } setImmediate (main);