首页 > 代码库 > [转载]混淆代码中的push与pop操作

[转载]混淆代码中的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 条指令。有助于降低混淆代码数量, 有助于分析。实际上,混淆代码虽然千变万化,但目的只有一个: 澎胀代码折磨人!

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