[转载]混淆代码中的push与pop操作

分析代码混淆时,一般的指令是比较好分析的,但对于利用栈进行混淆处理这类代码是比较头痛的,编写对付这类代码的自动分析模块是比较麻烦的,恐怕得追踪记录栈的使用情况。

例如,请分析下面10条代码,并将它的混淆部分去除,得到最简化代码。这是比较常见的一类混淆形式,实际上它只是一个混淆块的一部分:

... ...
11    push esp
12    push 0x78014532
13    push dword ptr [esp+8]
14    push eax
15    mov eax, 4
16    add eax, dword ptr [esp+0ch]
17    xchg eax, [esp]
18    pop dword ptr [esp+4]
19    pop edx
20    mov esp, [esp]
      ... ...

你能完成吗?能在一分钟之内完成吗?能在30秒内完成吗?一分钟内完成,你不错嘛。30秒内完成,你很优秀了,并且很有经验!

那么,这段代码倒底是干嘛的?实际上,它有用的只有一条指令:

pop edx

如果,你还不能分析出来,大概是没弄清 push 与 pop 指令的操作。那么,先看下面的讲解弄懂后,再来分析。

1. push 与 pop 指令的执行

对于寄存器与立即数操作数理解是没难度的,有困惑的是在于内存操作数上。一般的资料上说:

  • push eax: 先将 esp 减 4,然后往栈里写入 eax。
  • pop eax: 先从栈里读一个值到 eax,然后将 esp 加 4。

它们对 esp 的操作顺序是不同的。这个观点也不能说错, 但也不能说完全正确!一般情况下还可以(寄存器或者立即数操作数)。但是,如果这样理解,将会是很糟糕的!特别是在混淆代码分析中!

因为,对于下面两条指令很容易糊涂:

  • push dword ptr [esp+8]: 这个 esp 值是原值?还是已经减了 4 后的值?
  • pop dword ptr [esp+4]: 这个 esp 值是原值?还是已经加了 4 后的值?

实际上,在 CPU 内部的流水线操作的角度上看,push 与 pop 的顺序是完全一样的!

它们的三个步骤是相同的:

  1. 读源操作数
  2. 修改栈指针(sp/esp/rsp)
  3. 写入目标操作数

以 32 位操作数为例,则有:

push imme32/mem32/reg32

temp  <= source         ; 源操作数读入临时保存起来
    esp   <= esp - 4        ; 将 esp 减去 4
    [esp] <= temp           ; 往 [esp] 里写入 temp 值

pop mem32/reg32

temp  <= [esp]          ; 将 [esp] 的值读入临时保存起来
    esp   <= esp + 4        ; 将 esp 加上 4
    dest  <= temp           ; 向目标操作数写入 temp 值

因此,不管是 push 还是 pop 指令,都需要先将源操作数读入保存到 CPU 内部的临时寄存器里。然后再改变栈指针,最后写入目标操作数。

我们可以发现:

  • 对于 push 指令:目标操作数是 [esp]
  • 对于 pop 指令: 源操作数是 [esp]

那么,对于上面两条指令,我们就很清晰,很正确地理确了

  • push dword ptr [esp+8]: 这个 esp 值是原值,也就是减去 4 之前的值。因为 [esp+8] 是源操作数!
  • pop dword ptr [esp+4]: 这个 esp 值是加上 4 后的值。因为 [esp+4] 是目标操作数!

2. 逐步分析

弄清楚 push 与 pop 指令后, 再进行下面的分析

11    push esp
12    push 0x78014532
13    push dword ptr [esp+8]
14    push eax

假设,压栈前的 esp 值为 00012d3c,那么, 执行完这 4 条 push 指令后栈的内容如下:

00012d3c: | ... ...  |
                  +----------+
        00012d38: | 00012d3c |  <- 压入 esp 值
                  +----------+
        00012d34: | 78014532 |  <- 压入数值  
                  +----------+
        00012d30: | [esp+8]  |  <- 压入 00012d34+8 内存的值, 也就是 00012d3c 内的值
                  +----------+
        00012d2c: | eax      |  <- 压入 eax
                  +----------+
        00012d28: | ... ...  |

接下来的 3 条指令用来增加栈内 esp 映像值

15    mov eax, 4
16    add eax, dword ptr [esp+0ch]
17    xchg eax, [esp]

add eax, dword ptr [esp+0ch] 这条指令执行时, 当前的 esp 值为 00012d2c, [esp+0ch] 的地址就是 00012d38. 接着将 eax 值与 [esp] 交换, 也就是写入 eax 到当前 [esp],并恢复原 eax 值。

00012d3c: | ... ...  |
                  +----------+
        00012d38: | 00012d3c | <- 用这个值加 4 等于 00012d40
                  +----------+
        00012d34: | 78014532 | 
                  +----------+
        00012d30: | [esp+8]  |
                  +----------+
        00012d2c: | eax      | <- 恢复 eax 值,并写入 00012d40
                  +----------+
        00012d28: | ... ...  |

接下来执行两条 pop 指令, 栈内容改变为

18    pop dword ptr [esp+4]
19    pop edx

00012d3c: | ... ...  |
                  +----------+
        00012d38: | 00012d3c |
                  +----------+
        00012d34: | 78014532 | <--+ 将 00012d40 写入此处
                  +----------+    |
        00012d30: | [esp+8]  | <--+--- pop dword ptr [esp+4] 当前 esp 指向此处
                  +----------+    |
        00012d2c: | 00012d40 | ---+ 
                  +----------+
        00012d28: | ... ...  |

pop edx 指令执行后, 栈结构如下:

00012d3c: | ... ...  |
                  +----------+
        00012d38: | 00012d3c |
                  +----------+
        00012d34: | 00012d40 | <--- esp 指向此处
                  +----------+
        00012d30: | [esp+8]  | ---> 执行 pop edx 指令后, edx 寄存器的值就是 00012d3c 内的值
                  +----------+
        00012d2c: | 00012d40 |
                  +----------+
        00012d28: | ... ...  |

最后执行 mov esp, [esp] 指令, 栈结构如下:

00012d44: | ... ...  |
                  +----------+
        00012d40: | ... ...  | <--- esp 指向此处
                  +----------+
        00012d3c: | ... ...  |
                  +----------+
        00012d38: | 00012d3c |
                  +----------+
        00012d34: | 00012d40 | ---> 执行 mov esp, [esp] 指令更新 esp 值
                  +----------+
        00012d30: | [esp+8]  | 
                  +----------+
        00012d2c: | 00012d40 |
                  +----------+
        00012d28: | ... ...  |

3. 总结

在上面的分析中, esp 值由原来的 00012d3c 变为 00012d40, 而 edx 寄存器的值变为 [00012d3c] 内的值。因此, 这个混淆代码块实际上只执行这一条指令: pop edx

如果, 只有少量的这类代码, 那还是比较幸运的。问题是有大量的这类代码,并且不是一成不变的,演化出 N 种变形,那是很头痛的。对于一般代码,很容易通过脚本代码(例如用 python 写一个分析代码)处理,但这类代码要时时刻刻留意 esp 的变化,那是比较难处理的。

例如:

mov eax, 4
    lea edx, [edi+eax*4]        => 转化为:  push dword ptr [edi+10h]
    push dword ptr [edx]

通过将 N 条指令转化为一条指令,或少于 N 条指令。有助于降低混淆代码数量, 有助于分析。实际上,混淆代码虽然千变万化,但目的只有一个: 澎胀代码折磨人!

时间: 2024-10-05 04:29:35

[转载]混淆代码中的push与pop操作的相关文章

011实现一个栈,除了push和pop操作,还要实现min函数以返回栈中的最小值,时间复杂度都为O(1)(keep it up)

实现一个栈,除了push和pop操作,还要实现min函数以返回栈中的最小值. push,pop和min函数的时间复杂度都为O(1). 看到这个题目最直接的反应是用一个变量来保存当前栈的最小值,让我们来看看这样可行否? 如果栈一直push那是没有问题,入栈元素如果比当前最小值还小,那就更新当前最小值. 可是如果pop掉的栈顶元素就是最小值,那么我们如何更新最小值呢?显然不太好办. 既然只用一个变量没法解决这个问题,那我们就增加变量.如果说每个结点除了保存当前的 值, 另外再保存一个从该结点到栈底的

说明如何用一个数组A[1...n]来实现两个栈,使得两个栈中的元素总数不到n时,两者都不会发生上溢。注意PUSH和POP操作的时间应为O(1)。

校招开始了,发现自己数据结构,Algorithms的知识都还给老师了.喵了个呜的! <算法导论>开啃吧~ 从第三章数据结构开始吧~ 10.1-2 : 如何用一个数组A[1...n]来实现两个栈,使得两个栈中的元素总数不到n时,两者都不会发生上溢.注意PUSH和POP操作的时间应为O(1). 解:思想是,建一维数组,两个栈stack1和stack2分别从数组A[0]和A[N-1]开始push,栈指针相遇时,两个栈中元素总数为n. 在思考怎么用java 实现,晚些时候 上代码吧~

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型

import java.util.Stack; /**  * 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型.  * @author user  *  *思路:队列是先入先出,栈是先入后出,可以将数据压入第一个栈后,在弹出来压入第二个栈,弹得时候直接从第二个栈弹出,弹出后又将  *第二个栈中的所有数据弹出重新压入的一个栈  */ public class Solution {     Stack<Integer> stack1 = new Stack<

面试题9-用两个栈来实现一个队列,完成队列的Push和Pop操作

题目 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 思路: 一个栈压入元素,而另一个栈作为缓冲,将栈1的元素出栈后压入栈2中 代码 import java.util.Stack; /** *两个栈实现一个队列 * @author MSI */ public class Requeue{ Stack<Integer> sk1=new Stack<Integer>(); Stack<Integer> sk2=new Stack<

【算法】用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack<Integer> stack2 = new Stack<Integer>(); public void push(int node) { stack1.push(node); } public int pop() { if(stack1.empty()&&stack2.empty()){ th

一张图展示:用两个栈来实现一个队列,完成队列的Push和Pop操作

一  基本思路 将s1作为存储空间,以s2作为临时缓冲区. 入队时,将元素压入s1. 出队时,将s1的元素逐个"倒入"(弹出并压入)s2,将s2的顶元素弹出作为出队元素,之后再将s2剩下的元素逐个"倒回"s1. 二 图示 三 代码实现(Java) import java.util.Stack; public class Solution { Stack<Integer> stack1 = new Stack<Integer>(); Stack

代码中特殊的注释技术——TODO、FIXME和XXX的用处 (转载)

转自:http://blog.csdn.net/reille/article/details/7161942 作者:reille 本博客网址:http://blog.csdn.net/reille/,转载本博客原创文章请注明出处. 本文内容概要: 代码中特殊的注释技术——TODO.FIXME和XXX的用处. 更多请关注:http://velep.com/ ——————————————————————————————————————————————————————————————————— 前言:

代码中特殊的注释技术——TODO、FIXME和XXX的用处 (转载)

转自:http://blog.csdn.net/reille/article/details/7161942 作者:reille 本博客网址:http://blog.csdn.net/reille/,转载本博客原创文章请注明出处. 本文内容概要: 代码中特殊的注释技术——TODO.FIXME和XXX的用处. 更多请关注:http://velep.com/ ——————————————————————————————————————————————————————————————————— 前言:

(转载).NET代码混淆实践

今天突然对反编译.混淆感兴趣,在网上查了一些资料,文章大部分内容来源http://www.cnblogs.com/hsapphire/archive/2010/11/21/1883514.html. 1,名称混淆 1.1控制台程序 class Program { static void Main(string[] args) { string userID="asldjf3333djf"; string pwd = GetPassword(userID); ChangeInfo(use