简单说一下个人的想法
第一题 (http://bbs.pediy.com/showthread.php?t=193755)
基本就是纯粹考察ARM的逆向能力,IDA F5 + 部分ARM汇编,很容易就可以将算法部分分析出来。最后得出的结论就是,其实就是base64加了三条'-'
值得一提的是,通过第一题,对加密部分代码有了比较深入的了解之后,对后面两道题解答有很大帮助
第二题(http://bbs.pediy.com/showthread.php?t=193824)
so加了壳,静态分析会发现dynamic字段是空。
当时偷懒,没细看,直接通过gDvm->loadedClasses找到 "Lcom/crackme/MainActivity"类的ClassObject,然后枚举Method获取了crackme函数的地址。
然后静态分析,发现就是倒序之后base64加密
补充一下"静态分析会发现dynamic字段是空"的问题,其实就是利用了文件和内存中使用的偏移不同的方法,如下图,文件中会读0x3C000,而内存中会读0x3D000,实际上(此SO)内存中布局是完全按照文件中的布局映射的,只要读取0x3D000处,就可以得到正常的dynamic了
第三题(http://bbs.pediy.com/showthread.php?t=193877)
貌似是添加了AntiDebug,由于是周末,家里啥环境都没有,也就没有细搞。按照第二题的思路,写了一个程序,进程注入,把crackme的函数地址读出来了,然后就没有然后了~~
希望大神来给分析一下AntiDebug怎么实现的,又是怎么绕过的。
好吧,我承认我懒得分析了,目测单步测试so加载部分代码应该能有所收获。
结论就是前面 n-2个字符两两交换,然后算base64
第四题(http://bbs.pediy.com/showthread.php?t=193953)
真正考逆向水平的题目。方法就是单步~读ARM指令~对照IDA F5~单步~读ARM指令~对照IDA F5~~~~多尝试几遍就行了。还好so里面保留了部分des的符号,让那些不熟悉des加密的娃(例如me)可以猜到是用了des加密。
不知道是不是我找的源码的问题。测试的时候,发现自己程序的加密结果和crackme的不同,经过反复反复反复反复地测试,发现是S_BOX和另外一个初始化的数组值有差异,测试的方法就是逐过程对照结果,看看哪一步得到的结果不一样,然后对照参数。最后找到不同点。
然后就很简单了。必须吐槽一下,这道题测试好麻烦,手工输入32个字符还算好的。
哦~忘了一点,秘钥生成,是使用了用户名的前8个字节,然后依次异或了0x100个字符 =.= ,话说,异或不是有交换结合律么,结果就是直接异或0x93就行了。
以上就是个人的一些方法,欢迎来讨论或者提供更好的解题思路啊
================== YD的分割线===============
我x~~竟然总顶了,受宠若惊啊,所以决定还是把整个解题流程写一下,方便各位查阅
持续更新中~~~~~~
===================低调的分割线=================
前言
这次比赛,前三道题目的算法都是base64,最后一题是des。
由于dex部分十分简单,就是调用libcrackme.so中的crackme函数,所以这里就不赘述了。
Crackme1
目的是分析libcrackme.so,不过这个so如果用IDA打开的话,会卡一段时间,然后报错,同时生成6G+的数据文件。这种情况明显是文件格式有猫腻 ,处理这种问题,推荐010Editor的模板。
用010Editor打开libcrackme.so,然后执行ELF模板,会提示解析出错。然后就能找到出错的内容了
可以看到,是一个程序节内容错误,本着简(懒)单(惰)的原则,直接将此节内容清零,保存文件后,IDA就可以正常打开文件了。此问题在后续的so里面都会出现,就不再赘述了。
之后就是找到Java_com_crackme_MainActivity_crackme函数,F5分析代码。
然后发现。。。。。。原来是加密的。
处理加密,考虑到运行时必定已经解密,所以运行crackme1,然后再吧so文件dump出来,过程不多说了,这里提一下用IDA下面python插件dump内存的方法:
import idaapi
base = 0x74E71000
size = 266252
data = idaapi.dbg_read_memory(base,size)
if data != None:
f = open("e:\\crackme1.so","wb")
f.write(data)
f.close()
base根据实际情况修改。size可以看本地的so文件大小获取。当然也可以用idaapi的dbg_get_memory_info,不过这个函数用起来有点蛋疼,而且麻烦 。
不管怎么样,把内存中的so dump出来之后,就能看到正常的代码了
int __fastcall Java_com_crackme_MainActivity_crackme(int a1, int a2, int a3, int a4)
{
v4 = a4;
v5 = a1;
v6 = (*(int (**)(void))(*(_DWORD *)a1 + 676))();
v7 = (*(int (__fastcall **)(int, int, _DWORD))(*(_DWORD *)v5 + 676))(v5, v4, 0);
sub_536C(&dword_15220[1], v6, v7);
sub_597C(&dword_15220[1]);
return (*(int (__fastcall **)(int, int *))(*(_DWORD *)v5 + 668))(v5, &dword_15220[1]);
}
之前的v6和v7以及最后return时调用的函数,猜也能知道,是用来读写Java里面的String类的。所以关键内容是sub_536C和sub_597C
PS:如果看不懂的可以尝试一边单步调试,一遍看
int __fastcall sub_536C(int a1, const char *a2, const char *a3)
{
v3 = a3;
v4 = a1;
v5 = a2;
result = sub_5328();
if ( v3 )
{
if ( v5 )
{
v7 = strlen(v5);
v13 = v7;
v8 = v7;
v9 = strlen(v3);
v10 = v8 + 1;
v14 = v9;
n = v9 + 1;
*(_DWORD *)(v4 + 52) = operator new[](v10);
result = operator new[](n);
v11 = *(void **)(v4 + 52);
*(_DWORD *)(v4 + 56) = result;
if ( v11 )
{
if ( result )
{
memset(v11, 0, v10);
memset(*(void **)(v4 + 56), 0, n);
memcpy(*(void **)(v4 + 52), v5, v13);
result = (int)memcpy(*(void **)(v4 + 56), v3, v14);
}
}
}
}
return result;
}
sub_536C的内容很简单,就是为用户名和密码各申请一块内存。然后把指针放在一个申请的结构体里面
signed int __fastcall sub_597C(int a1)
{
v1 = a1;
sub_53E4();
v2 = *(_DWORD *)(v1 + 56);
if ( *(_BYTE *)(v2 + 3) != 45 || *(_BYTE *)(v2 + 7) != 45 || *(_BYTE *)(v2 + 11) != 45 )
{
*(_BYTE *)_cxa_allocate_exception(1) = 1;
_cxa_throw();
}
sub_5430(v1);
v3 = *(const char **)(v1 + 56);
v4 = (unsigned __int8)byte_15120;
if ( !byte_15120 )
{
do
byte_15121[v4++] = -128;
while ( v4 != 256 );
v5 = 0;
do
{
byte_15121[v5 + 65] = v5;
++v5;
}
while ( v5 != 26 );
v6 = &byte_15182;
do
{
*v6 = v5;
v5 = (v5 + 1) & 0xFF;
++v6;
}
while ( v5 != 52 );
v7 = &byte_15151;
do
{
*v7 = v5;
v5 = (v5 + 1) & 0xFF;
++v7;
}
while ( v5 != 62 );
byte_1514C = 62;
byte_15150 = 63;
byte_1515E = 0;
byte_15120 = 1;
}
if ( v3 )
{
v8 = strlen(v3);
v9 = (const void *)operator new[](v8 + 1);
}
else
{
v9 = 0;
}
v10 = strlen(v3);
v11 = 0;
v12 = v9;
v13 = 0;
v14 = v3;
while ( v11 < (signed int)(v10 - 3) )
{
v15 = 0;
do
{
v16 = byte_15121[*(&v14[v11] + v15)];
*(&v21 + v15) = v16;
if ( v16 & 0x80 )
*(&v21 + v15) = 0;
++v15;
}
while ( v15 != 4 );
v13 += 3;
v11 += 4;
v17 = v22;
*(_BYTE *)v12 = ((signed int)v22 >> 4) | 4 * v21;
v18 = v23;
*((_BYTE *)v12 + 1) = 16 * v17 | ((signed int)v23 >> 2);
*((_BYTE *)v12 + 2) = (v18 << 6) | v24;
v12 = (char *)v12 + 3;
}
v19 = (void *)operator new[](v13);
memmove(v19, v9, v13);
if ( v9 )
operator delete[]((void *)v9);
memcpy((void *)(v1 + 60), v19, v13);
if ( v19 )
operator delete[](v19);
sub_548C(v1);
return 1;
}
sub_597C的内容有点多,分段来分析
首先调用sub_53E4,sub_53E4内容如下:
signed int __fastcall sub_53E4(int a1)
{
if ( !*(_DWORD *)(a1 + 52)
|| (v1 = *(const char **)(a1 + 56)) == 0
|| (v2 = strlen(*(const char **)(a1 + 52)) - 6, v3 = strlen(v1), v2 > 0xE)
|| v3 - 12 > 0x12 )
{
*(_BYTE *)_cxa_allocate_exception(1) = 1;
_cxa_throw();
}
return 1;
}
可以看到,主要是用来判断输入的用户名和密码长度的。用户名长度为6-20,密码长度为12-30
v2 = *(_DWORD *)(v1 + 56);
if ( *(_BYTE *)(v2 + 3) != 45 || *(_BYTE *)(v2 + 7) != 45 || *(_BYTE *)(v2 + 11) != 45 )
{
*(_BYTE *)_cxa_allocate_exception(1) = 1;
_cxa_throw();
}
这一段是用来判断字符串的第4,8,12个字符是不是'-',不是就返回失败
sub_5430(v1);
然后是sub_5430,这个函数不能被F5,所以看不到C代码,只能看ARM,因为不是太重要,就不贴ARM代码了。这个函数的功能就是把密码里面的‘-’符号去掉,剩下的组成一段字符串。
v4 = (unsigned __int8)byte_15120;
if ( !byte_15120 )
{
do
byte_15121[v4++] = -128;
while ( v4 != 256 );
v5 = 0;
do
{
byte_15121[v5 + 65] = v5;
++v5;
}
while ( v5 != 26 );
v6 = &byte_15182;
do
{
*v6 = v5;
v5 = (v5 + 1) & 0xFF;
++v6;
}
while ( v5 != 52 );
v7 = &byte_15151;
do
{
*v7 = v5;
v5 = (v5 + 1) & 0xFF;
++v7;
}
while ( v5 != 62 );
byte_1514C = 62;
byte_15150 = 63;
byte_1515E = 0;
byte_15120 = 1;
}
之后这一段,可以发现,是跟我们输入的用户名密码没有任何关系的。其实是生成一个table,用这个table来变换字符串。所以逆向的时候,这部分代码可以直接照抄。
中间的一些条件判断就不提了
while ( v11 < (signed int)(v10 - 3) )
{
v15 = 0;
do
{
v16 = byte_15121[*(&v14[v11] + v15)];
*(&v21 + v15) = v16;
if ( v16 & 0x80 )
*(&v21 + v15) = 0;
++v15;
}
while ( v15 != 4 );
v13 += 3;
v11 += 4;
v17 = v22;
*(_BYTE *)v12 = ((signed int)v22 >> 4) | 4 * v21;
v18 = v23;
*((_BYTE *)v12 + 1) = 16 * v17 | ((signed int)v23 >> 2);
*((_BYTE *)v12 + 2) = (v18 << 6) | v24;
v12 = (char *)v12 + 3;
}
这一部分是关键的代码。把输入的密码通过一定的变换规则,变换为另一段字符串。
之后的代码就是把变换得到的字符串和输入的用户名做比较,相同则成功,不同则失败。
到这里,代码分析完了,剩下的就是怎么写注册机。
都知道,注册机要把这段过程逆过来,我个人建议,在逆这段代码的时候,最好先用C代码把这段代码实现一遍,然后根据自己实现的代码进行逆转。
剩下的就是考验代码分析能力和编码能力了。这点教不来,只能靠大家锻炼
附上我提交的代码
keygen1.cpp.txt+++++++++++++++++下面这货是crackme2+++++++++++++++++++++++
Crackme2:
上来二话不说,先把运行时的libcrackme.so文件dump出来。
但是IDA Attach上去之后,竟然发现Modules里面没有libcrackme.so,对于这种情况,可以通过python脚本调用idaapi.dbg_get_memory_info()来获取内存布局信息,当然根本方法就是读取/proc/<pid>/maps文件,通过读取该文件,可以获取libcrackme.so的基址,然后就能很容易地把文件dump出来了。
Dump出来的libcrackme.so的ELF文件头全是'\0',面对这种情况,我的方法是用原始的libcrackme.so的ELF头填充到dump出来的so中。
之后用IDA打开修复后的libcrackme.so文件,会发现导入函数和导出函数都是空的。
面对这种情况呢,实际上有两种解决方法。
第一种呢,就是本文最开始提到的,造成这种情况的原因,是因为dynamic节做了手脚,只要恢复了,就能看到导出函数了。
第二种是我做题的时候采取的笨方法,在libdvm.so中,导出了gDvm这个全局变量,gDvm里面有一项叫做loadedClasses,以哈希表的形式记录了已经加载的Class,通过遍历这个Class,可以找到com.crackme.MainActivity类的ClassObject结构体,在ClassObject里面又有一项叫directMethod,以数组的形式记录了该类的方法对象,遍历Method,可以找到crackme函数的Method对象,由于crackme是一个JNI调用。在Android4.4.4下(其他版本过程可能不太一样),第一次调用crackme函数,会掉用Method->nativeFunc,这个函数实际上是指向dvmResolveNativeMethod,在该函数中进行处理,处理完毕后,将crackme的实际地址,写入Method->jniArgInfo。也就是说,只要执行一次crackme,就能从Method->jniArgInfo获取crackme函数在libcrackme.so里面的偏移了。(再次重申,不同环境下可能不一样)
写了一个脚本遍历class:
import idaapi
from binascii import *
gDvm = 0x415A81F0
loadedClassesOffset = 0xAC
def get_uint32_of_pointer(addr):
x = idaapi.dbg_read_memory(addr, 4)
y = get_addr(x)
return int(y,16)
def get_addr(data):
s = b2a_hex(data)
addr = "0x"
try:
for i in range(3,-1,-1):
addr += s[2*i]
addr += s[2*i+1]
except:
print s
return addr
tmp = gDvm + loadedClassesOffset
hashtable = get_uint32_of_pointer(tmp)
size = get_uint32_of_pointer(hashtable)
addr = get_uint32_of_pointer(hashtable + 0xC)
xx(addr, size)
def xx(addr, size):
table = idaapi.dbg_read_memory(addr, size*8)
for i in range(0x2000):
tmp = table[i*8 + 4:i*8+8]
addr = get_addr(tmp)
if addr != "00000000":
iaddr = int(addr,16)
clazz = idaapi.dbg_read_memory(iaddr, 28)
if clazz != None:
name = get_addr(clazz[4*6:4*6+4])
iname = int(name,16)
sname = idaapi.dbg_read_memory(iname, 50)
if sname != None:
print addr + ' : ' + sname
里面用了一些硬编码。仅供参考,切勿直接copy使用
由于com.crackme.MainActivity类只有7(印象中)个直接方法,所以剩下的手动找就行了。
总之,获取到了crackme函数的地址,剩下的就是F5了
可以发现,算法的代码和crackme1出奇得一致。直到看到用户名比较那一段
记得之前可以F5的,但是这次不行了,所以把关键点标注了一下,凑活看吧,看不懂的单步走走。在sub_5718里面
不解释了,总之就是逆序比较,类似于
for(int i =0; i < n; i++){
if(s1[i] != s2[n - i -1]){
return false;
}
}
由于考虑到base64加解密顺序是不变的,所以只有在加解密前逆序一次就行了。
crackme2提交的代码,其实就是crackme1添加了点内容
keygen2.cpp.txt
*******************我才不是crackme3呢****************************
Crackme3
相比较于Crackme2,添加了反调试的内容。
依旧是两种解决方案。
第一种就是crackme2中提到的方案二。写个注入程序,把crackme的地址找出来,并把内存中的So dump一份。不多说了,附上我写的代码,代码注入部分源码来源于网上。
inject.zip
另外一种就是靠实力进行分析了。首先要把so修复一下,然后分析找到DT_INIT
然后需要以DEBUG模式启动App进行分析。(不会的请翻阅
http://bbs.pediy.com/showthread.php?t=178659 )
Debug模式启动后,在DT_INIT的偏移处下断点。然后就是单步测试。
最后可以找到
signed int __fastcall start()
{
signed int v0; // r3@2
pthread_t newthread; // [sp+4h] [bp-58h]@7
int v3; // [sp+8h] [bp-54h]@5
int v4; // [sp+Ch] [bp-50h]@9
int v5; // [sp+10h] [bp-4Ch]@11
int v6; // [sp+14h] [bp-48h]@3
int v7; // [sp+4Ch] [bp-10h]@1
v7 = 0;
if ( pthread_mutex_init((pthread_mutex_t *)&mutex, 0) != 0 )
{
v0 = -1;
}
else
{
v7 = pthread_create((pthread_t *)&v6, 0, (void *(*)(void *))debuggdb_scan, 0);
if ( v7 )
{
v0 = -1;
}
else
{
v7 = pthread_create((pthread_t *)&v3, 0, (void *(*)(void *))file_pro_thread_strengthen, 0);
if ( v7 )
{
v0 = -1;
}
else
{
v7 = pthread_create(&newthread, 0, (void *(*)(void *))prevent_attach_one, 0);
if ( v7 )
{
v0 = -1;
}
else
{
v7 = pthread_create((pthread_t *)&v4, 0, (void *(*)(void *))prevent_attach_two, 0);
if ( v7 )
{
v0 = -1;
}
else
{
v7 = pthread_create((pthread_t *)&v5, 0, (void *(*)(void *))prevent_attach_three, 0);
if ( v7 )
{
v0 = -1;
}
else
{
pthread_mutex_destroy((pthread_mutex_t *)&mutex);
v0 = 0;
}
}
}
}
}
}
return v0;
}
以及不停在pthread_kill的循环
while ( 1 )
{
for ( i = 0; i < *(_DWORD *)(v3 + 12); ++i )
{
v2 = pthread_kill(*(_DWORD *)(v3 + 4 * (i + 8)), 0);
if ( v2 == 3 )
exit(0);
if ( v2 == 22 )
exit(0);
}
sleep(1u);
}
然后就没准备继续分析下去了,有兴趣的可以自己看看这些函数。
总之就是想办法把解密后的So Dump出来,然后IDA就可以静态分析了。
过程依旧和前两个一样,只不过在最后比较的时候变成了每两个字符交换次序,然后比较
代码在sub_57D4里面
很明显在交换字符次序
其实就是
for(i = 0; i < len - 2; i += 2)
{
n[i] = name[i+1];
n[i+1] = name[i];
}
至此crackme3也算结束了。AntiDebug部分暂时不深究了。
keygen3.cpp.txt##################让我缓一缓吧,crackme4没什么新内容,就看逆向能力,有空再跟新##############################
附件汇总:
keygen1.cpp.txt
keygen2.cpp.txt
keygen3.cpp.txt
inject.zip
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
上传的附件: