前言 我们时常会遇到各种平台下的程序,有时我们为了搭一个动态调试的环境需要耗费不少的时间,更有甚者无功而返,那么这时不妨用angr试试,毕竟多一分尝试,多一分希望。
如何跨平台使用angr解题 通常来说,我们会在linux下安装angr框架,那么如果遇到不是linux的程序该怎么办呢?程序无法运行还能使用angr解决么!答案当然是可以。因为angr是符号执行框架,不是真正的在执行程序。我们只需要进行正确的条件设置就可以了。
ARM程序 这里以 android_arm_license_validation为例
深入浅出angr(五)
最关键的是设置合理的初始状态,从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')
深入浅出angr(五)
注意ARM程序是大端对齐。
为了正确的传入参数,我们需要了解arm程序传参方式。
参考文章
因此我们设置r0为输入的地址
state.regs.r0 = concrete_addr
后面就是类似的。
深入浅出angr(五)
当然如果,加密过程也同样非常复杂,那么我们就需要从加密函数开始,设置好参数,利用angr符号执行。
windows dll 这里以mma_howtouse为例
程序是windows的dll,如果采用正常的方式解题,那么搭建动态调试的环境势必要花费一番功夫。
IDA载入,首先我们应该注意的是dll程序的基址是0x10000000,而不是0x400000。
深入浅出angr(五)
通过导出表,我们可以定位到fnhowtouse函数,进行了handler的绑定,然后通过(*(&v2 + a1))();进行函数调用。
深入浅出angr(五)
为了调用fnhowtouse函数,可以使用callable方法
深入浅出angr(五)
首先加载howtouse.dll
p = angr.Project('howtouse.dll', load_options={'main_opts': {'base_addr': 0x10000000}})
howtouse = p.factory.callable(0x10001130)
通过callable方法将fnhowtouse转化为python可以调用的函数
深入浅出angr(五)
然后我们便可以通过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载入分析。
深入浅出angr(五)
What!这次又是个什么鬼图形,感觉出题人都要玩出花了。当然啦,angr最喜欢解决这种线性的题目了。
首先程序的入口。
深入浅出angr(五)
可以知道sub_29cd20是有用的函数
深入浅出angr(五)
好吧。
深入浅出angr(五)
挨个查看一下,发现一个可疑的case
深入浅出angr(五)
话说这程序导致我的IDA都有点不正常了。
深入浅出angr(五)
接下来查看sub_2D2E0函数
深入浅出angr(五)
真漂亮,还好不是在比赛的时候遇到这种题目。。
我仔细盘了一下代码。还真有趣。
看到sub_13590函数
深入浅出angr(五)
一百多行代码,F5之后就两行。
深入浅出angr(五)
sub_2D2E0函数的前半部分都在为0x29F210地址的数据赋值
深入浅出angr(五)
只有在最后几行,调用了sub_110F0函数,并传入了byte_29F210和edx,ecx作为参数
深入浅出angr(五)
查一下edx对应[ebp+var_3C]的交叉引用,可以知道长度为40
深入浅出angr(五)
查一下edx对应[ebp+var_30]的交叉引用可以知道key
深入浅出angr(五)
具体查看sub_110F0函数,这回比较正常
深入浅出angr(五)
查看sub_11010函数
深入浅出angr(五)
很明显是一个不太标准的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
如果学会了,是不是非常简单呢
总结 对于跨平台的程序,最重要的了解该平台调用约定,需要对程序的运行有所了解。
|