点击上方蓝字 江湖评谈设为关注/星标
前言
独孤九剑,九剑起势。
起因是进行OpenJDK24 Reverse的时候,在bt(堆栈)上发现用汇编写的解释器,如果仅阅读部分源码非常难以理解。通读JVM自然可以理解的,但代价有点大。用LLDB脚本+软硬断点的形式非常繁琐。Binary Ninja流程图+LLDB破尽天下招式/内功。本篇带你领略。
Binary Ninja+LLDB
Binary Ninja(后简称:BN)在Ubuntu22.04上完全不需要安装,也不需要任何其它组件的支持,非常方便。下载 地址:
https://binary.ninja/free/
选择Download for Linux,下载之后它是一个zip的压缩文件,解压命令:
unzip -x binaryninja_free_linux.zip
进入解压的文件夹,即可直接运行:
#cd binaryninja
#./binaryninja
进入之后,需要设置几个地方,你需要设置本地调试器,比如这里选择的是LLDB。菜单栏Debugger->Debugger Adapter settings,Java设置如下图:
HelloWord是你测试的用例,用命令行传递进去。菜单栏File->Open选择上图Executable Path里的可执行文件。
下面是功能:
下断点:BN左侧边框点击类似虫子的图标,有个BreakPoints里面可以添加/删除/转到断点。
快捷键:F6启动调试,F7单步,F8步过,F9运行,G跳转到地址。
菜单栏View->Graph即可打开流程图
菜单栏Analysis-》Reanalyz对程序进行分析,以便于形成相应的流程图。
以上搞好了之后,下面就可以开启LLDB。Java代码(也即是上面BN命令的HelloWord):
public class HelloWorld {
public static void main(String[] args) {
String name = "Alice";
int age = 0x10;
System.out.printf("Name: %s\n", name);
System.out.printf("Age: %d\n", age);
}
}
在Java_java_io_FileOutputStream_writeBytes处下断,以下使用printstack.py脚本打印出来的(printstack脚本参考:天下第一调试利器LLDB)。
(lldb)b Java_java_io_FileOutputStream_writeBytes
(lldb)r
(lldb)bt
(lldb) printstack
Function Name:
0x7ffff7e48cbf---Java_java_io_FileOutputStream_writeBytes
Fucntion ASM:
0x7fffe85457ba
0x7fffe85457ba: vzeroupper
Fucntion ASM:
0x7fffe853edee
0x7fffe853edee: mov rcx, qword ptr [rbp - 0x10]
//省略部分,便于观看
Fucntion ASM:
0x7fffe853ef88
0x7fffe853ef88: mov rcx, qword ptr [rbp - 0x10]
Fucntion ASM:
0x7fffe8537d01
0x7fffe8537d01: mov rdi, qword ptr [rbp - 0x28]
Function Name:
0x7ffff5eae505---JavaCalls::call_helper(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)
Function Name:
0x7ffff641e714---os::os_exception_wrapper(void (*)(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*), JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)
Function Name:
0x7ffff5eadea7---JavaCalls::call(JavaValue*, methodHandle const&, JavaCallArguments*, JavaThread*)
Function Name:
0x7ffff5fa46d9---::jni_invoke_static(JNIEnv *, JavaValue *, jobject, JNICallType, jmethodID, JNI_ArgumentPusher *, JavaThread *)
Function Name:
0x7ffff5fb2336---::jni_CallStaticVoidMethod(JNIEnv *, jclass, jmethodID, ...)
Function Name:
0x7ffff7fa8598---invokeStaticMainWithArgs
Function Name:
0x7ffff7fa9977---JavaMain
Function Name:
0x7ffff7fafda6---ThreadJavaMain
Function Name:
0x7ffff7c94ac3---start_thread
Function Name:
0x7ffff7d26850---__clone3
JVM调用main前面堆栈还算正常,后面就变成了汇编代码。从JavaCalls::call_helper到0x7fffe8537d01地址就变成汇编。这个地方的JVM调用如下:
call_stub-》
generate_call_stub-》
generate_normal_entry-》
利用这些函数生成汇编,然后跳转到汇编入口进行解释器方面的执行。这个地址(0x7fffe8537d01)如果通过脚本或者硬件断点来进行debug遇到jmp之类指令跳转向上跟踪,非常麻烦。
要理解这些汇编,需要流程图支撑。
此时我们可以运行上面的Binary Ninja启动调试,启动之后,BN需要加载一段时间进行代码识别。然后G跳转到0x7fffe8537d01地址(注意这里如果地址不识别,可以先下Glibc的函数write让BN进行单步汇编运行,对照LLDB地址同步多运行几次,以此让BN慢慢识别),此时我们就可以愉快的看到了0x7fffe8537d01地址所在的流程图了:
有了它,如虎添翼,把整个JVM倒翻一遍都可以。相较于源码,更喜欢二进制。
结尾
独孤九剑,破尽天下招式。LLDB这类正规调试器极其强大的同时,就因为它过于正规,没有Reverse的效果,所以导致部分功能缺失。Binary Ninja弥补了这方面的遗憾。
往期精彩回顾