逆向汇编指令与代码功能理解(2)

今天继续汇编指令和代码功能理解的学习

一、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 的地址

  • 参数压栈:
    1. push ecx: ​目标地址​(char *destination),即 Destination 缓冲区。
    2. 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{"
  • 参数压栈:
    1. push ecx: ​目标地址​(char *str),即 Destination 缓冲区。
    2. 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 movsdmovswmovsb 将一段数据从源地址(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, 4
mov [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操作数自增1op++;
dec op操作数自减1op--;
imul op
imul dest, src
imul dest, src1, src2
有符号乘法。形式多样:
1. imul eax : edx:eax = eax * eax
2. imul ebx, ecx : ebx = ebx * ecx
3. imul eax, ebx, 5 : eax = ebx * 5
dest = src1 * src2;
idiv op有符号除法。被除数在 edx:eax 中,除数由 op 指定。商存入 eax,余数存入 edxeax = (edx:eax) / op;
edx = (edx:eax) % op;
neg op对操作数取补(求负数,即 0 - opop = -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, count
sal dest, count
逻辑左移 / 算术左移(两者相同)dest <<= count; 相当于乘以 $2^{count}$
shr dest, count逻辑右移(无符号数),左侧补0dest >>= 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 ebp
mov ebp, esp
函数开头标准操作。保存旧的栈帧基址,然后将当前栈顶设置为新的栈帧基址。建立一个新的栈帧,ebp 成为了访问函数参数和局部变量的基准。
leave函数结尾操作。等价于:
mov esp, ebp
pop ebp
拆除当前栈帧,恢复调用者的栈帧。
(通常与 ret 连用)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值