前言

我们时常会遇到各种平台下的程序,有时我们为了搭一个动态调试的环境需要耗费不少的时间,更有甚者无功而返,那么这时不妨用angr试试,毕竟多一分尝试,多一分希望。

如何跨平台使用angr解题

通常来说,我们会在linux下安装angr框架,那么如果遇到不是linux的程序该怎么办呢?程序无法运行还能使用angr解决么!答案当然是可以。因为angr是符号执行框架,不是真正的在执行程序。我们只需要进行正确的条件设置就可以了。

ARM程序

这里以 android_arm_license_validation为例

最关键的是设置合理的初始状态,从IDA的分析来看,最后的比较部分在sub_1760函数,因此我们以此作为初始状态,同时看到该函数有一个v6参数,经过分析,可以知道该参数经过加密变换,最终用来校验的字符串,也就是说最后的校验过程我们可以不用了解,只需要使用angr将其爆破出来即可。

我们需要设置一个空的状态
state = b.factory.blank_state(addr=0x401760)

而后设置需要传入的参数,选取任意一个地址,用来存放变量。

>>> concrete_addr = 0xfff00000
>>> code = claripy.BVS('code', 10*8)
>>> state.memory.store(concrete_addr, code, endness='Iend_BE')

注意ARM程序是大端对齐。

为了正确的传入参数,我们需要了解arm程序传参方式。

参考文章

因此我们设置r0为输入的地址

state.regs.r0 = concrete_addr

后面就是类似的。

当然如果,加密过程也同样非常复杂,那么我们就需要从加密函数开始,设置好参数,利用angr符号执行。

windows dll

这里以mma_howtouse为例

程序是windows的dll,如果采用正常的方式解题,那么搭建动态调试的环境势必要花费一番功夫。

IDA载入,首先我们应该注意的是dll程序的基址是0x10000000,而不是0x400000

通过导出表,我们可以定位到fnhowtouse函数,进行了handler的绑定,然后通过(*(&v2 + a1))();进行函数调用。

为了调用fnhowtouse函数,可以使用callable方法

首先加载howtouse.dll

p = angr.Project('howtouse.dll', load_options={'main_opts': {'base_addr': 0x10000000}})
howtouse = p.factory.callable(0x10001130)

通过callable方法将fnhowtouse转化为python可以调用的函数

然后我们便可以通过howtouse(i)进行函数调用,而无需依赖环境。通过题目,我们可以知道howtouse(i)结果就是flag,因此我们可以通过

claripy.backends.concrete.convert(howtouse(i))

将结果转换为BVV类型,并通过.value将值取出。
因此代码可以如下进行组织:

getch = lambda i: chr(claripy.backends.concrete.convert(howtouse(i)).value)

    # Let's call this 45 times, and that's the result!
    return ''.join(getch(i) for i in range(45))

windows驱动

这里以flareon2015_10为例

程序是windows驱动,2.5M还是蛮大的,IDA载入分析。

What!这次又是个什么鬼图形,感觉出题人都要玩出花了。当然啦,angr最喜欢解决这种线性的题目了。

首先程序的入口。

可以知道sub_29cd20是有用的函数

好吧。

挨个查看一下,发现一个可疑的case

话说这程序导致我的IDA都有点不正常了。

接下来查看sub_2D2E0函数

真漂亮,还好不是在比赛的时候遇到这种题目。。

我仔细盘了一下代码。还真有趣。

看到sub_13590函数

一百多行代码,F5之后就两行。

sub_2D2E0函数的前半部分都在为0x29F210地址的数据赋值

只有在最后几行,调用了sub_110F0函数,并传入了byte_29F210edxecx作为参数

查一下edx对应[ebp+var_3C]的交叉引用,可以知道长度为40

查一下edx对应[ebp+var_30]的交叉引用可以知道key

具体查看sub_110F0函数,这回比较正常

查看sub_11010函数

很明显是一个不太标准的Tea解密函数。

在查sub_110F0的交叉引用时,发现还有其他几个地方引用了该函数,不过我仔细查看了一下,这三个地方引用sub_110F0函数都是对相同的数据进行解密,只是数据存放的位置不同罢了。所以也就先不去关心了。

所以把数据dump下来,解密就可以了。

代码如下:

void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up */
    uint32_t delta=0x61C88647;          /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum += delta;
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

int main()
{
    uint32_t k[4]={858927408,926299444,1111570744,1178944579};
    uint32_t v0[] = {0xfadc7f56,0xc49927aa};
    uint32_t v1[] = {0x92cf7c6c,0x1a476161};
    uint32_t v2[] = {0xfd63b919,0x20b6f20c};
    uint32_t v3[] = {0xfd5c2dc0,0x965471d9};
    uint32_t v4[] = {0xfff7434f,0x315d4cbb};
    // v为要加密的数据是两个32位无符号整数
    // k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位
    setbuf(stdin,0);
    setbuf(stdout,0);
    setbuf(stderr,0);
    decrypt(v0,k);
    decrypt(v1,k);
    decrypt(v2,k);
    decrypt(v3,k);
    decrypt(v4,k);
    char *flag;
    sprintf(flag,"%x%x%x%x%x%x%x%x%x%x",v0[0],v0[1],v1[0],v1[1],v2[0],v2[1],v3[0],v3[1],v4[0],v4[1]);

    printf("%s",flag);

    return 0;
}

最后的结果需要转换一下。

再让我们看看如何使用angr的hook来完成解密工作。

大致思路,就是在程序调用解密函数前,将对应的数据存放到相应的位置即可。

因此我们的Hook函数可以这么写

def before_tea_decrypt(state):
    # Here we want to set the value of the byte array starting from 0x29f210
    # I got those bytes by using cross-reference in IDA
    all_bytes = bytes([0x56, 0x7f, 0xdc, 0xfa, 0xaa, 0x27, 0x99, 0xc4, 0x6c, 0x7c,
             0xfc, 0x92, 0x61, 0x61, 0x47, 0x1a, 0x19, 0xb9, 0x63, 0xfd,
             0xc, 0xf2, 0xb6, 0x20, 0xc0, 0x2d, 0x5c, 0xfd, 0xd9, 0x71,
             0x54, 0x96, 0x4f, 0x43, 0xf7, 0xff, 0xbb, 0x4c, 0x5d, 0x31])
    state.memory.store(ARRAY_ADDRESS, all_bytes)

这时hook的长度应该为0
p.hook(0xadc31, before_tea_decrypt, length=0),有点类似于插桩

我们不会运行该程序,而是选取有用的部分运行。
我们只需要运行2D2E0函数即可。

这时需要用到callable方法:

它是将程序的二进制函数,变成像python本地的函数一样调用。

如下初始化

proc_big_68 = p.factory.callable(0x2D2E0, cc=p.factory.cc(func_ty=prototype), toc=None, concrete_only=True)

其中prototype是函数原型的申明

可以像这样声明:

prototype = SimTypeFunction((SimTypeInt(False),), SimTypeInt(False))

因此完整的callable调用如下

# Declare the prototype of the target function
    prototype = SimTypeFunction((SimTypeInt(False),), SimTypeInt(False))
    # Initialize the function instance
    proc_big_68 = p.factory.callable(BIG_PROC, cc=p.factory.cc(func_ty=prototype), toc=None, concrete_only=True)
    # Call the function and get the final state
    proc_big_68.perform_call(0)
    state = proc_big_68.result_state

最后我们可以通过state.solver.eval(state.memory.load(ARRAY_ADDRESS, 40), cast_to=bytes)获取flag

如果学会了,是不是非常简单呢

总结

对于跨平台的程序,最重要的了解该平台调用约定,需要对程序的运行有所了解。

点击收藏 | 1 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖