CTF中逆向的一些经验

2019-01-17 17,329

0x00 判断文件类型

我们拿到的题目一般是一个二进制可执行文件,而其中的大部分不是Windows平台下的PE文件就是Linux平台下的ELF文件。不管题目中所给文件带有什么扩展名,我们都应该亲自去检查文件头,使用任意文本编辑工具打开这个二进制文件,如果在开头看到MZ字样,那基本上可以确定这是一个Win系统下的可执行文件,相应的如果看到ELF字样即可基本断定这是一个运行于Linux系统下的可执行文件,扩展名为exe而实质上是一个ELF文件,这种情况在以往的CTF比赛中也是出现过的。


0x01 查壳与脱壳

由于近年来CTF比赛中带壳的逆向题出现频率稍有增加,在静态分析之前,进行查壳是非常有必要的,常用的工具有PEID等,进行查壳后,如果是简单且常见的壳如UPX等,那么直接可以使用工具完成脱壳,如果一时半会找不到可用的工具,也可以尝试手工脱壳,例如ASPack就能够采用ESP定律来脱除。如果是侧重考察脱壳的题,一般程序本身就比较简单了,可以这样说,这类题如果能成功脱壳,基本上也算是做出来了。当然,脱壳并不能解决所有问题,之前有CTF比赛中甚至出现了VMP的题目,对于这种情况,还是建议使用调试器去动态调试,毕竟在逆向工程中太依赖IDA的F5功能也是不行的,还是应该锻炼自己的汇编指令阅读能力。如果是VMP比较新的版本,可能还需要使用Sharp OD等插件来隐藏调试行为。


0x02 静态分析

一般是直接将二进制文件拖入IDA进行分析,在这个过程中,可以关注一下是否有符号库路径的提示出现,如果有这种情况,说明程序在编译阶段并没有清除相应的符号信息,我们或许能够从符号库路径的命名中得知一些关于本题的重要信息,大多数时候,静态分析完成后,我们会直接来到程序start处,直接F5,向下找即可来到main函数,直接查看反编译出来的C伪码,然而有时候IDA会出现识别不了函数的情况,比如说分析到某一段代码,视图就从C伪码变为汇编指令视图了,这时候可以尝试手工地在这段汇编指令中识别出函数,并且使用IDA的创建函数功能,这样一来就能够继续查看C伪码,提高我们的做题效率

 

如何从一段汇编代码中把函数找出来?

首先得找到函数头部,很多函数的头部看起来会如下所示:

push    ebp

mov     ebp, esp

先将调用者的堆栈的基址(ebp)入栈,以保存之前任务的信息。

然后将调用者的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者的栈底)。

然后我们继续寻找函数尾部

pop ebp

ret

需要注意的是,如果函数不只有一条路径返回值,我们还得确保函数尾部下方的跳转全都朝下,上方的跳转全部朝上

 

要是在IDA中遇到Sp analysis failed怎么办?

这种情况,有时候是由于某些指令的干扰导致IDA分析出来的栈平衡被破坏所致,会导致IDA生成C伪码失败

image.png

我们需要手动地调整栈平衡,在IDA中依次点击Options-General,勾选Stack pointer

函数刚开始的指令看起来如下图所示:

image.png

函数结束前的指令如下图:

image.png

可以看到每一条指令的前面增加了一个数字,这个数字即指示栈顶指针(SP),在调用带参数的Call后,push和pop或者进行抬栈、清栈后栈顶指针才会改变,上图的mov esp,ebp后SP值显然不合理,应该使用Alt+K快捷键进行调整,调整后如下图所示:

image.png

这时再使用F5,IDA已经能够生产正确的C伪码了


0x03一些常见题型

1. 方程组(HCTF2017逆向第一题)

对输入做了异或

image.png


本题中三个方程的关键代码

image.png

image.png

爆破解法:

def solve(idx1,idx2,idx3,idx4):
   for x in range(0,255):
       if(((x ^ idx1 )& idx2 )>> idx3) | (((x ^ idx1) << idx3 )& idx2) == idx4:
           print(chr(x^0x76), end='')
key1 = [0x6f,0xdd,0xdd,0x48,0x64,0x63,0xd7]
for ele in key1:
   solve(0xAD,0xAA,0x1,ele)
key2 = [0x2e,0x2c,0xfe,0x6a,0x6d,0x2a,0xf2]
for ele in key2:
   solve(0xbe,0xcc,0x2,ele)
key3=[0x6f,0x9a,0x4d,0x8b,0x4b,0xfa,0x1a]
for ele in key3:
solve(0xef,0xf0,0x4,ele)

Z3约束器解法:

from z3 import *
def z3_solve(idx1,idx2,idx3,idx4):
   solver = Solver()
   x = BitVec('x', 32)
   solver.add((((x ^ idx1 )& idx2 )>> idx3) | (((x ^ idx1) << idx3 )& idx2) == idx4)
   if solver.check() == sat:
       print(chr(solver.model()[x].as_long() ^ 0x76), end='')
key1 = [0x6f,0xdd,0xdd,0x48,0x64,0x63,0xd7]
for ele in key1:
   z3_solve(0xAD,0xAA,0x1,ele)
key2 = [0x2e,0x2c,0xfe,0x6a,0x6d,0x2a,0xf2]
for ele in key2:
   z3_solve(0xbe,0xcc,0x2,ele)
key3=[0x6f,0x9a,0x4d,0x8b,0x4b,0xfa,0x1a]
for ele in key3:
z3_solve(0xef,0xf0,0x4,ele)

Z3是一款由微软开发的约束求解器,其代码目前托管在https://github.com/Z3Prover/z3

如果题目的方程比较简单,使用爆破当然简单粗暴,方便快捷;但当题目涉及到多元方程或者方程组时,使用Z3求解是比较高效率的,强烈推荐。

本文作者:China H.L.B

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/95436.html

Tags:
评论  (4)
快来写下你的想法吧!

China H.L.B

文章数:2 积分: 17

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号