好友
阅读权限100
听众
最后登录1970-1-1
|
原题:
已经发了快一个月时间了,但并没有收到答案,看来大神并不屑于去做下啊
0x0 源码
本CM制作出后就是开源出来的,哈哈,关注我的github的人肯定就注意到了,https://github.com/Qrilee/crackme,期间我还更新了几次,当然这就跟题目无关了,纯属技术积累吧
0x1 正篇
本CM玩的点比较杂,反调试,dalvik代码自篡改(这就是我为什么让大家在5.0机器下玩喽),算法(AES,RC4),算法其实非常的简单,使用的都是标准的openssl算法库,可能难就难在我把算法从api中提取了出来,在编译优化完符号表之后大家看到的都是subxxxx的方法名,会对大家解题造成一定的困惑
本题没有使用静态注册jni函数的形式,所以需要先去分析JNI_OnLoad,so没有经任何的处理,所以可以很容易使用IDA打开并f5
[C++] 纯文本查看 复制代码 __int16 *__fastcall JNI_OnLoad(int a1)
{
int v1; // r4@1
__time_t v2; // r3@1
int v3; // r0@6
int v4; // r0@6
int v5; // r4@6
int v6; // r1@6
__int16 *result; // r0@8
int v8; // [sp+4h] [bp-1Ch]@1
struct timeval tv; // [sp+8h] [bp-18h]@1
v1 = a1;
v8 = 0;
gettimeofday(&tv, 0); //获取当前时间
v2 = tv.tv_sec % 2 + 10; //利用当前时间去计算出一个数
if ( v2 == 10 )
{
dword_214AC = 81;
dword_214B0 = 6;
byte_214A8 = 0; //给全局变量赋值,这个地方还没有看出来全局变量有什么用
}
else if ( v2 == 11 )
{
byte_214A8 = 1;
dword_214AC = 80;
dword_214B0 = 7;
}
free(&tv);
if ( (*(int (__fastcall **)(int, int *, __int16 *))(*(_DWORD *)v1 + 24))(v1, &v8, &word_10006)
|| (v3 = sub_8F64(),
v4 = sub_8CF8(v3), //这边有三个函数
sub_8BA4(v4),
v5 = v8,
(v6 = (*(int (__fastcall **)(int, const char *))(*(_DWORD *)v8 + 24))(v8, "com/qtfreet/crackme001/MainActivity")) == 0) //动态注册off_21458所在位置的函数
|| (*(int (__fastcall **)(int, int, char **, signed int))(*(_DWORD *)v5 + 860))(v5, v6, off_21458, 1) < 0 )
{
result = (__int16 *)-1;
}
else
{
result = &word_10006;
}
return result;
}
sub_8F64()函数
[C++] 纯文本查看 复制代码 int sub_8F64()
{
int v1; // [sp+0h] [bp-28h]@1
char v2; // [sp+Ch] [bp-1Ch]@1
char v3; // [sp+Dh] [bp-1Bh]@1
v3 = 97;
v2 = 48;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 49;
v3 = 54;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 50;
v3 = 102;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 51;
v3 = 99;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 52;
v3 = 57;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 53;
v3 = 48;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 54;
v3 = 49;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 55;
v3 = 101;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 56;
v3 = 50;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 57;
v3 = 51;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 97;
v3 = 52;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 98;
v3 = 53;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 99;
v3 = 55;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 100;
v3 = 100;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 101;
v3 = 56;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 102;
v3 = 98;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 108;
v3 = 116;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 105;
v3 = 108;
sub_8ECE(&v1, &byte_21490, &v2);
v3 = 105;
v2 = 113;
sub_8ECE(&v1, &byte_21490, &v2);
v2 = 116;
v3 = 113;
return sub_8ECE(&v1, &byte_21490, &v2);
}
直接对照源码看吧
[C++] 纯文本查看 复制代码 void initMap() {
opcodemap.insert(std::make_pair(48, 97));//
opcodemap.insert(std::make_pair(49, 54));//
opcodemap.insert(std::make_pair(50, 102));//
opcodemap.insert(std::make_pair(51, 99));//
opcodemap.insert(std::make_pair(52, 57));//
opcodemap.insert(std::make_pair(53, 48));//
opcodemap.insert(std::make_pair(54, 49));//
opcodemap.insert(std::make_pair(55, 101));//
opcodemap.insert(std::make_pair(56, 50));//
opcodemap.insert(std::make_pair(57, 51));//
opcodemap.insert(std::make_pair(97, 52));//
opcodemap.insert(std::make_pair(98, 53));//
opcodemap.insert(std::make_pair(99, 55));//
opcodemap.insert(std::make_pair(100, 100));//
opcodemap.insert(std::make_pair(101, 56));//
opcodemap.insert(std::make_pair(102, 98));//
opcodemap.insert(std::make_pair(108, 116));//
opcodemap.insert(std::make_pair(105, 108));//
opcodemap.insert(std::make_pair(113, 105));//
opcodemap.insert(std::make_pair(116, 113));//
}
这边就是初始化了一张表,类似于java里的hashmap,后面就会知道这张表的用途了,
sub_8CF8()函数,这个函数的功能就是反调试了
[C++] 纯文本查看 复制代码 int sub_8CF8()
{
if ( pthread_create((pthread_t *)&dword_214B4, 0, (void *(*)(void *))sub_899E, 0) )
exit(-1);
return pthread_detach(dword_214B4);
}
跟进sub_899E看看,最终调用的方法在sub_E3E0()里
[C++] 纯文本查看 复制代码 __pid_t sub_E3E0()
{
__int32 v0; // r4@1
__pid_t result; // r0@3
int v2; // r5@3
FILE *v3; // r5@9
int v4; // r7@10
char s; // [sp+Ch] [bp-11Ch]@3
char v6; // [sp+8Ch] [bp-9Ch]@6
__int16 v7; // [sp+96h] [bp-92h]@10
int v8; // [sp+10Ch] [bp-1Ch]@1
v8 = _stack_chk_guard;
v0 = syscall(20);
if ( sub_E258() == 2 )
kill(v0, 9);
sprintf(&s, "/proc/%d/status", v0);
result = fork();
v2 = result;
if ( !result )
{
if ( ptrace(0, 0, 0, 0) == -1 )
exit(v2);
LABEL_9:
v3 = fopen(&s, "r");
do
{
if ( !fgets(&v6, 128, v3) )
{
LABEL_8:
sleep(0xAu);
goto LABEL_9;
}
}
while ( strncmp(&v6, "TracerPid", 9u) );
v4 = atoi((const char *)&v7);
fclose(v3);
syscall(6, v3);
if ( !v4 )
goto LABEL_8;
result = kill(v0, 9);
}
if ( v8 != _stack_chk_guard )
_stack_chk_fail(result);
return result;
}
可以看到这里就是比较简单的去获取TracerPid的值,并且该方法不带任何的返回值,所以在调试之前直接nop掉这个方法就行了
第三个函数sub_8BA4()就是难点之一了,因为里面的字符串我全部进行了加密,加密算法就是题目中用到的rc4,如果仔细去看的话还是比较恶心的
[C++] 纯文本查看 复制代码 v0 = sub_82F4("Zvmmq56ICjFmg0doGjPySSQxMpk+mEJr+onBi14r6O1J1wKFCRT1IZVRccvJ9Sq8BVY=");
s2 = (char *)sub_82F4((const char *)&unk_1E190);
v1 = (const char *)sub_82F4("Bf6ostDWGjZ4kEt3GDPhSy8/cJhv3BIE1pjYpXwn8apOyhCOVw==");
算法就没必要分析了,key都是硬编码在so里的,关键点就是此处了
[C++] 纯文本查看 复制代码 v13 = (const char *)sub_10574(v4, *(_DWORD *)(v12 + 4));
if ( !strcmp(v13, s2) )
{
v14 = sub_5334(v4, v11);
gettimeofday(&tv, 0);
v15 = (void *)((unsigned int)(v14 + 16) >> 12 << 12);
v22 = tv.tv_sec;
if ( !mprotect(v15, 0x1000u, 3) )
{
v16 = (_BYTE *)(v14 + 64);
v17 = (_BYTE *)(v14 + 72);
if ( byte_214A8 )
{
v18 = dword_214B0;
*v16 = dword_214AC;
}
else
{
v19 = dword_214B0;
*v16 = dword_214AC - 1;
v18 = v19 + 1;
}
*v17 = v18;
mprotect(v15, 0x1000u, 1);
gettimeofday(&tv, 0);
sub_E3C8(v22, tv.tv_sec);
}
}
v9 = v20 + 1;
}
free((void *)v10);
}
看到mprotect可能就需要注意是否有自篡改了,此处就用上了之前初始的三个全局变量,并且此处是有反调试的,反调试点就是壳常用的gettimeofday去判断单步调试的时间差,对照源码看
[C++] 纯文本查看 复制代码 if (!strcmp(dexStringById(pDexFile, pMethodId->nameIdx), findMethod)) {
const DexCode *pCode = dexGetCode(pDexFile, pMethod);
struct timeval tv;
gettimeofday(&tv, NULL);
int resTime = tv.tv_sec;
if (mprotect(PAGE_START((int) (pCode->insns)), PAGE_SIZE,
PROT_READ | PROT_WRITE) == 0) {
if (flag) {
*((u1 *) (pCode->insns) + 48) = (u1) opCodeReverse;
*((u1 *) (pCode->insns) + 56) = (u1) opCodeToString;
} else {
*((u1 *) (pCode->insns) + 48) = (u1) (opCodeReverse - 1);
*((u1 *) (pCode->insns) + 56) = (u1) (opCodeToString + 1);
}
mprotect(PAGE_START((int) (pCode->insns)), PAGE_SIZE, PROT_READ) == 0;
gettimeofday(&tv, NULL);
int destTime = tv.tv_sec;
CalcTime(resTime, destTime);
}
pCode->insns;
}
}
free(pClassData);
此处其实篡改的代码就是java层里的如下这个函数
[Java] 纯文本查看 复制代码 public void checkCode() {
String s = this.ed.getText().toString().trim();
StringBuilder sb = new StringBuilder();
sb.append(s);
if (check(sb.toString().toLowerCase().trim())) {
Toast.makeText(this, "Congratulations", 0).show();
} else {
Toast.makeText(this, "U are wrong~", 0).show();
}
}
在sb.toString()处改为了reverse(),将StringBuilder里的字符串全部进行了倒序,这个点其实也很好过,在动态调试时多看几次就知道名堂了,所以自篡改这个点也是比较容易去分析的,
接下来就是关键函数了,为了分析方便我们导入jni.h头文件
[C++] 纯文本查看 复制代码 v28 = _stack_chk_guard;
s = (char *)a1->functions->GetStringUTFChars(&a1->functions, a3, 0);
memset(&v27, 0, 0x400u);
sub_84FC(&v27);
v3 = sub_9198(&v27);
dest = (char *)&v25;
memset(&v25, 0, 0x10u);
v4 = (const char *)(v3 + 3);
v5 = 0;
strncpy((char *)&v25, v4, 0x10u);
v26 = 0;
v6 = strlen(s);
v23 = (int *)&(&v21)[-2 * ((unsigned int)(v6 + 7) >> 3)];
memset(&v21, 0, v6);
v21 = &_stack_chk_guard;
while ( v5 < v6 )
{
v7 = &byte_21490;
v8 = (unsigned __int8)s[v5];
for ( i = dword_21494; i; i = v10 )
{
if ( *(_BYTE *)(i + 16) < v8 )
{
v10 = *(_DWORD *)(i + 12);
i = (int)v7;
}
else
{
v10 = *(_DWORD *)(i + 8);
}
v7 = (char *)i;
}
if ( v7 != &byte_21490 && (unsigned __int8)v7[16] > v8 )
v7 = &byte_21490;
*((_BYTE *)v23 + v5++) = v7[17];
}
v11 = v23;
v12 = dest;
*((_BYTE *)v23 + v6) = 0;
v13 = (const char *)sub_8A18(v11, v12);
v14 = v13;
v15 = strlen(v13);
v16 = sub_EAE4(v14, v15);
v17 = (const char *)sub_835C(v16);
v18 = v17;
v19 = strlen(v17);
result = (unsigned int)memcmp("KO+257fsDx9esUUzWD7Uc39tRa84ix4W", v18, v19) <= 0;
if ( v28 != *v21 )
_stack_chk_fail(result);
return result;
既然传入的参数赋值给了s,我们就留意下s这个参数是怎么被使用的就行了,sub_84FC函数点进去发现这个就是获取当前的包名的,然后将获取到包名再去调用sub_9198函数就进行加密,这个是个md5,因为包名固定,所以md5也是固定的,这个动态一看就知道了
这个md5的作用是给aes加密当作密钥的,当然了我不会这么轻易直接用这个原md5去干,肯定要做些手脚,
[C++] 纯文本查看 复制代码 v4 = v3 + 3;
v5 = 0;
strncpy((char *)&v25, v4, 0x10u);
这个地方可以看到从第四位开始取数值,总共取16位
[C++] 纯文本查看 复制代码 v6 = strlen(s);
v23 = (int *)&(&v21)[-2 * ((unsigned int)(v6 + 7) >> 3)];
memset(&v21, 0, v6);
v21 = &_stack_chk_guard;
while ( v5 < v6 )
{
v7 = &byte_21490;
v8 = (unsigned __int8)s[v5];
for ( i = dword_21494; i; i = v10 )
{
if ( *(_BYTE *)(i + 16) < v8 )
{
v10 = *(_DWORD *)(i + 12);
i = (int)v7;
}
else
{
v10 = *(_DWORD *)(i + 8);
}
v7 = (char *)i;
}
if ( v7 != &byte_21490 && (unsigned __int8)v7[16] > v8 )
v7 = &byte_21490;
*((_BYTE *)v23 + v5++) = v7[17];
}
v11 = v23;
直接对照源码看
[C++] 纯文本查看 复制代码 int size = strlen(flag);
unsigned char content[size];
memset(content, 0, size);
for (int i = 0; i < size; i++) {
int temp = (int) flag[i];
content[i] = opcodemap.find(temp)->second;
}
content[size] = 0;
这个地方就是进行查表操作,将输入进来的字符串的每一位在表中进行查找并替换,后面就没啥好分析的了,使用的就是两个标准算法,在进行两次加密后与固定字符串进行对比
[C++] 纯文本查看 复制代码 const char final_flag[] = {
75, 79, 43, 50, 53, 55, 102, 115, 68, 120, 57, 101, 115, 85, 85, 122,
87, 68, 55, 85, 99, 51, 57, 116, 82, 97, 56, 52, 105, 120, 52, 87,
0, 99, 111, 109, 47, 113, 116, 102, 114, 101, 101, 116, 47, 99, 114, 97,
};
因为算法都是可逆的,并且key都已知,就不分析了
0x2 踩坑
1.在编译器优化掉符号表之后给大家解题会造成一定的困惑
2.算法,不常接触算法的话可能在看到算法时会比较头疼
3.置换表,在编译好的so里直接去看map函数显得很乱
4.反调试,该cm中使用了三种反调试检测手段,TracerPid,gettimeofday,/proc/%d/wchan,不过都比较明显,还是好处理的
5.自篡改,这个坑不大,多试几次就知道了
0x3 答案
|
免费评分
-
查看全部评分
|