第七周学习笔记
- 了解不同进制之间的转化方法
- 了解字长与端序
- 了解汇编基础指令
- 初步了解x86架构
- 完成
BUUCTF
,Reverse
模块前三题
进制转换
二进制是一种只使用两个数字(0和1)的数制系统。它是计算机中最常用的数制系统,因为它可以用电子开关的两种状态(开或关)来表示。
十进制是一种使用十个数字(0到9)的数制系统。它是人类最常用的数制系统,因为它与我们的手指的数量相对应。
十六进制是一种使用十六个数字(0到9,以及A到F)的数制系统。它是一种方便的方式,可以用更少的位数来表示二进制数。例如,十六进制数FF等于二进制数11111111。
将十进制数转换为二进制数,可用以下方法:
- 将十进制数除以2,得到商和余数。
- 将余数写在右边,作为二进制数的最低位。
- 重复上述步骤,直到商为0为止。
- 将得到的二进制数从右到左读取。
例如,要将十进制数13转换为二进制数:
- 13除以2,商为6,余数为1。
- 将1写在右边,作为二进制数的最低位。
- 6除以2,商为3,余数为0。
- 将0写在左边,作为二进制数的次低位。
- 3除以2,商为1,余数为1。
- 将1写在左边,作为二进制数的次高位。
- 1除以2,商为0,余数为1。
- 将1写在左边,作为二进制数的最高位。
- 得到的二进制数为1101,从右到左读取为1101。
要将二进制数转换为十进制数:
- 将二进制数的每一位乘以2的相应次方,从右到左依次为0,1,2,等等。
- 将得到的乘积相加,得到十进制数。
例如,将二进制数1010转换为十进制数:
- 将1010的最低位0乘以2的0次方,得到0。
- 将1010的次低位1乘以2的1次方,得到2。
- 将1010的次高位0乘以2的2次方,得到0。
- 将1010的最高位1乘以2的3次方,得到8。
- 将得到的乘积相加,得到10。
将十进制数转换为十六进制数:
- 将十进制数除以16,得到商和余数。
- 如果余数小于10,将其写在右边,作为十六进制数的最低位。
- 如果余数大于等于10,将其用对应的字母(A到F)替换,然后写在右边,作为十六进制数的最低位。
- 重复上述步骤,直到商为0为止。
- 将得到的十六进制数从右到左读取。
例如,将十进制数255转换为十六进制数:
- 255除以16,商为15,余数为15。
- 将余数15用字母F替换,然后写在右边,作为十六进制数的最低位。
- 15除以16,商为0,余数为15。
- 将余数15用字母F替换,然后写在左边,作为十六进制数的最高位。
- 得到的十六进制数为FF,从右到左读取为FF。
将十六进制数转换为十进制数:
- 将十六进制数的每一位乘以16的相应次方,从右到左依次为0,1,2,等等。
- 如果某一位是字母(A到F),将其用对应的数字(10到15)替换,然后进行乘法运算。
- 将得到的乘积相加,得到十进制数。
例如,将十六进制数A5转换为十进制数:
- 将A5的最低位5乘以16的0次方,得到5。
- 将A5的最高位A用数字10替换,然后乘以16的1次方,得到160。
- 将得到的乘积相加,得到165。
字长和端序
字长是指计算机中一个字的位数,例如8位、16位、32位等。字长决定了计算机一次可以处理的数据量,通常与处理器的寄存器位数相同。
端序是指多字节数据在内存中或在数字通信链路中的字节排列顺序。有两种常见的端序:大端序和小端序。大端序将数据的低位字节存放在高位地址,高位字节存放在低位地址,符合人类的阅读习惯。小端序将数据的低位字节存放在低位地址,高位字节存放在高位地址,符合计算机的读取方式。
不同的处理器可能采用不同的端序,因此在网络传输或数据交换时,需要注意字节序的转换。网络传输一般采用大端序,也称为网络字节序。
x86架构
- 冯诺依曼体系结构是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。它由运算器、控制器、存储器、输入设备和输出设备组成,其中运算器和控制器共同构成中央处理器(CPU)。冯诺依曼体系结构的特点是采用二进制、程序存储和顺序执行。
- 寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果以及一些CPU运行需要的信息。寄存器的容量很小,通常只有几十个字节,因此它只能存储少量的数据。寄存器中存储的数据是二进制格式的,即由0和1组成的数字序列。
- 堆是为动态分配预留的内存空间。和栈不一样,从堆上分配和重新分配块没有固定模式;你可以在任何时候分配和释放它。这样使得跟踪哪部分堆已经被分配和被释放变的异常复杂;有许多定制的堆分配策略用来为不同的使用模式下调整堆的性能。
- 栈是为执行线程留出的内存空间。当函数被调用的时候,栈顶为局部变量和一些 bookkeeping 数据预留块。当函数执行完毕,块就没有用了,可能在下次的函数调用的时候再被使用。栈通常用后进先出(LIFO)的方式预留空间;因此最近的保留块(reserved block)通常最先被释放。进程空间布局是指进程在内存中的分区和组织方式。Linux进程的地址空间通常被划分为几个区域,每个区域都有不同的用途。下面是Linux进程地址空间布局的常见区域:
- 文本段(Text Segment):也称为代码段,它包含了程序的机器代码。这个区域通常是只读的,因为它包含了程序的指令,不允许程序修改它。
- 数据段(Data Segment):也称为静态数据段,它包含了已经被初始化的全局变量和静态变量。
- 堆(Heap):它是一个动态分配内存的区域,用来存放程序运行时创建的对象和数据结构。堆的大小可以根据程序的需要增长或缩小。
- 栈(Stack):它是一个后进先出的数据结构,用来存放函数的局部变量、参数和返回地址。每个函数调用都会在栈上分配一个栈帧(stack frame),函数返回时栈帧被弹出。
- 内存映射区域(Memory Mapped Region):它是一个用来映射文件或设备到内存的区域,可以实现文件或设备的高效访问。例如,动态链接库(DLL)就是通过内存映射的方式加载到进程的地址空间中的。
buuctf Reverse前三题
easyre
下载文件后使用exeinfo查壳。
为64位文件,使用ida64打开。
打开后直接就能看到flag :flag{this_Is_a_EaSyRe}
提交后成功。
reverse1
下载文件后查壳。
64位文件,ida64打开。
按shift+f12或导航栏中选择lumina-> push all来查看所有字符串。
ctrl+f进行查找,直接搜索flag。
跳转到任意一处。
ctrl+x查看交叉引用。
可以跳转到此处。
按f5或tab可以查看c语言类型的伪代码。
第一个for循环中的v3在后边没用到,不用管。
第二个for循环中,由经验可知“sub_1400111D1”为printf函数,“sub_14001128F”为scanf函数。
第一个if判断是否遍历完整个数组,第二行if在遍历每一位时对字符的ascii码值进行比较,按r可以直接查看ASCII码值对应的字符。
111为’o’,48为’0’。
分析之后代码可知,str1存放输入的flag,str2存放经过加工后的正确的flag。
双击str2跳转。
可见str2为{hello_world}
,只需将所有的o改为0即可得到正确的flag,即flag{hell0_w0rld}
。
reverse 2
依然是下载文件查壳。
虽然文件没有后缀,但依然可以看出他是64位程序。ida64,启动。
依然是shift+f12后查找flag。
之后ctrl+x跳转。
tab查看伪代码。
int __fastcall main(int argc, const char **argv, const char **envp)
{
int stat_loc; // [rsp+4h] [rbp-3Ch] BYREF
int i; // [rsp+8h] [rbp-38h]
__pid_t pid; // [rsp+Ch] [rbp-34h]
char s2[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v8; // [rsp+28h] [rbp-18h]
v8 = __readfsqword(0x28u);
pid = fork();
if ( pid )
{
waitpid(pid, &stat_loc, 0);
}
else
{
for ( i = 0; i <= strlen(&flag); ++i )
{
if ( *(&flag + i) == 105 || *(&flag + i) == 114 )
*(&flag + i) = 49;
}
}
printf("input the flag:");
__isoc99_scanf("%20s", s2);
if ( !strcmp(&flag, s2) )
return puts("this is the right flag!");
else
return puts("wrong flag!");
}
直接跳转flag,得到flag数组'hacking_for_fun}'
直接提交后失败,开始分析代码。
从for循环开始,strlen(&flag)获取flag数组长度。
*(&flag + i)
通过地址访问数组的每一位。
按r对ascii码值进行转换。
可知需要将flag中的i和r替换为1。
得到正确flag:{hack1ng_fo1_fun}
以要求格式提交后成功。
练习
.text:000011B1 var_4= dword ptr -4
.text:000011B1 arg_0= dword ptr 8
.text:000011B1 55 push ebp
.text:000011B2 89 E5 mov ebp, esp
.text:000011B4 83 EC 10 sub esp, 10h
.text:000011C1 C7 45 FC 00 00 00 00 mov [ebp+var_4], 0
.text:000011C8 83 7D 08 00 cmp [ebp+arg_0], 0
.text:000011CC 79 09 jns short loc_11D7
.text:000011CC
.text:000011CE C7 45 FC 01 00 00 00 mov [ebp+var_4], 1
.text:000011D5 EB 34 jmp short loc_120B
.text:000011D5
.text:000011D7 loc_11D7:
.text:000011D7 83 7D 08 00 cmp [ebp+arg_0], 0
.text:000011DB 75 09 jnz short loc_11E6
.text:000011DB
.text:000011DD C7 45 FC 02 00 00 00 mov [ebp+var_4], 2
.text:000011E4 EB 25 jmp short loc_120B
.text:000011E4
.text:000011E6 loc_11E6:
.text:000011E6 83 7D 08 1D cmp [ebp+arg_0], 1Dh
.text:000011EA 7F 09 jg short loc_11F5
.text:000011EA
.text:000011EC C7 45 FC 03 00 00 00 mov [ebp+var_4], 3
.text:000011F3 EB 16 jmp short loc_120B
.text:000011F3
.text:000011F5 loc_11F5:
.text:000011F5 83 7D 08 45 cmp [ebp+arg_0], 45h
.text:000011F9 7F 09 jg short loc_1204
.text:000011F9
.text:000011FB C7 45 FC 04 00 00 00 mov [ebp+var_4], 4
.text:00001202 EB 07 jmp short loc_120B
.text:00001202
.text:00001204 loc_1204:
.text:00001204 C7 45 FC 05 00 00 00 mov [ebp+var_4], 5
.text:00001204
.text:0000120B loc_120B:
.text:0000120B 8B 45 FC mov eax, [ebp+var_4]
.text:0000120E C9 leave
.text:0000120F C3 retn
答:这段代码的功能是根据输入的参数返回一个整数值,具体的逻辑是:
- 如果参数小于 0 ,返回 1 ;
- 如果参数等于 0 ,返回 2 ;
- 如果参数在 1 到 29 之间(包括 1 和 29 ),返回 3 ;
- 如果参数在 30 到 69 之间(包括 30 和 69 ),返回 4 ;
- 如果参数大于 69 ,返回 5 。
-
main 函数按照如下方式进行调用:
.text:00001221 6A 3C push 3Ch .text:00001223 E8 85 FF FF FF call test1 .text:00001228 83 C4 04 add esp, 4
- 该函数有几个参数
- 该函数使用了什么函数调用约定
- 当程序执行到 0x1228 时,eax 的值是多少
- 你能还原出 C 代码吗
答:
- 该函数有一个参数,即 3Ch (60) ,它是通过栈传递的。push 3Ch 将参数压入栈中,call test1 调用函数,add esp, 4 将栈指针恢复到原来的位置。
- 该函数使用了 cdecl 函数调用约定,它是一种常见的函数调用约定,特点是参数从右向左压入栈中,由调用者负责清理栈空间,返回值存放在 eax 寄存器中。
- 当程序执行到 0x1228 时,eax 的值是 test1 函数的返回值,取决于 test1 函数的内容。这里没有test函数的具体表达,无法确定 eax 的值。
- 还原 C 代码:
// 假设 test1 函数原型是 int test1(int x);
int main(void) {
int i = 60; // 3Ch
int j = test1(i); // 调用 test1 函数
return 0;
}