内存寻址与堆栈

一、上节课作业回顾

  • 寄存器结构:32位、16位、8位寄存器的包含关系(如 EAX → AX → AH/AL)。
  • MOV、ADD、SUB 等指令的所有合法形式,强调操作数宽度一致。
  • 内存读写练习:加深对数据宽度和内存编号的理解。

二、内存数据窗口显示

数据窗口与堆栈窗口的数值顺序

  • 在寄存器窗口和反汇编窗口中,多字节数值按“高位在前,低位在后”显示(与我们书写习惯一致)。
  • 在数据窗口(内存 dump)中,顺序相反:低位在前,高位在后,以字节为单位倒序排列。
  • 例如:内存地址 0x12FFDC 处若存储 0x12345678,则数据窗口中显示为 78 56 34 12

内存编号与宽度

  • 每个内存编号对应 1 字节
  • 通过 MOV 读/写内存时,用 BYTEWORDDWORD 指定操作宽度,会同时影响连续的几个字节。
  • 示例:
    • MOV BYTE PTR DS:[地址], 值 → 只改动 1 字节。
    • MOV WORD PTR DS:[地址], 值 → 改动连续 2 字节。
    • MOV DWORD PTR DS:[地址], 值 → 改动连续 4 字节。

三、内存寻址公式

核心:所有复杂的地址表达式最终都只为了得到一个内存编号。以下五种方式涵盖了 CPU 能识别的所有寻址格式。

1. 寻址公式一:[立即数]

1
2
3
4
5
6
; 读取内存的值
MOV EAX, DWORD PTR DS:[0x13FFC4]
; 向内存写入数据
MOV DWORD PTR DS:[0x13FFC8], EBX
; 获取内存地址编号(而非内存中的内容)
LEA EAX, DWORD PTR DS:[0x13FFC4]

2. 寻址公式二:[reg]

reg 代表任意 8 个 32 位通用寄存器之一。

1
2
3
4
MOV ECX, 0x13FFD0
MOV EAX, DWORD PTR DS:[ECX] ; 读取
MOV DWORD PTR DS:[EDX], 0x87654321 ; 写入
LEA EAX, DWORD PTR DS:[EDX] ; 取地址编号

3. 寻址公式三:[reg + 立即数]

1
2
3
4
MOV ECX, 0x13FFD0
MOV EAX, DWORD PTR DS:[ECX+4] ; 读取
MOV DWORD PTR DS:[EDX+0xC], 0x87654321 ; 写入
LEA EAX, DWORD PTR DS:[EDX+4] ; 取地址编号

4. 寻址公式四:[reg + reg * {1,2,4,8}]

乘数只能是 1, 2, 4, 8。

1
2
3
4
5
MOV EAX, 0x13FFC4
MOV ECX, 2
MOV EDX, DWORD PTR DS:[EAX+ECX*4] ; 读取
MOV DWORD PTR DS:[EAX+ECX*4], 0x87654321 ; 写入
LEA EAX, DWORD PTR DS:[EAX+ECX*4] ; 取地址编号

5. 寻址公式五:[reg + reg * {1,2,4,8} + 立即数]

1
2
3
4
5
MOV EAX, 0x13FFC4
MOV ECX, 2
MOV EDX, DWORD PTR DS:[EAX+ECX*4+4] ; 读取
MOV DWORD PTR DS:[EAX+ECX*4+4], 0x87654321 ; 写入
LEA EAX, DWORD PTR DS:[EAX+ECX*4+2] ; 取地址编号

四、堆栈

1. 为什么需要堆栈?

当需要临时存储大量数据(远超 8 个寄存器)时,必须有规律地使用内存,且满足:

  • 记录存了多少数据。
  • 快速定位任意一个数据。
  • 使用完毕能方便地“丢弃”。

2. 堆栈的基本设计

用两个寄存器分别记录起始地址(栈底)和当前地址(栈顶)。

  • 栈底(BASE)固定不动。
  • 栈顶(TOP)随数据的存入和弹出而上下移动。
  • 每次存取 4 字节(演示用),栈顶的值减 4(向低地址增长)表示入栈,加 4 表示出栈。
  • 读取中间数据时,可以用 BASE + 偏移TOP + 偏移 来定位。
TOP:0X100C
BASE:0X1000

3. 手动模拟堆栈操作

压入数据(入栈)的多种实现方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
; 假设 EBX 存栈底,EDX 存栈顶,初始都指向 0x13FFDC
MOV EBX,13FFDC BASE
MOV EDX,13FFDC TOP
; 方式一:先写数据,后移动栈顶
MOV DWORD PTR DS:[EDX-4], 0xAAAAAAAA
SUB EDX, 4

; 方式二:先移动栈顶,后写数据
SUB EDX, 4
MOV DWORD PTR DS:[EDX], 0xBBBBBBBB

; 方式三:先写数据,后用 LEA 移动栈顶
MOV DWORD PTR DS:[EDX-4], 0xDDDDDDDD
LEA EDX, DWORD PTR DS:[EDX-4]

; 方式四:先用 LEA 移动栈顶,后写数据
LEA EDX, DWORD PTR DS:[EDX-4]
MOV DWORD PTR DS:[EDX], 0xEEEEEEEE

读取中间数据

1
2
3
4
5
; 通过栈底加偏移:读第 1 个压入的数(最后一个位置)
MOV ESI, DWORD PTR DS:[EBX-4]

; 通过栈顶加偏移:读第 2 个压入的数
MOV EDI, DWORD PTR DS:[EDX+4]

弹出数据(出栈)

1
2
3
4
5
6
7
; 方式一:先读值,后移动栈顶
MOV ECX, DWORD PTR DS:[EDX]
LEA EDX, DWORD PTR DS:[EDX+4] ; 或 ADD EDX,4

; 方式二:先移动栈顶,后读值
LEA EDX, DWORD PTR DS:[EDX+4]
MOV ESI, DWORD PTR DS:[EDX-4]

4. 正式堆栈操作:PUSH / POP

CPU 提供了专门的寄存器(ESP 栈顶,EBP 栈底)和指令,但我们自己也可以用任何寄存器模拟。

PUSH 指令(可压入寄存器、内存、立即数):

1
2
3
4
PUSH EAX          ; 压入 32 位寄存器
PUSH AX ; 压入 16 位寄存器
PUSH DWORD PTR DS:[地址] ; 压入内存
PUSH 0x1234 ; 压入立即数

功能等价于:SUB ESP,4 + MOV [ESP], 源操作数

POP 指令

1
2
POP EAX           ; 弹出到 32 位寄存器
POP WORD PTR DS:[地址] ; 弹出到内存

功能等价于:MOV 目标, [ESP] + ADD ESP,4

PUSHAD / POPAD

  • PUSHAD:将所有 8 个 32 位通用寄存器按固定顺序压栈。
  • POPAD:按相反顺序弹出,恢复寄存器。

五、课后作业

  1. 内存寻址练习:对五种寻址公式,逐一实现:读取内存、写入内存、获取内存地址编号。
  2. 堆栈模拟练习
    • 用 EBX 做栈底、EDX 做栈顶,连续压入 5 个不同的数。
    • 分别通过栈底加偏移和栈顶加偏移的方式读取这 5 个数到寄存器中。
    • 将这 5 个数依次弹出,并恢复栈顶到初始位置。
  3. 指令变形挑战(选做):
    • 用至少两种不同的指令组合,实现 PUSH ECX 的功能。
    • 用至少两种不同的指令组合,实现 POP ECX 的功能。
    • 思考:PUSH ESPPOP ESP 如何用其他指令等效替代?

寄存器、内存操作

一、通用寄存器结构

32 位通用寄存器共有 8 个,每个 32 位寄存器的低 16 位又可作为 16 位寄存器使用。
其中 AX、CX、DX、BX 这四个 16 位寄存器,还能进一步拆分成两个 8 位寄存器(高 8 位和低 8 位)。

寄存器 编号(二进制) 编号(十进制)
32位 16位 8位
EAX AX AL 000 0
ECX CX CL 001 1
EDX DX DL 010 2
EBX BX BL 011 3
ESP SP AH 100 4
EBP BP CH 101 5
ESI SI DH 110 6
EDI DI BH 111 7

结构关系示意图

3116 150
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI
15 0
AX
CX
DX
BX
SP
BP
SI
DI
158 70
AHAL
CHCL
DHDL
BHBL

二、MOV 指令详解

格式:MOV 目标操作数, 源操作数
作用:将源操作数的值拷贝到目标操作数。

所有合法形式(r = 通用寄存器,m = 内存单元,imm = 立即数):

1
2
3
4
5
6
7
8
9
MOV r/m8,  r8
MOV r/m16, r16
MOV r/m32, r32
MOV r8, r/m8
MOV r16, r/m16
MOV r32, r/m32
MOV r8, imm8
MOV r16, imm16
MOV r32, imm32

使用规则

  1. 源操作数可以是立即数、通用寄存器、段寄存器、内存单元。
  2. 目标操作数可以是通用寄存器、段寄存器、内存单元。
  3. 两个操作数的宽度必须一致。
  4. 源操作数和目标操作数不能同时为内存单元。

三、常用运算指令语法

以下列出 ADD、SUB、AND、OR、XOR、NOT 所有合法形式,练习时务必逐条在调试器中验证。

ADD(加法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ADD AL, imm8
ADD AX, imm16
ADD EAX, imm32
ADD r/m8, imm8
ADD r/m16, imm16
ADD r/m32, imm32
ADD r/m16, imm8
ADD r/m32, imm8
ADD r/m8, r8
ADD r/m16, r16
ADD r/m32, r32
ADD r8, r/m8
ADD r16, r/m16
ADD r32, r/m32

SUB(减法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SUB AL, imm8
SUB AX, imm16
SUB EAX, imm32
SUB r/m8, imm8
SUB r/m16, imm16
SUB r/m32, imm32
SUB r/m16, imm8
SUB r/m32, imm8
SUB r/m8, r8
SUB r/m16, r16
SUB r/m32, r32
SUB r8, r/m8
SUB r16, r/m16
SUB r32, r/m32

AND(与)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
AND AL, imm8
AND AX, imm16
AND EAX, imm32
AND r/m8, imm8
AND r/m16, imm16
AND r/m32, imm32
AND r/m16, imm8
AND r/m32, imm8
AND r/m8, r8
AND r/m16, r16
AND r/m32, r32
AND r8, r/m8
AND r16, r/m16
AND r32, r/m32

OR(或)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
OR AL, imm8
OR AX, imm16
OR EAX, imm32
OR r/m8, imm8
OR r/m16, imm16
OR r/m32, imm32
OR r/m16, imm8
OR r/m32, imm8
OR r/m8, r8
OR r/m16, r16
OR r/m32, r32
OR r8, r/m8
OR r16, r/m16
OR r32, r/m32

XOR(异或)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
XOR AL, imm8
XOR AX, imm16
XOR EAX, imm32
XOR r/m8, imm8
XOR r/m16, imm16
XOR r/m32, imm32
XOR r/m16, imm8
XOR r/m32, imm8
XOR r/m8, r8
XOR r/m16, r16
XOR r/m32, r32
XOR r8, r/m8
XOR r16, r/m16
XOR r32, r/m32

NOT(取反)

1
2
3
NOT r/m8
NOT r/m16
NOT r/m32

四、内存基础

1. 寄存器与内存的区别

  • 寄存器:位于 CPU 内部,数量少,执行速度快,价格昂贵。
  • 内存:位于主板,速度相对较慢,成本低,容量大。
  • 本质相同:都是定宽的、用于存储 0/1 数据的容器。
  • 常用寄存器:EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI(均为 32 位)。

2. 计量单位

1
2
3
4
5
6
7
BYTE     字节  = 8 bits
WORD 字 = 16 bits (2 字节)
DWORD 双字 = 32 bits (4 字节)

1 KB = 1024 BYTE
1 MB = 1024 KB
1 GB = 1024 MB

3. 内存编号与寻址

  • 内存单元数量巨大,无法逐一命名,因此用 编号(地址) 标识。
  • 每个编号对应 1 个字节(8 bits)的内存空间。
  • CPU 是 32 位或 64 位,指的是 地址编号的宽度,而非寄存器的宽度。
  • 32 位地址总线能表示的范围:0x00000000 ~ 0xFFFFFFFF,共 0xFFFFFFFF + 1 = 4 GB
1
2
3
4
5
6
7
内存编号示意图:
0x00000000
0x00000001
0x00000002
...
...
0xFFFFFFFF

4. 内存读写语法

关键点

  • [ ] 表示地址(区别于立即数)。
  • 必须指定读写宽度:byte(1字节)、word(2字节)、dword(4字节)。
  • ptr 指明后面是一个地址(指针)。
  • ds 是段寄存器前缀(现阶段固定写法,暂不深究)。
  • 练习时地址建议使用 ESP 指向的栈区域,不要随意指定(部分内存受保护,不可直接读写)。

写入内存

1
MOV DWORD PTR DS:[0x0012FFC4], 0x12345678

读取内存到寄存器

1
MOV EAX, DWORD PTR DS:[0x0012FFD0]

其他宽度示例

1
2
MOV BYTE PTR DS:[0x0012FFC4], 0xAB       ; 写 1 字节
MOV WORD PTR DS:[0x0012FFC4], 0xABCD ; 写 2 字节

五、课后作业

练习目的:深刻理解寄存器、内存单元与数据宽度。

  1. MOV 指令全部形式
  2. ADD、SUB、AND、OR、XOR 指令全部形式
  3. NOT 指令全部形式

提示:地址选择 ESP 附近的值以避免访问违规。

数据宽度、逻辑运算与通用寄存器

一、数据宽度

数学中的数字可以无限大,但在计算机中,数据受硬件限制,都有长度限制。超出宽度的部分会被直接丢弃。

1. 4 位宽度表示:假设计算机只能存储4位2进制数

假设计算机只能存储 4 位二进制数,所有可能的值如下:

1
2
3
4
0000    0001    0010    0011    0100    0101    0110    0111    
0 1 2 3 4 5 6 7
1000 1001 1010 1011 1100 1101 1110 1111
8 9 A B C D E F

通过圆圈图理解有符号数和无符号数:

8 7 B C 4 3 F 0
无符号数:0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F
有符号数:
正数:0,1,2,3,4,5,6,7
负数:-1,-2,-3,-4,-5,-6,-7,-8
    F, E, D, C, B, A, 9, 8

计算机并不区分有无符号,它只存储二进制。如何解释这些二进制,是程序员自己的事。

2. 8 位宽度表示:假设计算机只能存储8位2进制数

80 7F BF C0 40 3F FF 0
无符号数:0 ~ FF
有符号数:
正数:0 ~ 7F
负数:-1 ~ -128
    FF, FE, FD, FC, ..., 80

3. 16位宽度表示:假设计算机只能存储16位2进制数

8000 7FFF BFFF C000 4000 3FFF FFFF 0
无符号数:0 ~ FFFF
有符号数:
正数:0 ~ 7FFF
负数:-1 ~ -32768
    FFFF, FFFE, ..., 8000

4. 32 位宽度表示:假设计算机只能存储32位2进制数

80000000 7FFFFFFF BFFFFFFF C0000000 40000000 3FFFFFFF FFFFFFFF 0
无符号数:0 ~ FFFFFFFF
有符号数:
正数:0 ~ 7FFFFFFF
负数:-1 ~ -2147483648
    FFFFFFFF, FFFFFFFE, ..., 80000000

5. 重要的计量单位

1
2
3
BYTE     字节     8 bits
WORD 字 16 bits (2 字节)
DWORD 双字 32 bits (4 字节)

二、逻辑运算

CPU 的本质是无数个开关的组合,所有运算最终都由基本的逻辑运算完成。

1. 或运算(OR)

规则:只要有一个 1,结果就为 1
汇编指令:OR
C 语言符号:|

1
2
3
4
    1010 0011
OR 1100 1010
——————————————
1110 1011

2. 与运算(AND)

规则:两个都为 1,结果才为 1
汇编指令:AND
C 语言符号:&

1
2
3
4
    1010 0011
AND 1100 1010
——————————————
1000 0010

3. 异或运算(XOR)

规则:不相同为 1,相同为 0
汇编指令:XOR
C 语言符号:^

1
2
3
4
    1010 0011
XOR 1100 1010
——————————————
0110 1001

4. 非运算(NOT)

规则:1 变 0,0 变 1
汇编指令:NOT
C 语言符号:!

1
2
3
NOT 1010 0011
——————————————
0101 1100

5. 逻辑运算的具体应用

(1) 计算机如何计算 2 + 3

计算机只能做逻辑运算和移位,2+3 的过程如下(假设 4 位宽度):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
   X = 2 (0010)
Y = 3 (0011)

① 异或求“不考虑进位的和”:
0010 XOR 0011 = 0001 → 存入 R

② 与运算后左移一位求“进位”:
0010 AND 0011 = 0010
0010 << 1 = 0100 → 存入 Y

③ 判断 Y 是否为 0,不为 0 则重复:
新 X = R (0001)
新 Y = 进位 (0100)

④ 再次异或:
0001 XOR 0100 = 0101 → R = 0101

⑤ 再次求进位:
0001 AND 0100 = 0000
0000 << 1 = 0000 → Y = 0,结束

结果:R = 0101 = 5

(2) 获取某个二进制位的值

如要测试 8F(1000 1111)的第 4 位(从右数,起始为 0):

1
2
3
4
    1000 1111
AND 0000 1000 ← 只有第 3 位(0起点)为 1
——————————————
0000 1000 ≠ 0,说明该位为 1

(3) 最简单的加密:异或

加密:数据 XOR 密钥 = 密文
解密:密文 XOR 密钥 = 数据

示例:明文 2026,密钥 68

1
2
3
4
    0010 0000 0010 0110  (2026)
XOR 0110 1000 0110 1000 (6868)
————————————————————————————————————
0100 1000 0100 1110 (484E) → 密文

解密时再用 6868 异或一次,即得回 2026。


三、通用寄存器

1. 32 位通用寄存器表

寄存器 主要用途 编号 存储数据范围
EAX 累加器 0 0 - 0xFFFFFFFF
ECX 计数 1 0 - 0xFFFFFFFF
EDX I/O指针 2 0 - 0xFFFFFFFF
EBX DS段的数据指针 3 0 - 0xFFFFFFFF
ESP 堆栈指针 4 0 - 0xFFFFFFFF
EBP SS段的数据指针 5 0 - 0xFFFFFFFF
ESI 字符串操作的源指针;SS段的数据指针 6 0 - 0xFFFFFFFF
EDI 字符串操作的目标指针;ES段的数据指针 7 0 - 0xFFFFFFFF

记住顺序:EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI(编号 0~7)。这 8 个是 32 位通用寄存器,每个能存 4 字节。

2. MOV 指令

格式:MOV 目标操作数, 源操作数
功能:将源操作数的值复制到目标操作数中。

示例

1
2
3
MOV EAX, 0x1          ; 将 1 放入 EAX
MOV ECX, 0x12345678 ; 将 0x12345678 放入 ECX
MOV EDX, EBX ; 将 EBX 的值复制到 EDX

四、课后作业

  1. 八进制数2-5在计算机中的结果是:1 777 777 777 777 777 777 775 ,为什么?
  2. 使用异或对87AD6进行加密后再进行解密,解密密钥:5
  3. 只用逻辑运算计算2-3?(涉及内容:逻辑运算、移位、数据宽度)
  4. 按照顺序记住8个通用寄存器的名称
  5. 使用MOV指令修改8个寄存器的值,单步执行观察修改后的结果

进制毕业

一、上节课作业回顾

二、三进制 —— 彻底忘记数字

1. 正常三进制

定义:由 3 个符号组成,分别是 0, 1, 2逢 3 进 1

从 0 开始写数:

1
2
3
4
5
6
7
8
9
10
11
12
13
0000  0001  0002
0010 0011 0012
0020 0021 0022
0100 0101 0102
0110 0111 0112
0120 0121 0122
0200 0201 0202
0210 0211 0212
0220 0221 0222
1000 1001 1002
1010 1011 1012
1020 1021 1022
1100 1101 1102

2. 自定义三进制(符号重排)

定义:由 3 个符号组成,分别是 2, 0, 1逢 3 进 1

1
2
3
4
5
6
7
8
9
2222 2220 2221 2202 2200 2201 2212 2210 2211
2022 2020 2021 2002 2000 2001 2012 2010 2011
2122 2120 2121 2102 2100 2101 2112 2110 2111
0222 0220 0221 0202 0200 0201 0212 0210 0211
0022 0020 0021 0002 0000 0001 0012 0010 0011
0122 0120 0121 0102 0100 0101 0112 0110 0111
1222 1220 1221 1202 1200 1201 1212 1210 1211
1022 1020 1021 1002 1000 1001 1012 1010 1011
1122 1120 1121 1102 1100 1101 1112 1110 1111

三、课堂练习(自定义进制填表)

请在 30 分钟内完成以下表格,直接写在 Excel 或笔记中,发给辅导老师。

  • 4 进制定义:0,1,2,3 和 自定义 3,1,0,2
  • 5 进制定义:0,1,2,3,4 和 自定义 4,2,0,1,3
  • 6 进制定义:0,1,2,3,4,5 和 自定义 5,3,1,0,2,4

目的:彻底掌握“符号任意、逢 N 进一、查数推导”的进制本质,为加密思想和底层理解打基础。

四、课后作业

  1. 9 进制计算
    定义:由 9 个符号组成,分别是 2, 9, 1, 7, 6, 5, 4, 8, 3,逢 9 进 1。

计算:123 + 234 = ?

1
2
3
4
   123
+ 234
————————
725
  1. 10 进制计算
    定义:由 10 个符号组成,分别是 !, @, $, %, ^, &, *, M, X, Z,逢 10 进 1。

自定义符号十进制加法表

计算:@$$X + %MZ& = ?

1
2
3
4
  @$$X
+ %MZ&
————————
&!$%

进制基础(幼儿园数学)

一、为什么要有数字?为什么要学进制?

  • 现实世界需要数字来方便描述。
  • 计算机用数字描述世界,内部存储全部是二进制(0和1)。
  • 计算机既可以描述现实存在的规则(如台球游戏),也可以描述全新的规则(如搜索引擎算法)。
  • 学习进制,本质是理解计算机如何用数字干活。
  • 计算机是‘可计算数学逻辑’在物理约束下的一种离散化、时序化的工程近似实现。

二、进制的定义

  • 十进制:由十个符号组成,分别是 0,1,2,3,4,5,6,7,8,9,逢十进一。
  • 九进制:由九个符号组成,分别是 0,1,2,3,4,5,6,7,8,逢九进一。
  • 十六进制:由十六个符号组成,分别是 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F,逢十六进一。

通用定义:N 进制由 N 个符号组成,逢 N 进一。

三、课堂练习:从零开始数数

1. 二进制从 0 写到 30

1
2
3
4
5
6
00001, 00010, 00011, 00100, 00101, 00110, 00111, 01000, 01001, 01010, 
01011, 01100, 01101, 01110, 01111, 10000, 10001, 10010, 10011, 10100,
10101, 10110, 10111, 11000, 11001, 11010, 11011, 11100, 11101, 11110,
11111, 100000, 100001, 100010, 100011, 100100, 100101, 100110, 100111,
101000, 101001, 101010, 101011, 101100, 101101, 101110, 101111, 110000,
110001, 110010

2. 八进制从 0 写到 80

1
2
3
4
5
6
7
8
9
10
1  2  3  4  5  6  7 10  
11 12 13 14 15 16 17 20
21 22 23 24 25 26 27 30
31 32 33 34 35 36 37 40
41 42 43 44 45 46 47 50
51 52 53 54 55 56 57 60
61 62 63 64 65 66 67 70
71 72 73 74 75 76 77 100
101 102 103 104 105 106 107 110
111 112 113 114 115 116 117 120

四、进制的本质

  • 十进制并不一定非要是 0~9,符号可以任意指定。
  • 例如:定义十进制符号为 m,x,z,6,5,8,o,c,0,2&,#,(,|,',),!,:,/,=那么在这些符号上的运算规则完全由查表决定。
  • 如果通信双方私自定义一套进制符号,外人无法直接看懂,这就形成了加密
  • 本质:N 个符号 + 逢 N 进一,符号可以自定义,运算规则通过查表得出。

五、课后思考

  • 2 + 3 = 1 在什么条件下成立?
  • 将下面的二进制数用十六进制表示
    1100 1011 0101 0100 1110 1011 0101 0111 1011 0100 1010 1011
    ∎ 0xCB54EB57B4AB
  • 将下面十六进制数用二进制表示
    487FDC120ACE69B953FE
1
2
3
4
0100 1000 0111 1111 1101 1100  
0001 0010 0000 1010 1100 1110
0110 1001 1011 1001 0101 0011
1111 1110
  • 二进制从1写到100
1
2
3
4
5
6
7
8
9
10
11
12
13
0000001 0000010 0000011 0000100 0000101 0000110 0000111 0001000 
0001001 0001010 0001011 0001100 0001101 0001110 0001111 0010000
0010001 0010010 0010011 0010100 0010101 0010110 0010111 0011000
0011001 0011010 0011011 0011100 0011101 0011110 0011111 0100000
0100001 0100010 0100011 0100100 0100101 0100110 0100111 0101000
0101001 0101010 0101011 0101100 0101101 0101110 0101111 0110000
0110001 0110010 0110011 0110100 0110101 0110110 0110111 0111000
0111001 0111010 0111011 0111100 0111101 0111110 0111111 1000000
1000001 1000010 1000011 1000100 1000101 1000110 1000111 1001000
1001001 1001010 1001011 1001100 1001101 1001110 1001111 1010000
1010001 1010010 1010011 1010100 1010101 1010110 1010111 1011000
1011001 1011010 1011011 1011100 1011101 1011110 1011111 1100000
1100001 1100010 1100011 1100100 1100101 1100110 1100111 1101000

作者题外话:这里你可以发现每一个数行列都是唯一对应的,也就意味着可以通过行列确定对应的数

六、计算机与数字的关系

  1. 计算机内部所有数据(程序、图片、声音)都以二进制存储。
  2. 受硬件限制(高/低电平),二进制只有 0 和 1,足以表达一切。
  3. 因为二进制书写太长,人们用十六进制作为简写形式。
  4. 四位二进制能表示的范围:0000 ~ 1111,正好对应 0 ~ F

七、进制运算——查表法

核心思想:进制不需要互相转换,一切运算都通过查表(数数)实现。

1. 八进制加法表(通过数数构建)

例如:1+7 就是从 1 往后数 7 个,1→2→3→4→5→6→7→10,所以 1+7=10。

完整加法表如下

2. 八进制乘法表(由加法表推导)

例如:3×5 = 5+5+5 = 12 + 5 = 17(查加法表得出)。
完整乘法表(下三角):

减法和除法同样是查加法表和乘法表,只不过方向反过来。

八、课堂小测(八进制运算)

1
2
3
4
5
6
7
    277         276         236         234
+ 333 * 54 - 54 / 4
———————— —————————— ———————— ——————————
632 1370 162 47
1666
——————————
20250

九、课后作业

  1. 编写 7 进制 加法表和乘法表,并计算:

  • 23456 + 54356 = ?
  • 5621 - 653 = ?
  • 234 × 65 = ?
1
2
3
4
5
6
7
8
  23456       5621        234
+ 54356 - 653 x 65
———————— ———————— ————————
111145 4635 1536
2103
————————
22566

  1. 编写 十六进制 加法表和乘法表,并计算:

    我没招了,十六进制表格太大了,上面7-8进制还是写过限制尺寸的,在手机上根本看不了的效果,这里放图片了

    实际上博客里直接用代码实现的地方博主都是实操过的,用代码块实现主要是为了方便复现

    十六进制加法表

    十六进制乘法表

    • 2D4E6 + CF3A6 = ?
    • 5FD1 - E5A = ?
    • 2CA × A5 = ?
1
2
3
4
5
6
7
   2D4E6      5FD1        2CA
+ CF3A6 - E5A x A5
—————————— ———————— ————————
FC88C 5177 DF2
1BE4
—————————
1CC32

提示:所有运算请用查表法,不要先转成十进制再转回来。进制本质就是数数。

堆栈图、缓冲区溢出与代码数据混淆

一、堆栈图——函数调用的完整过程

说明:Debug 版本编译的程序未做优化,保留了完整的堆栈帧和填充指令(如 0xCC),更适合学习分析。Release 版会优化掉很多步骤,但原理相通,只要把 Debug 版本的分析思路迁移过去即可。

1.1 准备工作

  • 将被分析的 EXE 拖入调试器,输入函数调用前的地址(例如 401078),下断点,运行至断点处。
  • 记录初始的 ESPEBP 值(例如 ESP = 12FF30EBP = 12FF80)。
  • 从参数压栈开始,逐步跟踪整个函数调用过程。

1.2 堆栈变化详细步骤

第1步:参数压栈

1
2
PUSH 4
PUSH 3
  • PUSH 将立即数压入堆栈,ESP 每次减 4(栈向低地址增长)。
  • 第一次 PUSH 4 后,ESP 变为 12FF2C,栈顶存放 00000004
  • 第二次 PUSH 3 后,ESP 变为 12FF28,栈顶存放 00000003
  • EBP 不变,EIP 随指令执行而变化。

第2步:CALL 指令

1
CALL 00401005 
  • CALL 完成两件事:
    1. 返回地址(当前指令的下一条指令地址,例如 401081)压入堆栈,ESP 再次减 4,变为 12FF24
    2. 修改 EIP 为被调用函数的首地址(本例中会经过一个 JMP 跳板,最终到达真正的函数体 401020)。
  • 此时栈顶存储的就是函数返回后应该继续执行的地址。

第3步:保存原 EBP

1
PUSH EBP
  • 将上层函数的 EBP12FF80)压栈,ESP 变为 12FF20
  • 目的:保留现场,以便函数返回时能恢复原来的栈帧基址。

第4步:建立新的栈帧基址

1
MOV EBP, ESP
  • 将当前 ESP 赋值给 EBP。此时 EBP = ESP = 12FF20
  • 之后在整个函数体内,EBP 不再变化(除非有特殊操作),作为访问参数和局部变量的基准。

第5步:开辟局部变量空间

1
SUB ESP, 44h
  • ESP 减去 0x44,即向低地址扩展 68 字节的栈空间。
  • 12FEDC12FF20 之间的区域就是本函数的 缓冲区(存放局部变量)。

第6步:保存其它寄存器

1
2
3
PUSH EBX
PUSH ESI
PUSH EDI
  • 继续将 EBXESIEDI 的值压栈,ESP 依次变为 12FED812FED412FED0
  • 目的依然是保留现场,避免子函数破坏上层寄存器的值。

第7步:用 0xCC 填充缓冲区(Debug 特性)

1
2
3
4
LEA EDI, DWORD PTR SS:[EBP-44h]
MOV ECX, 11h
MOV EAX, 0CCCCCCCCh
REP STOS DWORD PTR ES:[EDI]
  • LEAEBP-44h 的地址(即缓冲区起点 12FEDC)加载到 EDI
  • ECX 赋值 0x11(17次),EAX 赋值 0xCCCCCCCC
  • REP STOSEAX 的值重复写入 EDI 指向的内存,每次写入一个双字(4字节)后 EDI 自动加 4。
  • 执行结束后,从 12FEDC12FF1C 的整个缓冲区都被填充为 0xCC
  • ESP 仍为 12FED0

第8步:操作局部变量与参数

1
2
3
4
MOV DWORD PTR SS:[EBP-4], 2          ; 局部变量 int i = 2
MOV EAX, DWORD PTR SS:[EBP+8] ; 获取第一个参数(值为3)
ADD EAX, DWORD PTR SS:[EBP+0Ch] ; 加上第二个参数(值为4)→ EAX=7
ADD EAX, DWORD PTR SS:[EBP-4] ; 加上局部变量 i (2) → EAX=9

重要规则

  • [EBP-xxx] 通常访问的是 局部变量
  • [EBP+8] 开始是 参数[EBP+4]返回地址
  • 函数返回值一般通过 EAX 传递。

第9步:恢复现场(反向 POP)

1
2
3
POP EDI
POP ESI
POP EBX
  • 按入栈相反的顺序弹出,将值恢复给各个寄存器。
  • 每次 POPESP 加 4。执行后 ESP 变为 12FEDCEBP 依然为 12FF20
  • 原来的局部变量数据仍残留在栈中,但已不再属于有效栈帧。

第10步:恢复 ESP 和 EBP

1
2
MOV ESP, EBP
POP EBP
  • MOV ESP, EBPESP 重置为 12FF20,等价于撤销 SUB ESP, 44h 开辟的空间。
  • POP EBP 从栈顶弹出之前保存的原 EBP 值(12FF80),并恢复给 EBP 寄存器。此时 ESP 变为 12FF24

第11步:RET 指令返回

1
RETN
  • RET 将当前栈顶保存的 返回地址401081)弹出并放入 EIP
  • ESP 加 4,变为 12FF28。程序跳转到调用函数的下一条指令继续执行。

第12步:调用者清理参数(外平栈)

1
ADD ESP, 8
  • 调用者在 CALL 之前压入了两个参数(43),现在通过 ADD ESP, 8ESP 调整为 12FF30,恢复到调用前的值,实现 堆栈平衡
  • 最终 EBP = 12FF80ESP = 12FF30,与初始状态完全一致。

堆栈图交互演示(以下为基于 JS 加载的示意图。如禁用了 JS 加载,可忽略该区块):


二、缓冲区溢出基本原理

掌握了函数调用时堆栈的布局之后,就很容易理解缓冲区溢出攻击。

关键点

  • [EBP+4] 存放的是函数的返回地址,函数执行 RET 时会将其弹出到 EIP
  • 如果在局部变量区(缓冲区)写入的数据超过了分配的空间,就可能覆盖到 [EBP+4],从而篡改返回地址,让程序跳转到任意代码去执行。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

void Attack() {
while(1) {
printf("攻击程序\n");
}
getchar();
}

int main(int argc, char* argv[]) {
int arr[5] = {0};
arr[6] = (int)Attack; // 越界写入,覆盖返回地址
return 0;
}

原理分析

  • 数组 arr 有 5 个元素,位于栈上。arr[6] 正好对应到 [EBP+4] 的位置(取决于编译器生成的堆栈布局)。
  • Attack 函数的地址写入 arr[6],当 main 函数返回时,RET 指令会将该地址弹出到 EIP,从而开始执行 Attack,形成死循环。

作业:请分析以下代码为何会进入死循环,并用调试器验证堆栈变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void HelloWorld() {
int i = 0;
int a[] = {1,2,3,4,5,6,7,8,9,10};
for(i = 0; i <= 10; i++) {
a[i] = 0; // 当 i=10 时越界,覆盖了返回地址
printf("Hello World!\n");
}
}

int main(int argc, char* argv[]) {
HelloWorld();
getchar();
return 0;
}

三、代码还是数据——反调试的艺术

正向开发中,编译器会区分代码段和数据段。但在逆向工程师眼里,内存中的二进制没有绝对的代码与数据之分,我们让它是什么,它就是什么

3.1 全局变量与局部变量

1
2
3
4
5
6
int x = 100;    // 全局变量,位于 .data 段,程序整个生命周期存在

int Plus() {
int y = 2; // 局部变量,位于栈中,函数返回后即失效
return x + y;
}
  • 局部变量在堆栈中,通过 [EBP-xxx] 访问;全局变量在文件中固定的数据段。
  • 理解这一点是理解“把代码藏到数据区”的基础。

3.2 数组的本质

1
2
int nBuffer[] = {1,2,3,4,5,6,0};
char szBuffer[] = {0x41, 0x61, 0x12, 'A', 0};
  • 数组里装的都是二进制数据,无论元素是整数还是字符,最终在内存中都是连续的字节序列。
  • 代码编译后也是一串二进制机器码,因此完全可以“伪装”成数组。

3.3 函数指针调用

1
2
3
4
5
6
7
8
9
10
11
12
int Plus(int x, int y) {
return x + y;
}

typedef int (*pFunction)(int x, int y);

int main() {
int z = Plus(1,2); // 直接调用
pFunction p = (pFunction)Plus;
int m = p(4,5); // 通过函数指针调用
return 0;
}
  • 在反汇编窗口中观察,直接调用和函数指针调用最终都会跳转到同一段机器码。
  • 函数名在编译后只是一个地址,与数组名没有本质区别。

3.4 将代码藏在数据区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned char loc[] = {
0x55,0x8B,0xEC,0x83,0xEC,0x40,0x53,0x56,0x57,0x8D,0x7D,
0xC0,0xB9,0x10,0x00,0x00,0x88,0xB8,0xCC,0xCC,0xCC,0xCC,
0xF3,0xAB,0x8B,0x45,0x08,0x03,0x45,0x0C,0x5F,0x5E,0x5B,
0x8B,0xE5,0x5D,0xC3
}; // 这就是一个加法函数的机器码,但被声明为全局数组,存在 .data 段

typedef int (*fun)(int x, int y);

int main() {
fun f = (fun)&loc;
printf("%x", f(4,5)); // 调用了数据区里的“函数”
return 0;
}
  • 这段代码将加法函数的机器码硬编码在全局数组中。
  • 当通过函数指针调用 f 时,CPU 会跳转到数据段执行这些二进制指令,功能完全正确。
  • 这种技术常用于加壳反调试,让代码隐藏在非预期的区域,增加分析难度。

实践要点

  • 在 OD 等调试器中,若在数据区下断点,调试器会发出警告“代码在数据区”,甚至可能导致错误。这正是对抗逆向的一种手段。
  • 如果将来你在分析时发现代码段也全是 0x00,很可能是遇到了强力保护,应谨慎处理。

总结

  • 画透堆栈图是后续所有分析(函数调用约定、缓冲区溢出、漏洞利用、脱壳)的绝对基础。
  • 理解 EBPESP 的作用,以及返回地址的位置,才能理解“溢出”的原理。
  • 二进制无性,数据可为代码,代码可为数据——这是逆向分析与正向开发在思维上的根本区别。
  • 课后务必动手完成死循环程序的分析,用调试器验证堆栈变化,加深理解。

汇编基础:逻辑运算、寄存器与堆栈

一、逻辑运算(位运算)

引入:计算机只认识二进制(0和1),它是如何进行复杂的加减乘除、加密解密等运算的呢?
答案就是通过逻辑运算(位运算) 来实现。这是CPU运算的基石。

1.1 基本逻辑运算

或运算(OR)

  • 规则:两个操作位中,只要有一个为1,结果就为1。相当于电路中的“并联”开关,任一开关闭合,灯泡就会亮。
  • C语言符号|
  • 汇编指令OR

1
2
3
4
   1011 0001 0101 0101
OR 0101 0101 0101 0101
———————————————————————
1111 0101 0101 0101

与运算(AND)

  • 规则:两个操作位中,都为1时,结果才为1。相当于电路中的“串联”开关,必须两个开关都闭合,灯泡才会亮。
  • C语言符号&
  • 汇编指令AND

1
2
3
4
    1011 0001 0101 0101
AND 0101 0101 0101 0101
———————————————————————
0001 0001 0101 0101

异或运算(XOR)

  • 规则:两个操作位,不同(相异)时为1,相同(都为0或都为1)时为0
  • 核心特性:一个数A与另一个数B异或两次(A XOR B XOR B),结果会还原为A本身。这一特性使其在加密算法中非常常用。
  • C语言符号^
  • 汇编指令XOR
1
2
3
4
    1011 0001 0101 0101
XOR 0101 0101 0101 0101
———————————————————————
1110 0100 0000 0000

非运算(NOT)

  • 规则:单操作数运算,按位取反。1变成0,0变成1
  • 汇编指令NOT

1
2
3
NOT 1011 0001 0101 0101
———————————————————————
0100 1110 1010 1010

左移运算(SHL)

  • 规则:将二进制位整体向左移动指定位数。高位丢弃,低位补0。每左移一位,相当于乘以2。
  • 汇编指令SHL
1
2
3
4
   1011 0001 0101 0101
SHL 1 (左移一位)
———————————————————————
0110 0010 1010 1010

1.2 逻辑运算的应用

1. CPU如何做加法?(以 2+3 为例)

CPU只会做逻辑运算,所有算术运算(加、减、乘、除)都必须分解成逻辑运算和移位来实现。以下是 2+3 的计算过程:

  • 2 的二进制:0010
  • 3 的二进制:0011

第一步:不考虑进位的加法(异或)

1
2
3
4
    0010
XOR 0011
————————
0001 (结果存入R)

第二步:计算进位(与运算 + 左移)

1
2
3
4
5
6
7
    0010
AND 0011
————————
0010 (与运算结果)
SHL 1 (左移一位,得到进位值)
————————
0100 (进位值,存入Y)

第三步:判断运算是否结束
判断第二步得到的进位值(0100)是否为 0。若不为 0,则需将第一步的结果(0001)和这个进位值(0100)作为新的加数,重复上述过程。

新一轮运算:

  • 新加数X:0001
  • 新加数Y:0100

重复第一步:

1
2
3
4
    0001
XOR 0100
————————
0101 (新结果,存入R)

重复第二步:

1
2
3
4
5
6
7
   0001
AND 0100
————————
0000 (与运算结果为0)
SHL 1
————————
0000 (新进位值为0)

此时,新进位值为 0,循环结束。最终结果就是 R 中的值 0101,即十进制的 5

2. 一个简单的加密/解密算法(异或加密)

利用异或的特性,可以设计一个最简单的加密方案。

  • 客户端(发送方)与服务器端(接收方)需要事先约定一个密钥
  • 加密过程密文 = 明文 XOR 密钥
  • 解密过程明文 = 密文 XOR 密钥

示例:加密“20”
假设我们要发送的明文是 20,密钥是 26

  1. 明文 20 的二进制0010 0100
  2. 密钥 26 的二进制0001 1010
  3. 加密(XOR)
1
2
3
4
    0010 0100  (明文: 20)
XOR 0001 1010 (密钥: 26)
——————————————————————————
0000 1110 (密文: 14)

客户端将加密后的 14 发送给服务器。服务器收到后,用同样的密钥 26 进行解密:

  1. 解密(XOR)
1
2
3
4
    0000 1110  (密文: 14)
XOR 0001 1010 (密钥: 26)
————————————————————————————————————
0010 0100 (还原为明文: 20)

二、寄存器与内存

引入:我们知道了CPU怎么算,但计算的数据和结果该保存在哪里呢?
计算机中有两个核心的存储部件:CPU内部的寄存器和主板上的内存。它们本质都是存储0和1的“容器”,没有区别。

  • 寄存器:集成在CPU内部,读写速度极快但容量小、价格昂贵。
  • 内存:独立于CPU,读写速度相对慢但容量大、价格便宜。

2.1 32位通用寄存器

x86架构CPU提供了8个32位的通用寄存器,用于临时存储数据。它们是:

寄存器名 约定用途(了解即可,无需硬背)
EAX 累加器,常用于存放函数返回值
ECX 计数器,常用于循环
EDX 数据寄存器
EBX 基址寄存器
ESP 堆栈指针,始终指向栈顶
EBP 基址指针,常用于访问堆栈内的参数和局部变量
ESI 源变址寄存器
EDI 目的变址寄存器

“32位”意味着每个寄存器都是一个能存放32个二进制位(即4字节)的容器。

2.2 如何使用寄存器(基础汇编指令)

1
2
3
4
5
6
7
8
9
10
11
12
; 1. 将立即数存入寄存器
MOV EAX, 12345678H ; 将十六进制数12345678H 放入 EAX

; 2. 加法指令
ADD EAX, 1 ; EAX = EAX + 1

; 3. 寄存器间运算
MOV ECX, 2
ADD EAX, ECX ; EAX = EAX + ECX

; 4. 减法指令
SUB EAX, 3 ; EAX = EAX - 3

三、内存

当数据量远超8个寄存器的容量时,就必须使用内存。

3.1 什么是32位/64位计算机?

这个位数的核心指的是CPU的寻址宽度,即它能访问的内存地址的最大范围。

  • 32位CPU:地址总线宽度为32位,能表示的地址编号范围是 0x000000000xFFFFFFFF。每个地址指向一个字节,因此最大能直接管理的物理内存是 2^32 字节,即 4GB
  • 64位CPU:同理,能管理的理论内存上限远超4GB。

理解误区:不是因为“寄存器是32位的”才叫32位计算机,因为有些寄存器(如段寄存器)位数远超32位。

3.2 内存读写(寻址公式)

要读写内存,必须知道它的“门牌号”,即内存地址。以下是五种获取内存地址和读写数据的寻址方式。

说明:在编写汇编指令时,段寄存器前缀规则如下:

  • 地址如果是直接的数字(立即数):用 DS
  • 地址中包含了 ESPEBP:用 SS
  • 地址中包含了 EDI:用 ES
    (和16汇编有不同,在32位平坦模式下,段寄存器的作用主要是标识内存属性,与寻址本身关系不大,初学时可先按此规则套用。)

寻址公式一:[立即数]

1
2
3
4
5
6
7
8
9
10
; 读取内存的值(将地址0x213456里的4字节内容放到EAX中)
MOV EAX, DWORD PTR DS:[0x213456]
; 注意:EAX得到的是地址0x213456里存放的数值,而不是地址编号0x213456本身。

; 向内存写入数据
MOV DWORD PTR DS:[0x12321], 12345678H

; 获取内存地址(将地址编号0x123456本身放到ECX中)
LEA ECX, DWORD PTR DS:[0x123456]
; LEA = Load Effective Address

寻址公式二:[reg]

1
2
3
4
5
6
7
8
; reg可以是任意通用寄存器
MOV ECX, 0x13FFD0 ; 先将地址编号存入ECX
MOV EAX, DWORD PTR DS:[ECX] ; 从ECX存的地址里读取值

MOV EDX, 0x13FFD0
MOV DWORD PTR DS:[EDX], 0x12345678 ; 向EDX存的地址里写入值

LEA EAX, DWORD PTR DS:[EDX] ; 获取EDX中的地址编号

寻址公式三:[reg + 立即数]

1
2
3
4
5
6
7
8
9
MOV ECX, 0x13FFD0
; 读取地址 (ECX里的值 + 4) 处的内存数据
MOV EAX, DWORD PTR DS:[ECX + 4]

; 向地址 (ECX里的值 + 8) 处写入数据
MOV DWORD PTR DS:[ECX + 8], 0x12345678

; 获取地址编号 (ECX里的值 + 4) 本身
LEA EAX, DWORD PTR DS:[ECX + 4]

寻址公式四:[reg + reg * {1,2,4,8}]

注意:乘数只能是 1, 2, 4, 8

1
2
3
4
5
6
7
8
9
10
MOV EAX, 0x13FFD0
MOV ECX, 2
; 读取地址 (0x13FFD0 + 2*4) 处的内存数据
MOV EDX, DWORD PTR DS:[EAX + ECX * 4]

; 向该地址写入数据
MOV DWORD PTR DS:[EAX + ECX * 4], 0x12345678

; 获取该地址编号
LEA EAX, DWORD PTR DS:[EAX + ECX * 4]

寻址公式五:[reg + reg * {1,2,4,8} + 立即数]

1
2
3
4
MOV EAX, 0x13FFD0
MOV ECX, 2
; 读取、写入或获取地址,公式同上,再加一个立即数偏移
MOV EDX, DWORD PTR DS:[EAX + ECX * 4 + 10H]

四、堆栈

堆栈的本质就是一块被特殊管理的内存区域,用于存放函数调用过程中的临时变量、参数和返回地址等。

  • 关键特性:堆栈的操作遵循 后进先出 的原则。
  • 核心寄存器
    • ESP(堆栈指针):始终指向栈顶(低地址)。
    • EBP(基址指针):常用来作为基准点,访问当前函数栈帧内的参数和局部变量。
  • 重要备注:对于正向开发,堆栈细节常被编译器“透明化”;但对于逆向分析,堆栈是必须攻克的核心高地

这里放堆栈图

4.1 基本堆栈操作

  • PUSH(压栈/入栈):将数据放入堆栈。
    1. ESP = ESP - 4(栈顶向低地址移动4字节)。
    2. 将数据存入 [ESP] 指向的新位置。
  • POP(弹栈/出栈):从堆栈取出数据。
    1. [ESP] 当前指向的数据取出。
    2. ESP = ESP + 4(栈顶向高地址回退4字节)。
1
2
3
4
5
6
7
8
9
; 观察下面的操作对ESP和EBP的影响
PUSH 0x12345678
PUSH EAX
PUSH EBX

POP ECX ; ECX得到EBX压入的值,ESP+4
POP EDX ; EDX得到EAX压入的值,ESP+4
POP EAX ; EAX得到0x12345678,ESP+4
; 结论:POP操作只影响ESP,EBP保持不变。

这里放执行图

4.2 STOS与REP指令组合

  • STOS指令:将 EAX 的值拷贝到 EDI 寄存器所指向的内存地址,并自动修改 EDI
    • STOS DWORD PTR ES:[EDI] -> [EDI] = EAX; EDI 自动加/减4。
  • REP指令:重复执行紧跟的字符串指令(如STOS),重复次数由 ECX 决定。
1
2
3
4
5
6
7
8
9
; 组合用法示例:将EDI指向的内存区域,连续ECX次填充为EAX的值
MOV EAX, 0x88888888 ; 要填充的值
MOV ECX, 2 ; 重复2次
LEA EDI, DWORD PTR SS:[ESP] ; 从栈顶开始
REP STOS DWORD PTR ES:[EDI]
; 执行效果:从[ESP]开始,连续8个字节(2个双字)都被填充为0x88888888。
; EDI的值会根据EFLAG寄存器中的DF位(方向位)自动变化:
; DF=0(默认):EDI自动递增,向高地址覆盖。
; DF=1:EDI自动递减,向低地址覆盖。

这里放执行图

4.3 变形的艺术

逆向分析者通常会重点监控 PUSHPOP 等标志性指令。指令变形的核心思想是:使用其他功能等效的指令组合来代替这些敏感指令,增加分析难度,这是动态变形壳的基础技术

分析 PUSH EAX 到底做了什么?

  1. ESP 的值减4(栈顶提升)。
  2. EAX 的值存入新的栈顶地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
; 传统的 PUSH EAX
mov eax, 88888888H
push eax

; 等效的“变形”写法,可以达到同样效果:
mov eax, 88888888H
; 方式一:先存数据,再移动栈顶
mov dword ptr ds:[esp-4], eax
lea esp, dword ptr ss:[esp-4] ; LEA ESP,[ESP-4] 等同于 SUB ESP,4

; 方式二:先移动栈顶,再存数据
lea esp, dword ptr ss:[esp-4]
mov dword ptr ds:[esp], eax

同理,推导 POP ECX 的等效变形:

1
2
3
4
5
6
7
8
9
10
11
12
; 传统的 POP ECX
; 1. 从栈顶取出值给ECX
; 2. ESP = ESP + 4

; 等效变形写法:
; 方式一:先取值,再移动栈顶
mov ecx, dword ptr ss:[esp]
lea esp, dword ptr ss:[esp+4] ; LEA ESP,[ESP+4] 等同于 ADD ESP,4

; 方式二:先移动栈顶,再取值
lea esp, dword ptr ss:[esp+4]
mov ecx, dword ptr ss:[esp-4]

挑战:

push esp和pop esp怎么变形

五、课后作业

逐步画出以下代码执行时,每一行指令结束后,堆栈区域的变化图。需清晰标注出 ESPEBP 的位置,以及内存中被修改的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 假设执行前:ESP = 0x12FFC4, EBP = 0x12FFF0
push ebp ; 1. 压入旧的EBP
mov ebp, esp ; 2. 设置新的栈帧基址
sub esp, 40 ; 3. 为局部变量开辟40字节空间
push ebx ; 4. 保存现场:压入EBX
push esi ; 5. 保存现场:压入ESI
push edi ; 6. 保存现场:压入EDI
lea edi, dword ptr ss:[ebp-40] ; 7. EDI指向局部变量区的起始位置
mov ecx, 10 ; 8. 设置循环次数 (40字节 / 4字节 = 10次)
mov eax, 0CCCCCCCCH ; 9. 设置要填充的值 (0xCC是Debug模式下未初始化的栈空间填充值)
rep stos dword ptr es:[edi] ; 10. 将局部变量区填充为0xCC
mov eax, dword ptr ss:[ebp+8] ; 11. 获取第一个参数
add eax, dword ptr ss:[ebp+C] ; 12. 加上第二个参数(结果是返回值)
pop edi ; 13. 恢复现场
pop esi ; 14. 恢复现场
pop ebx ; 15. 恢复现场
mov esp, ebp ; 16. 恢复ESP,等价于 add esp, 40
pop ebp ; 17. 恢复旧的EBP
retn ; 18. 函数返回(从栈中弹出返回地址,并跳到该地址执行)

提示:这不是一个可直接调试的程序,这是一个独立的手绘练习。请将这段代码“放入”你画的内存图中,一步步模拟执行,这能帮你深入理解函数调用的标准过程(__stdcall)以及栈帧的建立和销毁。这个练习是后续学习缓冲区溢出和函数调用约定的绝对基础。

零基础入门:预备知识

本笔记整理自我所学习的课程。

一、课程体系与目标

  • 教学理念:以“逆向思维”驱动“正向知识”学习。
  • 初级班三大实战项目
    1. 反汇编引擎:将二进制机器码转换为汇编代码,是逆向工具的核心模块。
    2. PE解析器:解析Windows可执行文件的结构,从整体上理解程序的构成。
    3. 二选一项目:游戏数据逆向分析 / 编写一个加密壳(软件保护工具)。
  • 毕业课题:开发一个包含登录界面、进程注入、网络验证等功能的商业软件模板,综合运用所学。

二、核心预备知识:程序是如何储存的?

计算机中任何数据(程序、图片、视频等)最终都以二进制(0和1) 的形式存储。我们使用十六进制编辑器查看,只是为了更简洁地展示这些二进制数据。

程序图

左边是地址编号,右边是注释,中间是十六进制,任何文件存入计算机中都是以二进制存在的。

1. 进制基础

理解进制是分析数据的前提。

进制名称 基本符号 进位规则 备注
十进制 0-9 逢十进一 人类最熟悉
二进制 0, 1 逢二进一 计算机的母语
十六进制 0-9, A-F 逢十六进一 二进制的一种便捷表示法

核心转换关系:

  • 任何一种进制都是完美的是没有必要转换的,转换仅仅是为了方便人类理解。关键是要理解进制背后的计数原理,而非机械记忆转换结果,辅助理解请参考时间hh:mm:ss
  • 1位十六进制数恰好对应4位二进制数。
  • 我们之所以会想要转换是因为我们已经习惯了十进制,如果现在让你去运算M+X+Z=?你也是不会知道等于68的

//不同进制0-15

1
2
3
4
5
6
7
8
十进制	0       1       2       3
二进制 0000 0001 0010 0011
十进制 4 5 6 7
二进制 0100 0101 0110 0111
十进制 8 9 10 11
二进制 1000 1001 1010 1011
十进制 12 13 14 15
二进制 1100 1101 1110 1111
1
2
3
4
5
6
7
8
9

十六进制 0 1 2 3
二进制 0000 0001 0010 0011
十六进制 4 5 6 7
二进制 0100 0101 0110 0111
十六进制 8 9 A B
二进制 1000 1001 1010 1011
十六进制 C D E F
二进制 1100 1101 1110 1111

2. 数据存储格式

  • 为什么是两个十六进制一组?
    为什么我们用的这些软件都是一般都是将两个十六进制放在一起分割一下,却不是1个或者3个这样分割
    • 位 (bit):一个二进制位(0或1)。
    • 字节 (Byte)8个位组成1个字节,是计算机存储管理的最小单元
    • 因为 1位十六进制 = 4位二进制,所以 2位十六进制 正好表示 8位,即
      1个字节。因此编辑器用两个十六进制数为一组来显示数据,直观地反映了字节的边界。
  • 其他单位
    • 字 (Word):2字节,16位。
    • 双字 (Dword):4字节,32位。

3. 思考题:理解数据的宽度

在计算器中,已知:八进制3-2=1,八进制 2 - 3
的结果是多少?(提示:答案不是-1,这引出了数据宽度计算机如何存储数字这两个关键概念。)

八进制3-2计算结果

三、如何从整体上认识程序?—— PE文件结构

分析一个程序,首先要回答三个问题:

  1. 程序从哪里开始执行?(入口点
  2. 代码存在哪里?(代码基址
  3. 数据存在哪里?(数据基址

这些信息都记录在Windows可执行文件(.exe, .dll, .sys)的PE文件结构中。

  • 仅限于windows

概念:windows上运行的任何可执行程序都应该遵循的结构,PE结构适用于windows任何可执行程序

1.什么是数据,什么是代码

不精确但有助于理解的例子:1+2

  • 数据:程序处理的“原材料”,如 1+2 中的 12
  • 代码:指示CPU执行操作的指令,如 1+2 中的 + 号。
  • PE结构的作用,就是在一堆相同的“0和1”中,清晰地标识出哪些是代码、哪些是数据,以及它们各自的位置。

2. 手动解析PE结构(简要步骤)

以标题二中的程序图为例

没错我贴心的粘过来一份

  • 目的:在十六进制数据中,按图索骥,找到程序入口点和基址。
  • 原理:PE结构规定了一个“数据地图”,在文件的特定偏移处,存放着指向关键信息的地址。
  1. 从DOS头找到PE签名32位程序起始位置DWORD(偏移 0x3C
    处)。0x3ch=60,windows是小端序(低位在前,高位在后),所以是00000110
  2. 根据PE签名后的结构,查表“入口点地址”和“代码/数据基址”所在的字段,得到(+0x18h=24 +0x10h=16)=40
    ,入口点0000CB90;代码段+0x14h 00001000,数据段+0x18h 00010000,仅PE32有数据段
  3. 关键点:Windows系统内存取数据时采用小端序(低位在前,高位在后),读取多字节数值时需反向拼接。

当然这只是最最最简单的地方,而且实际程序更不会这么简单,所以只是一个参考

因此我们需要编写一个PE解析器工具,可以自动完成上述查找过程,帮助我们快速概览程序结构,是逆向分析的基础工具。

3. PE结构的应用:软件加壳

  • 原理:打乱、加密原有的PE结构,让分析者无法轻易看懂。
  • 应用:例如,在现有程序中插入一段自己的代码(弹出个对话框、执行特定操作),这需要对PE结构有极精确的理解,错一个字节都会导致程序崩溃。

四、从细节处分析代码:硬编码与反汇编

找到代码区后,如何理解一串二进制指令的含义?

  • 硬编码:代表特定指令的固定二进制数值,如 55 代表 push ebp
  • 反汇编:将“硬编码”翻译成人类可读的汇编语言的过程。
  • 反汇编引擎:这就是我们要做的项目,是一个将二进制自动转为汇编代码的核心模块。

五、逆向分析的完整路径

二进制数据 ➔ 反汇编 ➔ 汇编代码 ➔ 深入分析 ➔ C语言逻辑

这个过程就像把粉碎的纸屑先拼回完整的句子(汇编),再理解句子(汇编)表达的文章大意(C语言逻辑)。

六、总结与建议

  • 基础最重要:进制转换、PE结构、硬编码是逆向上的基本功,根基不牢后续会寸步难行。
  • 实践出真知:不要死记硬背PE结构,学会“查文档”和“用工具”。主动编写PE解析器等工具是最好的学习方式。
  • 正向是逆向上限的天花板:优秀的逆向工程师必须有扎实的正向开发能力(如C语言),否则分析深度将严重受限。
  • 自制工具的优势:学习编写自己的调试和分析工具(如反汇编引擎),可以有效规避游戏方对市面上流行分析工具的针对性检测。

通过webhook实现博客网站自动更新

安装webhook

1
apt install webhook

创建一个钩子

1
mkdir test.json

test.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[
{
"id": "这里填你webhook的名字,我没试过中文,也没试过带标点符号", //运行起来的效果是http://yourserver:9000/hooks/id里面你写的内容
"execute-command": "这里填你要执行的脚本的路径,没有就把这一行删了",
"trigger-rule": {
"match": {
"type": "value",
"value": "这里填你设置的密钥或者密码巴拉巴拉小魔仙",
"parameter": {
"source": "header",
"name": "X-Gitee-Token" //是的,我的仓库在gitee上,其他平台请自行查阅手册或者问ai
}
}
}
}
]

其他参数请查阅webhook的文档

https://github.com/adnanh/webhook/blob/master/docs/Templates.md

脚本的编写

1
2
3
4
5
6
#!/bin/bash

cd /path/to/ #你hexo的路径,或者git能pull的地方

{ echo "=== 执行时间:$(date +"%Y-%m-%d %T") ===";git pull origin master; } >> /你想保存的路径/名字.随便的后缀没有都可以 2>&1 #将执行的结果加上时间保存在文本里
# 如需重启服务,可在此添加:systemctl restart your-service,当然我需要重启的是容器,是不会用这条命令的

运行钩子

1
webhook -hooks test.json & #可以添加-verbose参数查看执行日志

仓库的设置

博主的仓库在gitee,下面以gitee为参考

打开仓库点击管理

打开仓库点击管理

点击webhooks

点击webhooks

点击添加webhook

点击添加webhook

向仓库推送更新

当你看到这篇文章时,说明自动部署已经成功了