写在前面
老婆镇楼
这个就相当于是词条了,我后面复习直接 ctrl+f 用。
ylg基础知识
这个就不是写给我看的了(bushi)
内联循环
在编程中,“内联”(inline)指的是将函数体的代码直接插入到调用它的地方,而不是通过常规的 call 指令跳转过去。将函数体的循环直接插入,循环次数由运行时决定。这样做的好处是:
·省去了函数调用和返回时的栈操作(压栈、跳转、恢复等),提高执行速度。
·对于小函数尤其有利,比如 strlen、memcpy 等。
size_t strlen(const char *s) { size_t len = 0; while (*s != '\0') { len++; s++; } return len;}如果编译器决定内联这个函数,那么在调用 strlen(str) 的地方,它不会生成 call strlen,而是直接生成一段循环指令来扫描字符串。
使用IDA反汇编时,可能会遇到的两种情况:
·直接调用 strlen:反编译窗口会显示strlen(local_58),容易识别。
·内联循环:你可能会看到一段像上面那样的指令序列,Ghidra 有时能识别并注释为 strlen,有时则显示为一段自定义的循环。你需要根据指令模式(如 repne scasb)或循环逻辑判断它是在计算长度。
反汇编出来的伪代码类似于:
for (i = 0; s[i] != 0; i = i + 1) {}i 是一个整数变量(通常从 0 开始)。 s 是一个指向字符串的指针(比如 char *s)。 循环条件是 s[i] != 0,也就是当前字符不是字符串结束符 \0。 每次循环,i 增加 1,但循环体是空的(大括号里什么都没有)。 当遇到 \0 时,循环结束,此时 i 的值就是字符串的长度(因为 \0 本身没有被计入)。
内联循环 = 编译器将小函数(如 strlen)的循环体直接嵌入到调用处,避免函数调用开销。
内存单元
定义:内存中用于存储数据的基本单位,通常指一个字节(8 位)。每个内存单元有一个唯一的地址(从 0 开始编号),CPU 通过地址访问这些单元。
mov al, [0x1234]表示从内存地址 0x1234 处读取一个字节到 AL 寄存器。
寄存器
定义:CPU 内部的高速存储单元,用于暂存指令、数据或地址。它们是 CPU 计算的核心。
通用寄存器:EAX、EBX、ECX、EDX 等,用于算术运算、数据暂存。 指针寄存器:ESP(栈指针)、EBP(基址指针),用于管理栈。 指令指针:EIP,存放下一条要执行指令的地址。
add eax, ebx表示将 EAX 和 EBX 的值相加,结果存回 EAX。
段寄存器
定义:x86 架构特有的寄存器,用于支持内存分段管理。每个段寄存器(如 CS、DS、SS、ES)存放一个段选择子(在保护模式下)或段基址(在实模式下)。 作用:与偏移地址组合,形成最终的物理地址或线性地址。
常见段寄存器:
CS(代码段):指向存放指令的段。 DS(数据段):指向存放数据的段。 SS(栈段):指向栈所在段。 ES、FS、GS(附加段):用于其他数据访问。
mov ax, [bx]隐含使用 DS 段寄存器,即实际访问的是 DS指向的内存单元。
ASCII 字符
每个字符占用 1 个字节,如 ‘A’ = 0x41。
Unicode 字符
UTF-16
每个字符占用 2 个字节(字),常见于 Windows 程序(如 wchar_t)。
UTF-8
变长编码,英文 1 字节,中文通常 3 字节。
在逆向中,如果看到连续的两个字节组成一个字符,很可能是 UTF-16 字符串,在 Ghidra 中需将数据类型设置为 unicode 才能正确显示。
汇编
这个模块的寄存器就不区分大小写了,汇编的内容是复习用
地址加法器
物理地址 = 段地址 * 16 + 偏移地址
实则为数据左移四位(二进制位)衍生:一个数据的x进制形式左移1位相当于*x
CS,IP
同时修改CS,IP的内容
jmp 段地址:偏移地址
CS IP程序
伪指令
定义一个段
segment和ends是一对成对使用的伪指令, segment说明一个段的开始 ends说明一个段的结束 End是一个汇编程序的结束标记 assume:假设
栈
操作原则
提供出入栈指令:
push ax :将寄存器ax中的数据送入栈中;
pop ax :从栈顶取出数据送入ax
栈帧
当函数被调用时,会在栈上分配一段空间,称为栈帧,用于存放:
·局部变量(如数组、结构体、普通变量)
·函数参数(某些调用约定下)
·返回地址(函数执行完后要跳回的下一条指令地址)
·保存的帧指针(如 EBP)
栈溢出
栈溢出(Stack Overflow)是一种程序漏洞,当程序向栈中某个局部缓冲区写入的数据超过了该缓冲区预分配的大小时,多余的数据就会覆盖栈上相邻的内存区域,包括可能的关键数据(如返回地址、局部变量、保存的寄存器等)。攻击者可以利用这一点篡改程序的行为,甚至执行任意代码。
在程序运行时,栈用于存储:
局部变量
函数参数
返回地址(函数执行完后要跳回的位置)
保存的寄存器值(如 EBP)
当一个函数被调用时,会压入返回地址;进入函数后,会分配局部变量空间(通常通过 sub esp, 空间大小)。栈是从高地址向低地址增长的(向下增长)。
这里我直接用ai的图吧
高地址+-----------------+| 函数参数 |+-----------------+| 返回地址 | ← 函数返回时会跳到这里+-----------------+| 保存的 EBP |+-----------------+| 局部变量 | ← 例如 char buf[10]+-----------------+低地址用C语言:
void vulnerable() { char buffer[8]; // 局部缓冲区,只能容纳 7 个字符 + 结尾的 '\0' gets(buffer); // 从标准输入读取一行,但不检查缓冲区大小}·gets() 会一直读取直到遇到换行符,如果用户输入超过 7 个字符(例”AAAAAAAAAAA”),数据就会溢出 buffer 的边界,覆盖掉栈上 buffer 之后的内存。 ·被覆盖的内容取决于输入长度: ·如果输入长度刚好 8 字节(含 ‘\0’),可能会覆盖保存的 EBP。 ·如果输入更长,会覆盖返回地址,甚至更上层栈帧的数据。 其他危险函数还包括 strcpy()、strcat()、sprintf() 等,如果不指定长度限制,都可能引发栈溢出。
栈溢出应用场景
这一部分对逆向也挺重要的
覆盖局部变量
int authenticated = 0;char password[8];gets(password);if (strcmp(password, "secret") == 0) { authenticated = 1;}如果输入足够长,可能覆盖 authenticated 变量,使其非零,从而绕过验证。
覆盖保存的 EBP
mov ax, 1000H栈段
将长度为N(N=<64)的一组地址连续、起始地址为16的倍数的内存单元当作栈来使用,从而定义了一个栈段
sub
sub ax,bx
表示将寄存器 ax 的值减去寄存器 bx 的值,结果存回 ax。
只修改IP内容
jmp某一合法寄存器
jmp ax
功能:用寄存器中的值修改IP
字型,字节数据
字型数据与字节数据
两字节与一字节的区分,N处字型数据读取的时候取N+1和N,从N+1开始读
大小端序
小端序(Little-Endian):低位字节存放在低地址,高位字节存放在高地址。x86 架构使用小端序。
大端序(Big-Endian):高位字节存放在低地址,低位字节存放在高地址。网络协议、ARM(可配置)等可能使用。
小端序:
地址 0x1000: 0x78地址 0x1001: 0x56地址 0x1002: 0x34地址 0x1003: 0x12大端序
地址 0x1000: 0x12地址 0x1001: 0x34地址 0x1002: 0x56地址 0x1003: 0x78x86 是小端序,看到内存中的字节顺序要反过来理解多字节数值。
mov
一般有如下形式
mov 寄存器,数据
mov 寄存器,段寄存器
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器mov ax,b
将b送入ax,即ax=8
mov ax,bx
将寄存器bx的数据送入ax,ax=bx
add
add ax,b
将ax的数值加上b,ax=ax+8
add ax,bx
将ax的数值加上bx,ax=bx+ax
DS和[address]
执行指令时,8086CPU自动收取DS中的数据为内存单元的地址。
mov从10000H中读取数据:
10000H(1000:0)
将段地址1000H放入ds
用mov al,[0]完成传送([]指的是一个内存单元,这个内存单元的偏移地址是0,它的段地址默认放在ds中)
数据-> 通用寄存器 -> 段寄存器
cmp
比较
cmp rax, 31
后续通常会紧跟一条条件跳转指令(如 je、jg、jl 等),根据比较结果改变程序执行流。
je(相等时跳转):若 RAX == 31。 jne(不相等时跳转):若 RAX != 31。 jg(有符号大于):若 RAX > 31。 jl(有符号小于):若 RAX < 31。 ja(无符号大于):若 RAX > 31(无符号比较)。 jb(无符号小于):若 RAX < 31(无符号比较)。
python
range(a,b,c)
从a开始,到b结束(不包含),长度为c
如果a长度是 34,那么会依次取 0, 2, 4, 6, …, 32。
a[i+x]
这是字符串切片操作,从a中取出从i开始、长度为x的子串。
异或
异或(XOR,符号为 ^)是一种按位运算
性质
交换律:a ^ b = b ^ a
结合律:(a ^ b) ^ c = a ^ (b ^ c)
与0异或:a ^ 0 = a(0不变)
与自身异或:a ^ a = 0(自身抵消)
可逆性(自反性):如果 c = a ^ b,那么 a = c ^ b
在逆向中的应用
cipher = ord(flag[i]) ^ keyresult += str(hex(cipher))[2:].zfill(2)加密时用的是key,解密的时候也用key
original = cipher ^ keychr()
将整数转换成对应字符
chr(65) 返回A
append
是列表的方法,用于在列表末尾添加一个元素。
”.join(a)
是字符串的 join 方法,它把列表a中的所有字符串元素(这里是单个字符)连接成一个长字符串,连接符是空字符串 ”(即直接拼接)。
假设解密过程得到 original 依次为:112, 121, 116, 104, 111, 110
chr(112) -> ‘p’
chr(121) -> ‘y’
chr(116) -> ‘t’
chr(104) -> ‘h’ chr(111) -> ‘o’
chr(110) -> ‘n’ 列表 flag_chars 变成 [‘p’,‘y’,‘t’,‘h’,‘o’,‘n’] 然后 ”.join(flag_chars) 得到 “python”。
这样,最终打印出来的就是解密得到的 flag。
%
在数学中,取模运算(Modulo Operation)就是求两个数相除的余数。 对于整数 a 和 b(b ≠ 0),a % b 的结果是 a 除以 b 后得到的余数。
a = b * (a // b) + (a % b)
其中a // b是整数除法(在 Python 中称为地板除,结果向下取整)a % b 就是余数,且0 ≤ 余数 < |b|(当 b > 0 时成立,负数情况见后)。
正数取模
print(7 % 3) # 输出 1负数取模
不同编程语言对负数取模的处理可能不同。Python 的取模运算遵循一个原则:余数的符号与除数(第二个操作数)相同。
并且保证:0 ≤ 余数 < |b| 当 b > 0;如果 b < 0,则 b < 余数 ≤ 0。
print(-7 % 3) # 输出 2当 b > 0 时,a % b 的结果在 [0, b-1] 之间。
当 b < 0 时,a % b 的结果在 [b+1, 0] 之间(即非正)。
满足公式:a = (a // b) * b + (a % b) 永远成立。
常见用途
判断奇偶
num = 7if num % 2 == 1: print("奇数")else: print("偶数")循环索引
arr = ['a', 'b', 'c']index = 5print(arr[index % len(arr)]) # 输出 'c',因为 5 % 3 = 2限制数值范围
value = 123# 将 value 限制在 0~9 之间value %= 10 # 123 % 10 = 3获取数字的个位数、十位数等
num = 12345last_digit = num % 10 # 5second_last = (num // 10) % 10 # 4周期性任务(每隔n次执行一次)
for i in range(100): if i % 10 == 0: print(f"第 {i} 次执行特殊操作")密码学简单加密校验(以后再补)
C
strlen
C 语言标准库中的一个函数,用于计算字符串的长度(不包括结尾的空字符 ‘\0’
#include <string.h>size_t strlen(const char *s);参数:s 是指向字符串首字符的指针(必须是 \0 结尾)。 返回值:size_t 类型(通常是无符号整数),表示字符串的字符数,不包括结尾的 \0。 strlen 从 s 指向的地址开始,逐字节读取内存,直到遇到值为 0 的字节(即 \0)为止,然后返回读取的字节数(不包括最后的 \0)。
示例:
char str[] = "hello";int len = strlen(str); // len = 5当看到 strlen 调用,往往表示程序正在处理字符串,可能是验证输入长度、计算缓冲区大小等。 反汇编中可能直接出现 call strlen,也可能被编译器优化为内联循环(如 repne scasb)。
C++
void
void 关键字表示“无类型”或“空” 当函数不需要返回任何数据时,返回类型声明为 void
void*
void* 是一种特殊指针,可以指向任意类型的数据,但不能直接解引用(因为不知道类型)。通常用于底层内存操作或泛型接口:
int a = 10;void* ptr = &a; // 可以指向 int// *ptr = 20; // 错误!不能解引用 void*int* intPtr = (int*)ptr; // 需要强制转换后才能使用Some information may be outdated