写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 跟羽夏学 Ghidra ——简述 ,方便学习本教程。请认准 博客园 的 寂静的羽夏 ,目前仅在该平台发布。
实验代码
在该教程开始之前,我先把实验程序的代码放上,之后所有的文章的实验程序都是依靠该代码,采用C编写,是个Linux编译器GCC都会有的:
// License : GPL // Author : 寂静的羽夏,博客园,wingsummer // Comment : 本代码由寂静的羽夏进行编写示例,提供讲解介绍之用,未经授权不能用于商业用途 #include <stdio.h> /*======= 变量 =======*/ // 全局变量 char gvar1; int gvar2; struct tstruct { char var1; short var2; int var3; long var4; } gstruct; void variable() { //局部变量 gvar1 = '1'; gvar2 = 5; puts("==="); struct tstruct lstruct; lstruct.var1 = 1; lstruct.var2 = 2; lstruct.var3 = 3; lstruct.var4 = 4; // 全局变量赋值 puts("==="); gvar1 = 'P'; gvar2 = 5; gstruct.var1 = 1; gstruct.var2 = 2; gstruct.var3 = 3; gstruct.var4 = 4; } /*======= 循环 =======*/ void loop() { puts("for 循环"); for (int i = 0; i < 5; i++) printf("i | for : %dn", i); puts("do while 循环"); int i = 0; do { printf("i | do : %dn", i); } while (i < 5); puts("while 循环"); i = 0; while (i < 5) { printf("i | while : %dn", i); } } /*======= 函数 =======*/ void test1() { puts("test1 func exec!"); } void test2(int arg1) { printf("test2 func exec : %dn", arg1); } char test3(int arg1, char arg2) { printf("test3 func exec : %d , %cn", arg1, arg2); } int test4(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6) { printf("test4 func exec : %d , %d , %d , %d , %d , %dn", arg1, arg2, arg3, arg4, arg5, arg6); } /*===== 破解示例 ======*/ int crackMe(); int getKey(); /*===================*/ int main() { while (1) { puts("欢迎来到“寂静的羽夏”的 Ghidra 教学教程,请输入数字来进行破解训练:n" "0. 退出训练n" "1. 变量训练n" "2. 循环训练n" "3. 函数训练n" "4. 破解示例n"); int sw; scanf("%d", &sw); switch (sw) { case 1: variable(); break; case 2: loop(); break; case 3: test1(); puts("==="); test2(1); puts("==="); char i1 = test3(5, 'A'); printf("ret : %c", i1); puts("==="); int i2 = test4(1, 2, 3, 4, 5, 6); printf("ret : %d", i2); break; case 4: if (crackMe()) puts(">> 祝贺破解成功!"); else puts("抱歉,没成功哦,再试一次!"); setbuf(stdin, NULL); break; default: goto _exit; } } _exit: puts("按任意键继续 ..."); getchar(); return 0; } int crackMe() { int key = getKey(); if (key ^ 5 == 0x123456) return 0; return 1; } int getKey() { int key; puts("请输入密钥:"); scanf("%d", &key); return key; }
安装 Ghidra
下面来简单介绍一下如何安装。
如果你电脑上没有星火商店,请到Github,不过可能不能顺利进入,它的 链接 。
如果有星火商店,直接搜Ghidra就是,那个是我(寂静的羽夏)单独打的一个包,能够把它需要的Java依赖给一块安装上,因为软件比较大,安装起来可能需要花费一些时间,尤其没有预先装依赖的:
安装好后,它的目录为/opt/Ghidra。经过测试,在Deepin 20.7上使用我的打包不需要进行额外的配置。但为了以防万一,请按照我在星火商店的声明做好配置。如果正常启动,最终将会是这个界面:
下面我继续介绍在 Linux 平台几个用于逆向分析的几个实用工具。
file
file是一个命令行工具,通过魔数识别文件类型。给个例子:
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ file tutorial tutorial: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=6d9b944f22066b0b4925a4471cdbf616d8d50da5, not stripped
这个是将示例代码编译后的文件,使用file命令行识别,我们可以获得它是一个ELF文件,是64位的等信息。
如果想更加深入了解file这一个命令行工具,请在终端输入man file,查看帮助文档。
nm
将源文件编译为目标文件时,编译器必须嵌入有关全局(外部)符号位置的信息,以便链接器在组合目标文件以创建可执行文件时能够解析对这些符号的引用。除非指示从最终可执行文件中删除符号,否则链接器通常将符号从目标文件中带到最终可执行程序中。nm就可以列出这样的符号:
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ nm tutorial 0000000000404050 B __bss_start 0000000000404058 b completed.7325 0000000000401480 T crackMe 0000000000404040 D __data_start 0000000000404040 W data_start 00000000004010c0 t deregister_tm_clones 00000000004010b0 T _dl_relocate_static_pie 0000000000401130 t __do_global_dtors_aux 0000000000403e18 t __do_global_dtors_aux_fini_array_entry 0000000000404048 D __dso_handle 0000000000403e20 d _DYNAMIC 0000000000404050 D _edata 0000000000404088 B _end 0000000000401544 T _fini 0000000000401160 t frame_dummy 0000000000403e10 t __frame_dummy_init_array_entry 000000000040248c r __FRAME_END__ U getchar@@GLIBC_2.2.5 00000000004014a9 T getKey 0000000000404000 d _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 0000000000402214 r __GNU_EH_FRAME_HDR 0000000000404070 B gstruct 0000000000404080 B gvar1 0000000000404060 B gvar2 0000000000401000 T _init 0000000000403e18 t __init_array_end 0000000000403e10 t __init_array_start 0000000000402000 R _IO_stdin_used U __isoc99_scanf@@GLIBC_2.7 0000000000401540 T __libc_csu_fini 00000000004014e0 T __libc_csu_init U __libc_start_main@@GLIBC_2.2.5 00000000004011e1 T loop 0000000000401325 T main U printf@@GLIBC_2.2.5 U puts@@GLIBC_2.2.5 00000000004010f0 t register_tm_clones U setbuf@@GLIBC_2.2.5 0000000000401080 T _start 0000000000404050 B stdin@@GLIBC_2.2.5 0000000000401275 T test1 0000000000401286 T test2 00000000004012a8 T test3 00000000004012d3 T test4 0000000000404050 D __TMC_END__ 0000000000401162 T variable
上面是已经编译好的示例代码,如果是obj文件,就会成这样:
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ nm tutorial.o 000000000000031e T crackMe U getchar 0000000000000347 T getKey 0000000000000010 C gstruct 0000000000000001 C gvar1 0000000000000004 C gvar2 U __isoc99_scanf 000000000000007f T loop 00000000000001c3 T main U printf U puts U setbuf U stdin 0000000000000113 T test1 0000000000000124 T test2 0000000000000146 T test3 0000000000000171 T test4 0000000000000000 T variable
从输出中我可以看到中间有些单字母,它就有特殊含义,列一下:
| 字母 | 含义 |
|---|---|
| U | 未定义的符号(通常是外部符号引用) |
| T | text中定义的符号(通常是函数名) |
| t | text中定义的本地符号。在C程序中,这通常等同于静态函数 |
| D | 初始化的数据值 |
| C | 未初始化到数据值 |
当显示可执行文件中的符号时,会显示更多信息。在链接过程中,符号被解析为虚拟地址(如果可能),这导致在运行nm时可以获得更多信息,正如上面所示的。
ldd
创建可执行文件时,必须解析该可执行文件引用的任何库函数的位置。链接器有两种方法来解析对库函数的调用:静态链接和动态链接。提供给链接器的命令行参数决定使用这两种方法中的哪一种。可执行文件可以被静态链接、动态链接或两者都有。
当使用静态链接时,链接器将应用程序的对象文件与所需库的副本组合,以创建可执行文件。在运行时,不需要定位库代码,因为它已经包含在可执行文件中。
动态链接不同于静态链接,因为链接器不需要复制任何所需的库。
这两种方式各有优缺,根据自己的需要进行。通过ldd,我们可以获取该类信息:
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ ldd tutorial linux-vdso.so.1 (0x00007fffb53ec000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe91797e000) /lib64/ld-linux-x86-64.so.2 (0x00007fe917b6e000)
注意:该工具不要用于处理不受信任的可执行文件,否则有可能会被执行恶意代码
strings
strings专门用于从文件中提取字符串内容,通常不考虑这些文件的格式。使用具有默认设置的字符串(至少四个字符的7位ASCII序列):
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ strings tutorial /lib64/ld-linux-x86-64.so.2 libc.so.6 __isoc99_scanf puts stdin printf getchar setbuf __libc_start_main GLIBC_2.7 GLIBC_2.2.5 __gmon_start__ H=P@@ []AA]A^A_ for i | for : %d do while i | do : %d while i | while : %d test1 func exec! test2 func exec : %d test3 func exec : %d , %c test4 func exec : %d , %d , %d , %d , %d , %d Ghidra ret : %c ret : %d ... ;*3$" GCC: (Uos 8.3.0.3-3+rebuild) 8.3.0 crtstuff.c deregister_tm_clones __do_global_dtors_aux completed.7325 __do_global_dtors_aux_fini_array_entry frame_dummy __frame_dummy_init_array_entry tutorial.c __FRAME_END__ __init_array_end _DYNAMIC __init_array_start __GNU_EH_FRAME_HDR _GLOBAL_OFFSET_TABLE_ __libc_csu_fini puts@@GLIBC_2.2.5 stdin@@GLIBC_2.2.5 loop _edata crackMe test3 test1 setbuf@@GLIBC_2.2.5 printf@@GLIBC_2.2.5 gvar2 gstruct variable getKey __libc_start_main@@GLIBC_2.2.5 __data_start getchar@@GLIBC_2.2.5 __gmon_start__ __dso_handle _IO_stdin_used __libc_csu_init _dl_relocate_static_pie __bss_start main test4 test2 gvar1 __isoc99_scanf@@GLIBC_2.7 __TMC_END__ .symtab .strtab .shstrtab .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .text .fini .rodata .eh_frame_hdr .eh_frame .init_array .fini_array .dynamic .got .got.plt .data .bss .comment
虽然我们看到一些字符串看起来像是程序输出的,但其他字符串似乎是函数名和库名。我们应该小心,不要对程序的行为做出任何结论。记住,二进制文件中字符串的存在绝不表示该二进制文件以任何方式使用该字符串。
如果该程序加上-t参数,会加上字符串的位置:
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ strings -t x tutorial 2a8 /lib64/ld-linux-x86-64.so.2 409 libc.so.6 413 __isoc99_scanf 422 puts 427 stdin 42d printf 434 getchar 43c setbuf 443 __libc_start_main ...
加上-e参数可以指定字符串格式,比如16位的Unicode(示例程序没有):
┌─[wingsummer][wingsummer-PC][~/.../C/Code] └─➞ strings -e l tutorial
结语
本文提供的分析命令行工具并不一定是最好的。然而,它们确实代表了任何希望对二进制文件进行反向工程的人都可以使用的工具。更重要的是,它们代表了推动Ghidra发展的工具。之后的博文,我们将逐步深入Ghidra。
下一篇
跟羽夏学 Ghidra ——初识