2021 KCTF 第七题 千里寻根
首先用x64dbg调试,发现有壳,很多smc,一层套一层,而且好像有反调试,不能直接跑起来
不想脱壳,直接运行exe,等待输入序列号的时候,再用调试器附加
通过ReadFile回溯定位到读取输入序列号的位置:
00000001400045C7 | 48 | lea rdx,qword ptr ds:[14027E065] | 输入缓冲区
00000001400045CE | 48 | lea rcx,qword ptr ds:[14027E01C] | "%500s"
00000001400045D5 | E8 | call crackme.14027CA68 |
往下单步看一看,找找感觉,走到处理输入的地方:
0000000140002E06 | 48 | lea rcx,qword ptr ds:[14027E065] |
0000000140002E0D | E8 | call crackme.140001000 | 输入字符16进制换
继续走看到这个:
0000000140002FA2 | 0F | rdtsc | 随机数
0000000140004787 | 48 | mov rcx,6400000 |
000000014000478E | 48 | div rcx |
。。。
0000000140002A94 | FC | cld |
0000000140002A95 | F3 | rep movsq | 把代码拷到140280000区段内的一个随机偏移处
0000000140002A98 | 48 | sub rcx,rcx |
0000000140002A9B | 48 | or rcx,rax |
0000000140002A9E | 48 | and rcx,7 |
0000000140002AA2 | F3 | rep movsb |
。。。
0000000140004672 | 65 | mov rsi,qword ptr gs:[10] | 堆栈
后面就是利用gs:[10]复制堆栈数据并切换堆栈,跳转到新代码空间执行,销毁前一轮代码,smc解码,一般可能会只夹杂一行有效代码,
然后重新rdtsc取随机偏移,拷贝代码和数据,切换新堆栈,进入下一轮smc
感觉对我来说都是体力劳动,实在也没啥太多技术性的描述
可以对gs:[10]下了个硬件写断点,每次修改后意味着要进入下一轮了
根据硬件断点的命中次数,应该是循环了2021轮
旅途无比艰辛啊,直接瞻仰一下最后一轮的代码吧:
0000000140501331 | B9 | mov ecx,11 | 比较长度
0000000140501336 | E8 | call crackme.14050133B |
000000014050133B | 5E | pop rsi |
000000014050133C | 48 | add rsi,616C | 相对寻址到用序列号解码出的用户名
0000000140501343 | E8 | call crackme.140501348 |
0000000140501348 | 5F | pop rdi |
0000000140501349 | 48 | add rdi,16 | 相对寻址到输入的用户名
000000014050134D | F3 | repe cmpsb | 比较结果
000000014050134F | 85 | test ecx,ecx |
0000000140501351 | E9 | jmp crackme.1405000B2 |
00000001405000B2 | 9C | pushfq |
00000001405000B3 | E8 | call crackme.1405000B8 |
00000001405000B8 | 5F | pop rdi |
00000001405000B9 | 48 | add rdi,DD |
00000001405000C0 | 48 | mov rax,C3C3C3C3C3C3C3C3 |
00000001405000CA | BA | mov edx,130A |
00000001405000CF | 8B | mov ecx,edx |
00000001405000D1 | C1 | shr ecx,3 |
00000001405000D4 | FC | cld |
00000001405000D5 | F3 | rep stosq | 销毁部分代码
00000001405000D8 | 8B | mov ecx,edx |
00000001405000DA | 83 | and ecx,7 |
00000001405000DD | F3 | rep stosb |
00000001405000DF | 9D | popfq |
00000001405000E0 | 75 | jne crackme.140500122 | 分支显示结果正确或错误
最后就是到每一轮中去找出那一行有效逻辑代码,感觉自动化识别我两三天也写不好,我选择肉眼识别
毕竟原始代码的执行是要恢复到真实堆栈位置的,可以利用rsp的位置辅助判断
过程很漫长,实际可写的内容却不多,下面描述调试过程中记录下来的验证算法逻辑:
16进制转换后的序列号是32个字节,平分成两半,用一个什么算法分别解密,两次输入排序不同,算法很短,眼熟却想不起名字了
解密出来的32个字节看上去都是可显ascii,确认后是个变种base64的结果
base64解码后得到24个字节,前8个字节经过xor操作刚好等于用户名的前8个字符
后面16个字节又是用前面那种算法解密,只是排序不同
最后解密出的16个字节就是完整的用户名了
用户名:KCTF
序列号:B91AE5FCDA57D87406968CBDB8829799790A77302D7E8754B705894489B37A10
keygen:
void encode(WORD *t,WORD *o)
{
WORD u,v,w,y,k,e,n,h;
y = t[4] - t[5];
v = t[2] + t[3];
u = t[0] ^ t[1];
w = u & v;
h = ((~u & y) | w);
k = get_bits1(t[6] ^ t[7]);
n = (h * u >> k) + 0x18;
e = (h | n) & (y ^ n) | (h & n);
o[6] = rotl16(t[7], 16 - k) ^ h;
o[7] = rotl16(t[6], 16 - k) ^ h;
o[4] = t[4] - n;
o[5] = t[5] - n;
o[3] = t[2] - (y ^ n);
o[2] = t[3] + (y ^ n);
o[0] = e ^ t[1];
o[1] = e ^ t[0];
}
void en1(BYTE *t, BYTE *o)
{
BYTE k[] = {0x3C,0x14,0xEA,0xB0,0xD9,0xF5,0x67,0x82};
BYTE a[8];
DWORD i;
for (i=0;i<8;i++)
{
a[i] = k[i] ^ t[i];
}
o[3] = a[0];
o[2] = o[3] ^ a[2];
o[1] = o[2] ^ a[1];
o[0] = o[1] ^ a[3];
o[7] = a[4];
o[6] = o[7] ^ a[6];
o[5] = o[6] ^ a[5];
o[4] = o[5] ^ a[7];
}
void x1(BYTE *t)
{
BYTE x[0x10];
x[0x09] = t[0];
x[0x0A] = t[1];
x[0x0B] = t[2];
x[0x0C] = t[3];
x[0x05] = t[4];
x[0x06] = t[5];
x[0x07] = t[6];
x[0x08] = t[7];
x[0x03] = t[8];
x[0x04] = t[9];
x[0x01] = t[10];
x[0x02] = t[11];
x[0x0D] = t[12];
x[0x0E] = t[13];
x[0x0F] = t[14];
x[0x00] = t[15];
memcpy(t, x, 16);
}
void x2(BYTE *t)
{
BYTE x[0x10];
x[0x06] = t[0];
x[0x09] = t[1];
x[0x08] = t[2];
x[0x0B] = t[3];
x[0x02] = t[4];
x[0x05] = t[5];
x[0x04] = t[6];
x[0x07] = t[7];
x[0x00] = t[8];
x[0x03] = t[9];
x[0x0E] = t[10];
x[0x01] = t[11];
x[0x0A] = t[12];
x[0x0D] = t[13];
x[0x0C] = t[14];
x[0x0F] = t[15];
memcpy(t, x, 16);
}
void x3(BYTE *t)
{
BYTE x[0x10];
x[0x0E] = t[0];
x[0x0D] = t[1];
x[0x0C] = t[2];
x[0x0B] = t[3];
x[0x02] = t[4];
x[0x01] = t[5];
x[0x00] = t[6];
x[0x0F] = t[7];
x[0x04] = t[8];
x[0x03] = t[9];
x[0x06] = t[10];
x[0x05] = t[11];
x[0x0A] = t[12];
x[0x09] = t[13];
x[0x08] = t[14];
x[0x07] = t[15];
memcpy(t, x, 16);
}
void base64en(BYTE *o, BYTE *p)
{
DWORD i;
memset(o, 0, 0x20);
for (i=0;i<8;i++)
{
o[i*4+0] |= (p[i*3+0] & 0x03);
o[i*4+0] |= (p[i*3+2] & 0x0F) << 2;
o[i*4+1] |= (p[i*3+0] & 0x0C) >> 2;
o[i*4+1] |= (p[i*3+1] & 0x3C);
o[i*4+2] |= (p[i*3+0] & 0xF0) >> 4;
o[i*4+2] |= (p[i*3+1] & 0x03) << 4;
o[i*4+3] |= (p[i*3+1] & 0xC0) >> 2;
o[i*4+3] |= (p[i*3+2] & 0xF0) >> 4;
}
BYTE *s = (BYTE *)"0O/+KuChaNiUB1FAckL4Hot9zlWEMpm2TG5Sb6Ixdeq8YJPjygXQsRvwVZr73Dnf";
for (i=0;i<0x20;i++)
{
o[i] = s[o[i]];
}
}
void kg()
{
BYTE x[0x10] = {0};
BYTE y[0x20] = {0};
BYTE z[0x20] = {0};
BYTE o[0x20] = {0};
DWORD i;
for (i=0;i<0x10;i++)
{
x[i] = (BYTE)(0x1F - i);
}
memcpy(x, "KCTF", 5);
en1(x, y);
encode((WORD *)&x[0], (WORD *)&y[8]);
x3(&y[8]);
base64en(z, y);
encode((WORD *)&z[0], (WORD *)&o[0]);
x1(&o[0]);
encode((WORD *)&z[0x10], (WORD *)&o[0x10]);
x2(&o[0x10]);
for (i=0;i<32;i++)
{
printf("%02X", o[i]);
}
}
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
最后于 2021-5-23 10:53
被ccfer编辑
,原因: