写在前面
此系列是本人一个字一个字码出来的,包括示例和实验截图。本人非计算机专业,可能对本教程涉及的事物没有了解的足够深入,如有错误,欢迎批评指正。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 跟羽夏学 Ghidra ——简述 ,方便学习本教程。请认准 博客园 的 寂静的羽夏 ,目前仅在该平台发布。
前言
本篇涉及一些底层的知识,主要是变量在内存的分配情况。如果不清楚,可以参考我的 羽夏看C语言系列教程 。虽然是Win
平台的,但相差无几,原理相通。
在开始之前,一定要将代码编译好(其实上一篇就开始用了),这次我们要开始用示例进行学习工作。注意在简述说的基础知识都一定要会,不过我用的时候会提一下。
本篇寂静的羽夏的博文,将专注于variable
函数的分析,紧扣“数据”关键词。
函数定位
我们从分析variable
函数开始,讲解与数据相关的知识,首先需要定位。那么如何定位呢?
通过上一篇博文的学习,我们知道了需要从Symbol Tree
就能找到,双击就能跳转到函数位置:
与此同时,我们可以看到反汇编的内容:
void variable(void)
{
gvar1 = 0x31;
gvar2 = 5;
puts("===");
puts("===");
gvar2 = 5;
gstruct[0] = 1;
gstruct._2_2_ = 2;
gstruct._4_4_ = 3;
gstruct._8_8_ = 4;
gvar1 = 0x50;
return;
}
和我们的源码作比较:
// Written by WingSummer
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;
}
可以看出,类型对不上,局部变量的结构体相关赋值被吃掉了。但是,Ghidra
已经在反汇编结果中有了提示:
undefined AL:1 <RETURN>
undefined8 Stack[-0x10]:8 local_10 XREF[1]: 00401196(W)
undefined4 Stack[-0x14]:4 local_14 XREF[1]: 0040118f(W)
undefined2 Stack[-0x16]:2 local_16 XREF[1]: 00401189(W)
undefined1 Stack[-0x18]:1 local_18 XREF[1]: 00401185(W)
Ghidra
已经识别到了局部变量,但是,并没有使用,反汇编并没有将其列入。不过,我们先把反汇编的变量名和源代码的名字弄的一致。有如下三种方法:
- 在符号树列表:
- 在反编译窗口:
- 在反汇编窗口:
你可以使用以上方式来修改。
在我们的源代码中gvar1 = '1'
,在反汇编就成了gvar1 = 0x31
,这个是由于反汇编是使用的ASCII
表示的字符1
,我们有两种方式进行转化:
- 在反编译窗口:
- 在反汇编窗口:
与此同时,我们在程序中用到了大量的结构体,但是Ghidra
并没有完全给识别出来,那么怎么创建呢:
在Data Type Manager
中,选中tutorial
(因为我的程序的名字叫这个),右击菜单的New
,然后找到Structure
,点击,最终的编辑结果:
在这里注意的一点是,结构体是四个字节对齐的,至于为什么自己回去复习功课。
我们编辑完毕后,点击保存按钮,那么我们如何使用我们自定义类型呢:
点击后,搜索找到我们的类型,点击确定,最终会得到下面的结果:
void variable(void)
{
tstruct lstruct;
gvar1 = '1';
gvar2 = 5;
puts("===");
puts("===");
gvar2 = 5;
gstruct.var1 = 1;
gstruct.var2 = 2;
gstruct.var3 = 3;
gstruct.var4 = 4;
gvar1 = 'P';
return;
}
至此,创建结构体的方式学会了,我们就可以创建枚举、别名、共用体。这些我就不在本篇赘述了,请自行触类旁通。
回到主函数
下面我们回到主函数,继续学习如何更改函数签名以及修正原是字符串的未知类型。
主函数的反编译结果如下:
undefined8 main(void)
{
int iVar1;
int local_14;
uint local_10;
char local_9;
while( true ) {
while( true ) {
while( true ) {
while( true ) {
puts(&DAT_004020d8);
__isoc99_scanf(&DAT_0040218c,&local_14);
if (local_14 != 2) break;
loop();
}
if (2 < local_14) break;
if (local_14 != 1) goto LAB_0040146a;
variable();
}
if (local_14 != 3) break;
test1();
puts("===");
test2(1);
puts("===");
local_9 = test3(5,0x41);
printf("ret : %c",(ulong)(uint)(int)local_9);
puts("===");
local_10 = test4(1,2,3,4,5,6);
printf("ret : %d",(ulong)local_10);
}
if (local_14 != 4) break;
iVar1 = crackMe();
if (iVar1 == 0) {
puts(&DAT_004021c0);
}
else {
puts(&DAT_004021a1);
}
setbuf(stdin,(char *)0x0);
}
LAB_0040146a:
puts(&DAT_004021e8);
getchar();
return 0;
}
首先我们修改一下主函数的函数声明,也就是函数签名(有三种方式):
- 在符号树列表:
- 在反编译窗口:
- 在反汇编窗口:
点击会弹出一个窗体,我们修改一下如下图所示:
可以看到,通过该对话框可以修改函数名、返回值、调用约定以及函数属性(不定参数、内敛函数、无返回值),更改参数,这些可以自行探索。
接下来我们看到puts(&DAT_004020d8)
这样的代码,这明明是字符串,但并没有识别到,仅仅被认为是普通数据。我们可以通过修改识别为ASCII
字符串:
修改后,是如下结果:
s__Ghidra_0._1._2._3._4._004020d8 XREF[2]: main:0040132d(*),
main:0040132d(*)
004020d8 e6 ac ds E6h,ACh,A2h,E8h,BFh,8Eh,E6h,9Dh,A5h,
a2 e8
bf 8e
但是这完全没有正常字符串的样子,这个是编码问题。在 Linux 下,中文的编码通常是UTF-8
,我们需要修改一下:
点击后,就会弹出一下弹窗,修改如下:
最终,我们的结果如下(由于字符串太长,被隐掉了):
s__Ghidra_0._1._2._3._4._004020d8 XREF[2]: main:0040132d(*),
main:0040132d(*)
004020d8 e6 ac ds u8"欢迎来到“寂静的羽夏”的 Ghidra 教学教程
a2 e8
bf 8e
拓展
在本寂静的羽夏的实验示例中,我们还没有涉及数组以及如果将代码识别为数据或者把数据是别为代码,这怎样处理,下面开始介绍。
由于没有提前设计,我们假设variable
函数中的gvar2
是一个数组,长度是5,那么我们如何转化呢?
在gvar2
的位置右击,找到Data
,选中Create Array
,你将会得到如下界面:
输入数字即可,由于它不是,就不用点确定了。
下面我们取消之前的假设,我们做一个新的,假设variable
函数开头的一句汇编是数据,其实不是代码,我们如何将其转为数据呢?
我们先把代码转为数据:
点击后,该处汇编将会变成未定义类型的字节。但我反悔了,我又想把它弄成数据(不要撤销):
右键菜单通常比较麻烦,这里只是为了介绍才这么做,通常用C
和D
这两个快捷键,对代码和数据之间进行转化。
其次,我还没有介绍如何更改类型,只是说了如何使用自己定义的类型。下面看一下:
点击后,会弹出一个对话框,输入正确的类型名字即可。
至此,该博文结束。