BUUCTF - 网鼎杯 2020 青龙组 - jocker 分析与记录

First Post:

Last Update:

         无壳,IDA打开可以直接进入main函数:

        第12行调用VirtualProtect函数更改了offset encrypt处的访问保护权限

1
2
3
4
5
6
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

参见:https://docs.microsoft.com/en-us/windows/win32/memory/memory-protection-constants

         该处数据为0x4:PAGE_READWRITE

Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled, attempting to execute code in the committed region results in an access violation.

        简单来说就是让这块数据能够被读写了(通常text段中只能拥有读/写中的一种)

        继续往下读,发现输入值应该符合 24字节 的长度,然后遇到wrong 和 omg这两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char *__cdecl wrong(char *a1)
{
char *result; // eax
int i; // [esp+Ch] [ebp-4h]

for ( i = 0; i <= 23; ++i )
{
result = &a1[i];
if ( (i & 1) != 0 )
a1[i] -= i;
else
a1[i] ^= i;
}
return result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl omg(char *a1)
{
int result; // eax
int v2[24]; // [esp+18h] [ebp-80h] BYREF
int i; // [esp+78h] [ebp-20h]
int v4; // [esp+7Ch] [ebp-1Ch]

v4 = 1;
qmemcpy(v2, &unk_4030C0, sizeof(v2));
for ( i = 0; i <= 23; ++i )
{
if ( a1[i] != v2[i] )
v4 = 0;
}
if ( v4 == 1 )
result = puts("hahahaha_do_you_find_me?");
else
result = puts("wrong ~~ But seems a little program");
return result;
}

        wrong对输入值进行了一些加减或异或处理,然后将结果在omg中同unk_4030C0处数据进行对比;wrong的逆算法容易实现,照抄就行了

         (现在才知道能够通过导出窗口快捷的提取出数据,一直以来的手抄实在是太笨了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int k[24] = { 0x66,0x6b,0x63,0x64,0x7f,0x61,0x67,0x64,0x3b,0x56,0x6b,0x61,0x7b,0x26,0x3b,0x50,0x63,0x5f,0x4d,0x5a,0x71,0xc,0x37,0x66 };
for (int i = 0;i < 24; i++)
{
if ((i & 1) != 0)
{
k[i] += i;
}
else
{
k[i] ^= i;
}
cout << (char)k[i];
}
cout << endl;

        得到结果flag{fak3_alw35_sp_me!!},提交发现错误;由于往下还有关键的encrypt段没分析,所以不用太怀疑flag是否算错,可以大胆的将它当作一个假的flag

        再往下读for循环,发现它对offset encrypt进行了异或,判断其为代码段解密,可以用动调转到这个地方

         IDA没能及时更新,需要我们手动修正为函数

        选中00401500~0040152F,将其标为代码(Force)

         然后在00401502处创建函数,即可得到合适的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// positive sp value has been detected, the output may be wrong!
void __usercall __noreturn sub_401502(int a1@<ebp>)
{
unsigned __int32 v1; // eax

v1 = __indword(0x57u);
*(_DWORD *)(a1 - 32) = 1;
qmemcpy((void *)(a1 - 108), &unk_403040, 0x4Cu);
for ( *(_DWORD *)(a1 - 28) = 0; *(int *)(a1 - 28) <= 18; ++*(_DWORD *)(a1 - 28) )
{
if ( (char)(*(_BYTE *)(*(_DWORD *)(a1 - 28) + *(_DWORD *)(a1 + 8)) ^ Buffer[*(_DWORD *)(a1 - 28)]) != *(_DWORD *)(a1 + 4 * *(_DWORD *)(a1 - 28) - 108) )
{
puts("wrong ~");
*(_DWORD *)(a1 - 32) = 0;
exit(0);
}
}
if ( *(_DWORD *)(a1 - 32) == 1 )
puts("come here");
}

        IDA分析得到的代码并不是那么易读,显然,它将一些索引给翻译错了,但并非无法理解的程度

        首先,提取unk_403040处的数据放入(a1-108)处,以及循环中用到的Buffer

1
2
char Buffer[] = "hahahaha_do_you_find_me?";
unsigned int unk_403040[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };

         *(a1-28)实际上是一个索引,指示了这个循环会执行19次;而(*(a1 - 28) + *(a1 + 8))相当于输入值指针加上一个偏移,其内容就是我们的输入值

        这个输入值和Buffer异或后的结果应该等于(a1 - 108)的内容,也就是unk_403040处的数据,同样也容易写出解密代码

1
2
3
4
5
6
7
8
char key1[] = "hahahaha_do_you_find_me?";
unsigned int f[19] = {0x0E,0x0D ,0x09 ,0x06 ,0x13 ,0x05 ,0x58 ,0x56 ,0x3E ,0x06 ,0x0C ,0x3C ,0x1F ,0x57 ,0x14 ,0x6B ,0x57 ,0x59 ,0x0D };
for (int i = 0; i < 19; i++)
{
f[i] ^= key1[i];
cout << (char)f[i];
}
cout << endl;

得到flag{d07abccf8a410c

我们知道,flag应有24字节,但for循环只有19次,也就是缺少了5个字符;由于encrypt函数已经读完了,所以我们需要的结果应该在最后一个函数中,也就是finally函数

        将40159A~40159D处的数据全都转为代码,并将函数改为Undefine

         重新在40159A处创建函数,得到新函数finally:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl finally(char *a1)
{
unsigned int v1; // eax
int result; // eax
char v3[9]; // [esp+13h] [ebp-15h] BYREF
int v4; // [esp+1Ch] [ebp-Ch]

strcpy(v3, "%tp&:");
v1 = time(0);
srand(v1);
v4 = rand() % 100;
if ( (v3[*&v3[5]] != a1[*&v3[5]]) == v4 )
result = puts("Really??? Did you find it?OMG!!!");
else
result = puts("I hide the last part, you will not succeed!!!");
return result;
}

        time(0)用以获取当前时间,第10行将其作为种子,第11行获取随机数;大概率我们是难以获取到出题人得到的种子的,因此,这个随机数若是必要的,应该只能通过预测得出

        以及下面的if判断条件过于难以理解,不妨试着用OD去动调一下吧(个人觉得OD的动调会更好用一些,也好在这个函数没有被加密,OD还是能分析出来的,否则只能用IDA动调了,虽然没什么差别……)

        即便用OD动调也仍然不是很容易能够读懂其意义 

        关键的比较在401617处,如果相等的话,就说明flag输对了

        大致就是取flag的第几位同“**%tp&:**”几位,相等即可;并且这正好是5个字节,很可能就是剩下的flag

        但汇编代码中似乎也同样没有相应的加密过程,只能靠猜测它没有被复杂的加密

        通过前半段的flag猜测最后一个字符应该为‘}’,将其与“**%tp&:**”的最后一个异或后得到 71,并由此得到最后结果

1
2
3
4
5
6
7
char key2[] = "%tp&:";
int v5 = '}' ^ key2[4];
for (int i = 0; i < 5; i++)
{
cout << (char)(key2[i] ^ v5);
}
//flag{d07abccf8a410cb37a}

        我也试着将这个提交成功的flag输入进去,但它仍然不会输出成功的标识,可能是出题人的一点“恶意”吧……最后要靠猜测来得到结果,说实在的,有点难以释然,总觉得是不是自己看漏了什么重要内容…… ​

插画ID:90713460