xuenixiang 发表于 2020-6-15 00:45:59

深入浅出angr(五)


前言  我们时常会遇到各种平台下的程序,有时我们为了搭一个动态调试的环境需要耗费不少的时间,更有甚者无功而返,那么这时不妨用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_29F210和edx,ecx作为参数
  查一下edx对应的交叉引用,可以知道长度为40
  查一下edx对应的交叉引用可以知道key
  具体查看sub_110F0函数,这回比较正常
  查看sub_11010函数
  很明显是一个不太标准的Tea解密函数。
  在查sub_110F0的交叉引用时,发现还有其他几个地方引用了该函数,不过我仔细查看了一下,这三个地方引用sub_110F0函数都是对相同的数据进行解密,只是数据存放的位置不同罢了。所以也就先不去关心了。
  所以把数据dump下来,解密就可以了。
  代码如下:
void decrypt (uint32_t* v, uint32_t* k) {
uint32_t v0=v, v1=v, sum=0xC6EF3720, i;/* set up */
uint32_t delta=0x61C88647;          /* a key schedule constant */
uint32_t k0=k, k1=k, k2=k, k3=k;   /* 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=v0; v=v1;
}

int main()
{
uint32_t k={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,v0,v1,v1,v2,v2,v3,v3,v4,v4);

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方法:
https://xz.aliyun.com/t/79.png  它是将程序的二进制函数,变成像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]
查看完整版本: 深入浅出angr(五)