x86 反汇编,涉及模块化散列
Posted
技术标签:
【中文标题】x86 反汇编,涉及模块化散列【英文标题】:x86 disassemble, involves modular hashing 【发布时间】:2020-07-22 08:40:48 【问题描述】:我已经评论了我认为代码在做什么。
我试过把aaaaaaaaa
,AAAAAAAAA
,!!!!!!!!!
,和000000000
,他们都工作。但它不会接受bbbbbbbbb
或111111111
。似乎密码接受所有 ascii 字符对应于0x21
,0x31
,0x41
,...
最初,它调用<getchar@plt>
,然后进入一个接受10 个字符的for 循环。
之后,它开始一个新的循环,我不明白。你能解释一下这个循环吗?这是在做模块化划分吗?提前致谢!
8048443: 88 44 1d e5 mov %al,-0x1b(%ebp,%ebx,1)
8048447: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804844b: 83 7d f4 09 cmpl $0x9,-0xc(%ebp) # x ($ebp - 0xc) counter 9
804844f: 7e ea jle 804843b <puts@plt+0xe7>
8048451: 8b 45 f4 mov -0xc(%ebp),%eax
8048454: c6 44 05 e5 00 movb $0x0,-0x1b(%ebp,%eax,1)
8048459: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp) # counter = 1
8048460: eb 15 jmp 8048477 <puts@plt+0x123> # start of the loop
8048462: 8b 45 f4 mov -0xc(%ebp),%eax
8048465: 83 e8 01 sub $0x1,%eax
8048468: 0f b6 44 05 e5 movzbl -0x1b(%ebp,%eax,1),%eax
804846d: 0f be c0 movsbl %al,%eax
8048470: 01 45 f0 add %eax,-0x10(%ebp)
8048473: 83 45 f4 01 addl $0x1,-0xc(%ebp)
8048477: 83 7d f4 0a cmpl $0xa,-0xc(%ebp) # 10
804847b: 7e e5 jle 8048462 <puts@plt+0x10e> # end loop
804847d: 8b 45 f0 mov -0x10(%ebp),%eax
8048480: 89 c2 mov %eax,%edx
8048482: c1 fa 1f sar $0x1f,%edx
8048485: c1 ea 1c shr $0x1c,%edx
8048488: 01 d0 add %edx,%eax
804848a: 83 e0 0f and $0xf,%eax # only look at the lowest four bits
804848d: 29 d0 sub %edx,%eax
804848f: 89 45 f0 mov %eax,-0x10(%ebp)
8048492: 83 7d f0 03 cmpl $0x3,-0x10(%ebp) # compare to 3
8048496: 75 16 jne 80484ae <puts@plt+0x15a> # go to wrong answer
8048498: b8 b4 85 04 08 mov $0x80485b4,%eax
804849d: 8d 55 e5 lea -0x1b(%ebp),%edx
80484a0: 89 54 24 04 mov %edx,0x4(%esp)
80484a4: 89 04 24 mov %eax,(%esp)
80484a7: e8 98 fe ff ff call 8048344 <printf@plt> # correct answer
80484ac: eb 0c jmp 80484ba <puts@plt+0x166>
80484ae: c7 04 24 e2 85 04 08 movl $0x80485e2,(%esp)
80484b5: e8 9a fe ff ff call 8048354 <puts@plt> # wrong answer```
【问题讨论】:
仅供参考,注释掉和评论是不同的。前者意味着将代码变成注释块使其不执行,后者意味着添加 cmets 来扩充代码。您“评论”了代码。 您遗漏了第一个循环的顶部;jle 804843b
向后跳转到 mov %al,-0x1b(%ebp,%ebx,1)
之前的几个字节。我想这就是你在谈论的 getchar 循环?呃,这是未优化的代码,它一直将所有内容都存储在内存中。
sar/shr 部分正在使用带符号整数类型执行n %= 16;
,它必须为负n
产生负余数。这就是为什么你应该使用unsigned
或写n &= 15;
当你不想 想要它,并希望它高效编译。它在屏蔽之前/之后为负 n
添加/减去 15。此外,这表明您的代码可能是由 gcc4.7 或更早版本编译的;较新的 gcc 足够聪明,可以使用 cltd
将 EAX 签名扩展为 EDX:EAX 而不是 mov/sar 31。godbolt.org/z/tRLEkq 显示与 return
n%16` 相同的代码生成。
【参考方案1】:
我正要在Reverse engineering code that does SAR by 31, shr, then add and subtracts that around an AND 上发布有关此代码的 sar/shr/add/and/sub 部分的答案,但在我完成之前大约一分钟就被删除了。这部分代码可能经常弹出,值得回答:
AND 周围的代码正在为有符号的 32 位整数类型(例如 int
)执行 n %= 16;
,实现 C 有符号余数语义。 对于非负整数,它相当于n &= 0xf;
,即只保留低 4 位。
您可以让 GCC4.7 为该函数准确地发出这段代码 (Godbolt compiler explorer):
int mod16(int n)
return n % 16;
# gcc4.7.4 -O1 -m32 -mregparm=1 (first arg in EAX instead of on the stack)
mod16(int):
movl %eax, %edx
sarl $31, %edx
shrl $28, %edx
addl %edx, %eax
andl $15, %eax
subl %edx, %eax
ret # return with n%16 in EAX.
有趣的事实:gcc4.8 和更高版本使用 cdq
而不是 mov/sar $31
将 EAX 签名扩展为 EDX:EAX。即设置 EDX 的所有位 = EAX 的符号位。
代码中的存储/重新加载,并使用 EBP 作为帧指针,表明它是在没有(额外)优化的情况下编译的。与大多数编译器相比,GCC 仍然经常使用它的有效方法在单个表达式中做事。我在 Godbolt 上使用了-O1
,以避免编译器存储/重新加载到内存中。
在 C 中,%
必须为负的 n
产生一个负余数,但在实现有符号整数除法语义的 AND 指令周围模拟它仍然比使用 idiv
的幂更有效2.(对于非幂的 2 个编译时常量 with a multiplicative inverse,同样处理。)
回想一下 (n/16)*16 + n%16 = n
对所有 n
而言,并且 C 符号除法向 0 截断。(这适用于任何除数,而不仅仅是 2 的正幂)。例如-22/16 = -1
和-22 % 16 = -6
。-1 * 16 + -6 = -22
。编译器必须生成适用于所有可能的n
的代码;它(通常)不知道您的 int
在您的代码运行时永远不会是负数。
算术右移转向 -Inf,因此它需要类似的修正来实现 /
除法语义。
这就是为什么当您不需要需要负余数时应该使用unsigned
或n &= 15;
的原因,并且希望它能够高效地编译而不是编译成这个过于复杂的序列。 如果编译器可以证明该值是非负的,那么 and $0xf, %eax
周围的这些多余的东西可以优化掉,但这并不总是会发生(并且永远不会在调试版本中发生)。
这是几个小时前来自someone else's question 的代码的一部分,因此我们可以提供缺失的第一条从-0x10(%ebp)
加载EAX 的指令
804847d: 8b 45 f0 mov -0x10(%ebp),%eax # EAX = n; you left out this insn
8048480: 89 c2 mov %eax,%edx
8048482: c1 fa 1f sar $0x1f,%edx # edx = 0 or -1 (sign bit of n)
8048485: c1 ea 1c shr $0x1c,%edx # edx = 0 or 15
逻辑右移 28 总是移入零,而不是符号位的副本,因此将 -1
(0xffffffff) 变为 15
。 (32-28 = 寄存器底部还剩 4 位)。当然,将 0 保留为 0。
所以 EDX = 0 或 15,对于非负或负 n
。
8048488: 01 d0 add %edx,%eax
804848a: 83 e0 0f and $0xf,%eax
# only look at the lowest four bits
804848d: 29 d0 sub %edx,%eax
它在 AND 周围加/减 15(或 0)。
这是针对负输入执行 -((-eax) & 15)
的优化方法,基于 -x = ~x + 1
与 &
结合的 2 的补码标识。
请注意,x & 0xf
会产生 [0..15] 范围内的数字,因此负 15 将始终为负数或零。如果输入已经是 16 的倍数(即使是负数,因为这是 2 的补码的工作方式),则低位将从 0 开始。所以 (n + 15) & 15 = 15,并且 15-15 = 0,例如 -128 % 16
的正确结果。
对于最初为负的输入,这只是加/减 0,即使值保持不变的加法恒等式。所以整个事情相当于n & 0xf
非负n
。
整个就相当于(eax < 0) ? -((-eax) & 15) : (eax & 15)
GCC 将它用于n % 16
也表明它为每个可能的int
提供了正确的结果。 (对于n % 16
,没有任何具有 C 未定义行为的 n
值。
804848f: 89 45 f0 mov %eax,-0x10(%ebp) # store back into where we loaded from
所以我们知道它是n %= 16;
,因为它将结果存储回它最初加载的堆栈槽的顶部。
8048492: 83 7d f0 03 cmpl $0x3,-0x10(%ebp)
# compare to 3
是的,没错。 FLAGS 是根据n - 3
设置的,因为 AT&T 语法是向后的。但是如果它们相等,则设置 ZF。
【讨论】:
以上是关于x86 反汇编,涉及模块化散列的主要内容,如果未能解决你的问题,请参考以下文章