frida-labs(3)
challenge 0x0A
activityMainBinding.sampleText.setText(stringFromJNI()); |
get_flag |
在Java层中引用了 stringFromJNI
函数,可以看到该函数作用就是设置了文本“Hello Hackers”
继续再看get_flag
函数,这个函数没有在JAVA层中声明,也没有在库中引用。
接下来,看他的反编译:
该函数接受两个整数值,将它们相加,然后检查结果是否等于 3。如果等于 3,则存在循环。它对硬编码字符串 FPE>9q8A>BK-)20A-#Y
进行解码并记录解码后的flag。因此,要获取flag,我们需要调用此方法。
所以,这块使用frida直接调用get_flag
函数
1、下面是一个参考模板
var native_adr = new NativePointer(<address_of_the_native_function>); |
逐行解释一下:
var native_adr = new NativePointer(<address_of_the_native_function>); |
要在 frida 中调用本机函数,我们需要一个 NativePointer
对象。我们应该将要调用的本机函数的地址传递给 NativePointer 构造函数。接下来,我们将创建 NativeFunction
对象,它代表我们要调用的实际本机函数。它围绕本机函数创建一个 JavaScript 包装器,允许我们从 frida 调用该本机函数。
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']); |
第一个参数应该是 NativePointer
对象,第二个参数是本机函数的返回类型,第三个参数是要传递给本机函数的参数的数据类型列表。现在我们可以像在 java 空间中一样调用该方法。
native_function(<arguments>); |
后面直接调用即可。
2、编写脚本调用
①获取函数的地址,这块使用基址+偏移的方法
[Android Emulator 5554::com.ad2001.frida0xa ]-> Module.getBaseAddress("libfrida0xa.so") |
So the offset = 0x00028bb0
- 0x00010000
= 0x18BB0
. Now we can add that to the base.
现在就获取到了函数的地址:
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x18BB0) |
②调用函数
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x18BB0) // Address of the get_flag() function |
需要参数的总和等于 3
才能通过 if
检查。因此,我们可以传递 1
和 2
作为参数并调用该方法。所以最终的脚本会是这样的。
var adr = Module.findBaseAddress("libfrida0xa.so").add(0x18BB0) // Address of the get_flag() function |
下面检查一下日志输出:
使用 adb logcat
监听一下日志输出
这块可以hook _android_log_print
这个导出函数,打印它的最后一个参数也可以!
challenge 0x0B
单击该按钮不会输出任何内容,没有什么参考价值。直接使用 jadx 吧。
在反编译开始时,我们可以看到原生函数 getFlag()
的声明。
在底部,我们可以看到应用程序使用 System.loadLibrary()
加载 frida0xb.so
。
在 onCreate
方法中,它调用本机函数中的 getFlag()
方法。它不接受任何参数,也不返回任何内容。
查看反编译后的代码,getFlag()
函数,内容为空。这明显不科学。
继续查看汇编代码,发现是有代码片段的:
首先,它将值 0xdeadbeef
加载到局部变量 local_14
中,然后将其与 0x539
进行比较。我们知道他们不相等。仅当满足此条件时才会执行下面的代码块。因此,ghidra 优化了其反编译,因为这种比较永远不会成立。您可以使用图表选项获得更好的想法。
因此,为了获得反编译,我们可以禁用 ghidra 中的优化。转到 Edit
选项 -> Tools Options
。
点击确认后,反编译的代码可以正常显示了:
那么问题来了,如何获得flag呢?
可以看到这块的if
循环,当判断为false
的时候,才会向下执行代码。
单击按钮时将调用该函数。但由于 if
检查永远不会为真,因此我们不会得到该标志。
如果我们可以修改这些汇编指令,那让if语句不跳转,那不就可以正常执行代码了吗。
为了修改/临时修补指令,我们可以使用 x86 架构的 X86Writer
类和 ARM64 架构的 Arm64Writer
类。
当我使用 x86
时,我将使用 X86Writer
。让我们看一个基本模板。
|
**X86Writer**
的实例化:
var writer = new X86Writer(<address_of_the_instruction>); |
- 这将创建
X86Writer
类的实例并指定我们要修改的指令的地址。这将设置写入器在指定的内存位置上进行操作。
Inserting instructions: 插入说明:
try { /* Insert instructions here */ } |
- 在
try
块中,我们可以插入要修改/添加的 x86 指令。X86Writer
实例提供了插入各种x86指令的各种方法。我们可以为此使用文档。
Flushing the Changes : 刷新更改:
writer.flush(); |
- 插入指令后,调用
flush
方法将更改应用到内存。这可确保将修改后的指令写入内存位置。
Cleanup : 清理 :
finally { /* Dispose of the X86Writer to free up resources */ writer.dispose(); } |
finally
块用于确保正确清理X86Writer
资源。调用dispose
方法来释放与X86Writer
实例关联的资源。
现在我们对模板有了一个大概的了解。接下来,我们必须找出要修补/修改的指令。这需要一些逆向工程基础知识。请考虑以下三个说明。
首先,它将 0xdeadbeef
加载到 local_14
中,然后将其与 0x539
进行比较。如果两个操作数相同, CMP
指令将设置零标志。在这个 CMP
指令之后,我们有一个 JNZ
指令。如果零标志未设置,该指令(跳转非零)会将执行流程更改为指定地址。如果设置了零标志,则不会跳转并继续执行下一条指令。但不幸的是,在这种情况下,由于 0xdeadbeef
不等于 0x539
,因此不会设置零标志,并且 JNZ
指令将跳转到地址 < b9> 这是函数的结尾。
我们希望应用程序不进行跳转,以便继续执行下一条指令、解码并记录我们的标志。因此我们可以修补 JNZ
指令,使其不进行跳转。我们可以将 JNZ
替换为 NOP
指令。 NOP
代表无操作。除了将执行传递给下一条指令之外,它不执行任何其他操作。通过替换 NOP
代替 JNZ
,执行将继续而不跳转,并记录标志。您还可以尝试其他指令,例如 JE
,它与 JNZ
相反。
使用 X86Writer 修补
首先找到我们想要修补的 JNZ
指令的地址。
我们可以通过用基数 0x00010000
减去 0x20e2a
来找到偏移量。现在要获取实际的函数地址,我们可以使用将此偏移量与基地址相加。
[Android Emulator 5554::com.ad2001.frida0xb ]-> Module.getBaseAddress("libfrida0xb.so") |
[Android Emulator 5554::com.ad2001.frida0xb ]-> Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000) |
var jnz = Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000); |
现在我们需要将 NOP
指令放置在 JNZ
指令的位置。要放置 NOP
指令,我们可以使用方法 PutNop()
可以参考文档:https://frida.re/docs/javascript-api/#x86writer
在更新脚本之前,请考虑一下这一点。这里应该放置多少个 NOP
指令?
典型 x86
架构中 NOP
指令的大小为1字节。但是看看我们试图替换的 JNZ
指令的大小,它是6个字节。
从 0x00020e30
中减去 0x00020e2a
得到 6。总之,我们必须在 JNZ
的位置放置 6 个 NOP
指令,因为在修补时指令与其他指令的比较,必须保证新指令的大小占被替换指令的大小。那么我们来更新一下脚本吧。
var jnz = Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000); |
程序崩溃了。该错误表明进程由于保护失败而崩溃。发生此崩溃的原因是,我们尝试写入没有写入权限的内存。我们正在尝试写入二进制文件的 .text
部分。默认情况下它没有 write
权限。这就是它崩溃的原因。我们可以使用 ghidra 的内存映射工具来检查这一点。
那这种问题怎么解决呢???
这里使用 Memory.protect
函数。我们可以使用这个函数来修改内存区域的保护属性。 Memory.protect
函数的语法是:
Memory.protect(address, size, protection); |
address
:要更改保护的内存区域的起始地址。
size
:内存区域的大小(以字节为单位)。
protection
:内存区域的保护属性。
我们可以使用此函数使 .text
部分可写。我们不会使整个部分都可写。我们只需要更改一个小区域的权限,这样我们就可以插入 NOP
指令而不会导致应用程序崩溃。在我们的例子中,我们可以提供 JNZ
函数的地址作为 Memory.protect()
函数的第一个参数。对于大小,我们可以指定 0x1000
,这已经足够了,因为我们的 NOP
指令只需要 6 个字节。为了保护,我们需要读、写和执行。所以我们可以传递 rwx
。
最后脚本更新为:
var jnz = Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000); |
使用Arm64Writer修补
让我们在 ARM64 设备上尝试一下同样的事情。对于 ARM64,我们必须使用 ARM64 编写器。
让我们反编译该应用程序以获得 ARM64 的 libfrida0xb.so
在 x86 中,我们看到了 cmp
指令,但在这里我们看到了 subs
指令。它从 0xdeadbeef 中减去 0x539
;如果结果为零,则设置零标志。如果未设置零标志,则 b.ne LAB_0011532c
(不等于则转移)指令将跳转到位置 LAB_0011532c
。否则,不会跳转,并执行 b.ne LAB_0011532c
之后的下一条指令。我们不希望它跳跃。因此,与我们上面所做的类似,我们可以修补 b.ne
指令。让我们尝试一些不同的东西,而不是使用 nop
指令。
让我们用 b
指令替换 b.ne
。无论标志如何,这都将分支到指定的标签或位置。我们可以将 b.ne
指令替换为直接分支到下一条指令的分支指令。
让我们使用文档来看看哪个方法提供了此指令。
https://frida.re/docs/javascript-api/#arm64writer
现在我们需要重复上面的过程来找到 b.ne
指令的地址以及下一条指令的地址,以便我们可以将该地址传递给 putBImm(address)
函数。您可以使用 ghidra 来查找偏移量。我不会再解释了。
所以得到偏移量后,可以通过 Module.findBaseAddress("libfrida0xb.so")
找到基址,加上偏移量,就得到了指令的实际地址。让我们编写最终的脚本。
var adr = Module.findBaseAddress("libfrida0xb.so").add(0x15248); // Addres of the b.ne instruction |
在 ARM64 中,您不必担心指令的对齐方式,因为所有指令都是 4 字节对齐的。