今天继续汇编指令和代码功能理解的学习
一、polar--re--shell
下载附件后先试着运行一下:
从这里可以看到程序首先是输出了一段话,然后要求我们输入,再判断正确与否,错误输出了error
那我们将这个先查壳
有壳,UPX壳,脱壳

脱壳成功,再去ida分析
首先找到main函数,从截图_main_0的位置开始,以下就是main函数了
这里在地址004619A0的都是一些定义
004619A0 push ebp
- 将旧的基址指针 (EBP) 压入栈中保存。
004619A1 mov ebp, esp
- 将当前栈指针 (ESP) 的值赋给 EBP,建立新的栈帧。现在 EBP 指向当前函数栈帧的底部。
004619A3 sub esp, 13Ch
- 为局部变量在栈上分配
0x13C(即 316) 字节的空间。var_XXX之类的变量都是相对于 EBP 的偏移,代表这些局部变量。
004619A9到004619BC是典型的调试模式初始化。它将刚刚分配的 0x13C 字节的栈空间全部填充为 0xCC(即 int 3 断点指令)。这有助于在程序意外执行到未初始化的内存时触发断点,方便调试。
004619CD call j_@__CheckForDebuggerJustMyCode@4
- 调用一个特定的运行时库函数,用于检查调试器(“Just My Code”调试功能的一部分)。
接下来直接去看我们刚才的exe文件的运行效果,也就是该代码的核心环节
00461A20至00461A2D: 初始化输入缓冲区:
这一步为后续的操作留下了干净的缓冲区
- lea eax, [ebp+Str1]计算局部变量Str1(用户输入缓冲区)在内存中的有效地址,加载到eax寄存器。
lea指令用于地址计算,不访问内存数据。- 从右到左的顺序压入memset函数的三个参数:push eax(目标内存地址(刚才的eax寄存器));push 0(填充的值,用0来填充内存);push 32h(要填充的字节数,32h)
- call j_memset:调用函数,执行后,Str1的缓冲区的50个字节都初始化为0
- add esp, 0ch:清理栈空间。调用完成后调整栈指针esp来平衡堆栈
00461A30至00461A3A: 打印输入提示
push offset aPleaseInputFla: 将字符串 "please input flag\r\n" 的地址压入栈中。offset表示获取的是该字符串在内存中的地址(通常存储在程序的.rdata段),而不是字符串本身。
call sub_45A4C4: 调用函数sub_45A4C4。根据其行为(输出字符串),我们可以推断它是标准C库函数printf或puts的一个包装或特定版本。add esp, 4: 清理栈空间。因为只压入了1个参数(4字节),所以给esp加4,使栈指针恢复到调用前的状态。这一段得到的效果就是
00461A3D至00461A4B: 获取用户输入
lea eax, [ebp+Str1]: 再次获取Str1缓冲区的地址,放入eax。- 按参数顺序压栈,准备调用
sub_45A9A1:
push offset unk_519E50: 压入第一个参数。push eax: 压入第二个参数。即Str1的地址,这是用于存储输入结果的目标缓冲区。call sub_45A9A1: 调用该函数add esp, 8: 清理栈空间。清除了两个参数(共8字节)
00461A4E至00461A5B: 字符串比较这里首先是涉及到了两个字符串Str1和Str2,再看到j_strcmp(调用c库函数strcmp,对两个字符进行比较),我们需要去追踪一下Str2
最后在伪代码中看到了flag
flag是拿到了,我们继续分析main函数的流程
00461A5E至00461A7E: 检查比较结果并给予反馈最关键的主要是在跳转这里
jz short loc_461A71: 条件跳转指令。JZ(Jump if Zero) 表示如果上一条指令的结果为0(即 ZF=1),就跳转到地址loc_461A71。如果
strcmp返回0(字符串相等),就跳转到显示 "success" 的代码块;否则,顺序执行下一行显示 "error"。
然后再运行exe文件,检查flag
polar--re--拼接
首先下载得到的附件名zip,我们修改为1.zip
解压后得到的文件去查壳
没有那就直接去ida
到ida直接f5看伪代码
这里其实大概的猜出来意思了,但是看汇编指令更具有准确性

这里同样,从地址004119C0到004119ED都是前期准备工作,这里我们的main函数已经开始了
从004119F2开始就是我们的核心了
这里需要重点分析一下:
004119F2 mov [ebp+Source], offset aFlag:
进行的操作:将内存地址aFlag(存储着“flag{”)存入栈上的局部变量Source中(前面有定义)
详解:
[ebp+Source]:这是在栈上为Source指针变量预留的空间。offset aFlag:获取标号为aFlag的地址。在程序的只读数据区(通常是.rdata段),有一块内存存储着字符串常量"flag{",aFlag就是这个字符串的首地址。当我们去追踪这个aFlag时,确实是一个.rdata段
004119F9 mov [ebp+var_18], offset a03ff6cf238c5cd :
进行的操作:将内存地址
a03ff6cf238c5cd(存储着字符串"03ff6cf238c5cd8e7b4ee1e9567ad5a4}")存入栈上的局部变量var_18中。详解:
[ebp+var_18]:这是在栈上为另一个指针变量var_18预留的空间。offset a03ff6cf238c5cd:获取另一个字符串常量"03ff6cf238c5cd8e7b4ee1e9567ad5a4}"的首地址。同样追踪这个字符串,是在.rdata段
这一段的关键部分就差不多了,后面是在对缓冲区进行处理

这里是对前面的两个字符串进行了处理---拼接
00411A23至00411A30: 拼接Flag的第一步 - 复制前缀
将前面存入栈上的局部变量Source的内容加载到了eax中,,也就是讲字符串“flag{”加载到了eax
lea ecx, [ebp+Destination]: 获取目标缓冲区Destination的地址
- 参数压栈:
push ecx: 目标地址(char *destination),即Destination缓冲区。push eax: 源地址(const char *source),即字符串"flag{"。call j_strcpy: 执行调用。将字符串"flag{"拷贝到Destination缓冲区。add esp, 8: 清理栈上的2个参数(8字节)。此时,在Destination中的内容就已经有了“flag{”
00411A33至00411A40: 拼接Flag的第二步 - 追加主体和上一个类似,先将局部变量var_18的值加载到了eax,这里var_18指向第二个字符串
lea ecx, [ebp+Destination]: 再次获取Destination缓冲区的地址。此时它已经包含"flag{"。- 参数压栈:
push ecx: 目标地址(char *str),即Destination缓冲区。push eax: 源地址(const char *src),即哈希字符串。call j_strcat: 执行调用。将哈希字符串 追加到Destination缓冲区中已有内容("flag{")的后面。此刻,
Destination中的内容为:"flag{03ff6cf238c5cd8e7b4ee1e9567ad5a4}"
之后的信息,
先是提醒用户输入,然后将所输入的存入Str1中
然后再调用了c库的strcmp,比较Str1和Destination,如果相同那么久输出congratulation,否则就跳转到loc_411A97,也就是输出error
polar--re--康师傅
解压后得到exe文件,
查壳后没有壳,给ida分析
同理,前期,0045E450到004E47D都是前期准备,

这组指令使用 rep movsd、movsw、movsb 将一段数据从源地址(esi)快速复制到目标地址(edi)
mov ecx, 9: 设置计数器为9。rep 前缀会使后续的 movsd 指令重复执行 ecx (9) 次
movsd: 每次执行从 esi 复制一个 双字 (4字节) 到 edi,并递增 esi 和 edi。执行9次,共复制 36字节
movsw: 复制一个 字 (2字节)
movsb: 复制一个 字节 (1字节)
总复制字节数:36 + 2 + 1 = 39字节。源字符串 "oehnr8?>;?<?:9k>09;hj00o>:<o?81h;8h9l;t" 的长度正好是39字符

调用 memset,将用户输入缓冲区 Str2 的 50 (0x32) 字节全部初始化为 0
这一段主要是在做一些准备以及给了我们一个很乱的字符串,盲猜与flag有关
接下来进入关键环节
这里先是提醒用户输入密码并且获取用户的输入
offset 操作符获取的是字符串常量的内存地址,而不是字符串本身。
这里开始了数据的处理,进行循环(先对索引进行初始化,然后再进行跳转)
这里的[ebp+var_84] 被用作循环计数器,通常可以理解为变量 i
D1就是在对var_84这个局部变量进行初始化为0,而DB则无条件跳转到loc_45E4EC

这里调用了strlen函数,计算Str2的长度,结果返回在 eax 寄存器中![]()
这一句话非常关键,决定了程序能否继续
cmp: 比较指令
这里比较的两者分别是[ebp+var_84]和eax寄存器;第一个存的是前面的计的数;第二个存的是用户输入的字符串的长度。
如果 i 不小于 (Not Below, jnb) 字符串长度,则跳转到 loc_45E51A
0045E500至0045E518: 循环体 - 核心解密操作
0045E500 mov eax, [ebp+var_84]:将当前循环索引i的值加载到eax寄存器
0045E506 movsx ecx, [ebp+eax+Str2]:从用户输入字符串Str2中读取第i个字符,并将其符号扩展到ecx寄存器。
0045E50B xor ecx, 9:将取出的字符(现在在ecx中)与数值9进行异或 (XOR) 运算。14:将异或运算后的结果(
cl是ecx的低8位)写回用户输入字符串Str2的第i个位置。这也就是说:用户输入的字符串被逐个字符解密,解密后的结果覆盖掉原来的密文输入
但是main函数还没结束,他的结果回复还没有
在这里,调用了strcmp函数,比较Str1和Str2
回溯前面,Str1是那个乱七八糟的字符
而Str2是用户的输入进行解密的结果
那么最终比对的就是解密后的和那一串字符串
那么我们就将那个乱七八糟的字符拿来与9进行异或
def main():
# 程序中的加密字符串
ciphertext = "oehnr8?>;?<?:9k>09;hj00o>:<o?81h;8h9l;t"
# 核心解密操作:对每个字符进行 XOR 9 运算
plaintext = ''.join(chr(ord(c) ^ 9) for c in ciphertext)
# 输出结果
print("密文:", ciphertext)
print("解密结果:", plaintext)
print("Length:", len(plaintext), "characters")
# 标准的主程序入口
if __name__ == "__main__":
main()

常用的汇编指令汇总
1. 数据传送指令 (Data Transfer)
这些指令负责在寄存器、内存之间移动数据。
| 汇编指令 | 功能描述 | 高级代码对应 |
|---|---|---|
mov dest, src | 将源操作数(src)的值复制到目标操作数(dest)。注意: 这是复制,不是移动。 | dest = src; 这是最直接的对应。 |
lea dest, [src] | 加载有效地址。计算源操作数的地址,并将这个地址(而不是地址中的值)存入目标操作数。 | dest = &src; 常用于计算数组元素的地址或进行简单的算术运算。例如: lea eax, [ebx+ecx*4+10h] 对应 eax = ebx + ecx * 4 + 0x10; |
xchg op1, op2 | 交换两个操作数的值。 | swap(op1, op2); |
push src | 将操作数压入栈顶。等价于:sub esp, 4mov [esp], src | stack.push(src); |
pop dest | 将栈顶的值弹出到目标操作数。等价于:mov dest, [esp]add esp, 4 | dest = stack.pop(); |
2. 算术运算指令 (Arithmetic Operations)
| 汇编指令 | 功能描述 | 高级代码对应 |
|---|---|---|
add dest, src | 目标操作数 = 目标操作数 + 源操作数 | dest += src; |
sub dest, src | 目标操作数 = 目标操作数 - 源操作数 | dest -= src; |
inc op | 操作数自增1 | op++; |
dec op | 操作数自减1 | op--; |
imul opimul dest, srcimul dest, src1, src2 | 有符号乘法。形式多样: 1. imul eax : edx:eax = eax * eax2. imul ebx, ecx : ebx = ebx * ecx3. imul eax, ebx, 5 : eax = ebx * 5 | dest = src1 * src2; |
idiv op | 有符号除法。被除数在 edx:eax 中,除数由 op 指定。商存入 eax,余数存入 edx。 | eax = (edx:eax) / op;edx = (edx:eax) % op; |
neg op | 对操作数取补(求负数,即 0 - op) | op = -op; |
3. 位操作指令 (Bitwise Operations)
| 汇编指令 | 功能描述 | 高级代码对应 | |
|---|---|---|---|
and dest, src | 按位与 | dest &= src; 常用于将特定位清零(掩码操作)。 | |
or dest, src | 按位或 | `dest | = src;` 常用于将特定位置1。 |
xor dest, src | 按位异或 | dest ^= src;常见用法1: xor eax, eax 快速将寄存器清零(比 mov eax, 0 效率高)。常见用法2: 用于加密、解密的简单算法。 | |
not op | 按位取反 | op = ~op; | |
shl dest, countsal dest, count | 逻辑左移 / 算术左移(两者相同) | dest <<= count; 相当于乘以 $2^{count}$ | |
shr dest, count | 逻辑右移(无符号数),左侧补0 | dest >>= count; (对于无符号数) 相当于除以 $2^{count}$ | |
sar dest, count | 算术右移(有符号数),左侧用符号位填充 | dest >>= count; (对于有符号数) |
4. 流程控制指令 (Control Flow)
这是逆向中最关键的一类指令,它们决定了程序的执行路径。
| 汇编指令 | 功能描述 | 高级代码对应 |
|---|---|---|
cmp op1, op2 | 比较两个操作数,本质上是计算 op1 - op2,但不保存结果,只根据结果设置标志寄存器(EFLAGS)。 | 类似于 if (op1 == op2) 或 if (op1 > op2) 等条件判断的准备步骤。 |
test op1, op2 | 对两个操作数进行按位与,不保存结果,只设置标志位。常用于测试寄存器是否为0或某些位是否设置。 | if (op1 & op2)常见用法: test eax, eax 用于判断 eax 是正数、负数还是零。 |
jmp label | 无条件跳转 | goto label; |
je / jz label | 如果相等/为零则跳转 (ZF=1) | if (a == b) goto label; |
jne / jnz label | 如果不相等/不为零则跳转 (ZF=0) | if (a != b) goto label; |
jg / jnle label | 如果大于(有符号)则跳转 | if (a > b) goto label; (signed) |
jge / jnl label | 如果大于等于(有符号)则跳转 | if (a >= b) goto label; (signed) |
ja / jnbe label | 如果高于(无符号)则跳转 | if (a > b) goto label; (unsigned) |
jae / jnb label | 如果高于等于(无符号)则跳转 | if (a >= b) goto label; (unsigned) |
call proc | 调用函数。将下一条指令地址(返回地址)压栈,并跳转到目标地址。 | proc(); |
ret | 从函数返回。从栈顶弹出返回地址,并跳转到该地址。 | return; |
ret n | 从函数返回,并平衡栈 n 字节。常见于 stdcall 等约定。 | return; 并且调用者不需要自己清理栈参数。 |
5. 栈帧操作指令 (Stack Frame)
这些指令用于在函数开头(prologue)和结尾(epilogue)构建和销毁栈帧。
| 汇编指令 | 功能描述 | 高级代码对应 |
|---|---|---|
push ebpmov ebp, esp | 函数开头标准操作。保存旧的栈帧基址,然后将当前栈顶设置为新的栈帧基址。 | 建立一个新的栈帧,ebp 成为了访问函数参数和局部变量的基准。 |
leave | 函数结尾操作。等价于:mov esp, ebppop ebp | 拆除当前栈帧,恢复调用者的栈帧。 |
(通常与 ret 连用) |










&spm=1001.2101.3001.5002&articleId=150934138&d=1&t=3&u=8a49802262874e6a86a290b491f88c0a)
1296

被折叠的 条评论
为什么被折叠?



