challenge 0x0A

image.png

image.png

activityMainBinding.sampleText.setText(stringFromJNI());

image.png

get_flag
Java_com_ad2001_frida0xa_MainActivity_stringFromJNI

在Java层中引用了 stringFromJNI函数,可以看到该函数作用就是设置了文本“Hello Hackers”

image.png

继续再看get_flag函数,这个函数没有在JAVA层中声明,也没有在库中引用。

image.png

image.png

接下来,看他的反编译:

该函数接受两个整数值,将它们相加,然后检查结果是否等于 3。如果等于 3,则存在循环。它对硬编码字符串 FPE>9q8A>BK-)20A-#Y 进行解码并记录解码后的flag。因此,要获取flag,我们需要调用此方法。

image.png

所以,这块使用frida直接调用get_flag函数

1、下面是一个参考模板

var native_adr = new NativePointer(<address_of_the_native_function>);
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
native_function(<arguments>);

逐行解释一下:

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")
"0xc1859000"

image.png

image.png

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
var get_flag_ptr = new NativePointer(adr);
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);

需要参数的总和等于 3 才能通过 if 检查。因此,我们可以传递 12 作为参数并调用该方法。所以最终的脚本会是这样的。

var adr = Module.findBaseAddress("libfrida0xa.so").add(0x18BB0) // Address of the get_flag() function
var get_flag_ptr = new NativePointer(adr);
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
get_flag(1,2);

img

下面检查一下日志输出:

使用 adb logcat监听一下日志输出

img

这块可以hook _android_log_print这个导出函数,打印它的最后一个参数也可以!

challenge 0x0B

image.png

单击该按钮不会输出任何内容,没有什么参考价值。直接使用 jadx 吧。

image.png

在反编译开始时,我们可以看到原生函数 getFlag() 的声明。

在底部,我们可以看到应用程序使用 System.loadLibrary() 加载 frida0xb.so

onCreate 方法中,它调用本机函数中的 getFlag() 方法。它不接受任何参数,也不返回任何内容。

image.png

查看反编译后的代码,getFlag()函数,内容为空。这明显不科学。

继续查看汇编代码,发现是有代码片段的:

image.png

首先,它将值 0xdeadbeef 加载到局部变量 local_14 中,然后将其与 0x539 进行比较。我们知道他们不相等。仅当满足此条件时才会执行下面的代码块。因此,ghidra 优化了其反编译,因为这种比较永远不会成立。您可以使用图表选项获得更好的想法。

image.png

因此,为了获得反编译,我们可以禁用 ghidra 中的优化。转到 Edit 选项 -> Tools Options

image.png

image.png

点击确认后,反编译的代码可以正常显示了:

image.png

那么问题来了,如何获得flag呢?

可以看到这块的if循环,当判断为false的时候,才会向下执行代码。

单击按钮时将调用该函数。但由于 if 检查永远不会为真,因此我们不会得到该标志。

如果我们可以修改这些汇编指令,那让if语句不跳转,那不就可以正常执行代码了吗。

为了修改/临时修补指令,我们可以使用 x86 架构的 X86Writer 类和 ARM64 架构的 Arm64Writer 类。

当我使用 x86 时,我将使用 X86Writer 。让我们看一个基本模板。


var writer = new X86Writer(<address_of_the_instruction>);

try {
// Insert instructions

// Flush the changes to memory
writer.flush();

} finally {
// Dispose of the X86Writer to free up resources
writer.dispose();
}

**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 实例关联的资源。

现在我们对模板有了一个大概的了解。接下来,我们必须找出要修补/修改的指令。这需要一些逆向工程基础知识。请考虑以下三个说明。

image.png

首先,它将 0xdeadbeef 加载到 local_14 中,然后将其与 0x539 进行比较。如果两个操作数相同, CMP 指令将设置零标志。在这个 CMP 指令之后,我们有一个 JNZ 指令。如果零标志未设置,该指令(跳转非零)会将执行流程更改为指定地址。如果设置了零标志,则不会跳转并继续执行下一条指令。但不幸的是,在这种情况下,由于 0xdeadbeef 不等于 0x539 ,因此不会设置零标志,并且 JNZ 指令将跳转到地址 < b9> 这是函数的结尾。

img

我们希望应用程序不进行跳转,以便继续执行下一条指令、解码并记录我们的标志。因此我们可以修补 JNZ 指令,使其不进行跳转。我们可以将 JNZ 替换为 NOP 指令。 NOP 代表无操作。除了将执行传递给下一条指令之外,它不执行任何其他操作。通过替换 NOP 代替 JNZ ,执行将继续而不跳转,并记录标志。您还可以尝试其他指令,例如 JE ,它与 JNZ 相反。

使用 X86Writer 修补

首先找到我们想要修补的 JNZ 指令的地址。

image.png

我们可以通过用基数 0x00010000 减去 0x20e2a 来找到偏移量。现在要获取实际的函数地址,我们可以使用将此偏移量与基地址相加。

[Android Emulator 5554::com.ad2001.frida0xb ]-> Module.getBaseAddress("libfrida0xb.so")
"0xc2083000"
[Android Emulator 5554::com.ad2001.frida0xb ]-> Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000)
"0xc2093e2a"
var jnz = Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000);
var writer = new X86Writer(jnz);

try {

writer.flush();

} finally {

writer.dispose();
}

现在我们需要将 NOP 指令放置在 JNZ 指令的位置。要放置 NOP 指令,我们可以使用方法 PutNop()

image.png

可以参考文档: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);
var writer = new X86Writer(jnz);

try {

writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()

writer.flush();

} finally {

writer.dispose();
}

image.png

程序崩溃了。该错误表明进程由于保护失败而崩溃。发生此崩溃的原因是,我们尝试写入没有写入权限的内存。我们正在尝试写入二进制文件的 .text 部分。默认情况下它没有 write 权限。这就是它崩溃的原因。我们可以使用 ghidra 的内存映射工具来检查这一点。

image.png

那这种问题怎么解决呢???

这里使用 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);
Memory.protect(jnz, 0x1000, "rwx");
var writer = new X86Writer(jnz);

try {

writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()

writer.flush();

} finally {

writer.dispose();
}

img

img

使用Arm64Writer修补

让我们在 ARM64 设备上尝试一下同样的事情。对于 ARM64,我们必须使用 ARM64 编写器。

让我们反编译该应用程序以获得 ARM64 的 libfrida0xb.so

img

img

在 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

img

现在我们需要重复上面的过程来找到 b.ne 指令的地址以及下一条指令的地址,以便我们可以将该地址传递给 putBImm(address) 函数。您可以使用 ghidra 来查找偏移量。我不会再解释了。

所以得到偏移量后,可以通过 Module.findBaseAddress("libfrida0xb.so") 找到基址,加上偏移量,就得到了指令的实际地址。让我们编写最终的脚本。

var adr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);  // Addres of the b.ne instruction
Memory.protect(adr, 0x1000, "rwx");
var writer = new Arm64Writer(adr); // ARM64 writer object
var target = Module.findBaseAddress("libfrida0xb.so").add(0x1524c); // Address of the next instruction b LAB_00115250

try {

writer.putBImm(target); // Inserts the <b target> instruction in the place of b.ne instruction
writer.flush();

console.log(`Branch instruction inserted at ${adr}`);
} finally {

writer.dispose();

}

在 ARM64 中,您不必担心指令的对齐方式,因为所有指令都是 4 字节对齐的。

img

img