写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 跟羽夏学 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 : %d\n", i);
puts("do while 循环");
int i = 0;
do
{
printf("i | do : %d\n", i);
} while (i < 5);
puts("while 循环");
i = 0;
while (i < 5)
{
printf("i | while : %d\n", i);
}
}
/*======= 函数 =======*/
void test1() { puts("test1 func exec!"); }
void test2(int arg1) { printf("test2 func exec : %d\n", arg1); }
char test3(int arg1, char arg2) { printf("test3 func exec : %d , %c\n", 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 , %d\n", 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用于显示可执行文件中的符号时,会显示更多信息。在链接过程中,符号被解析为虚拟地址(如果可能),这导致在运行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@@
[]A\A]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 ——初识