-
-
[原创]看雪CTF2016 第27题分析-27-上帝溜猪
-
发表于: 2016-12-26 11:32 6122
-
此题计算是比较简单的,主要是用输入的SN各字节对一个KEY各字节依次XOR,再用XOR后的KEY各字节依次乘以0x5e并对Code进行XOR,亮点在于解密后的Code未知,之前不能知道正确的代码是什么。不过此题也有一些问题,主要有4:
1.SN各字节对KEY的各字节都要做XOR运算,实际上仅相当于所有SN各位XOR得到一个C,再用C对KEY各字节XOR,也就是说11字节SN做XOR之后得到一个字节C,这样的对应关系是多对一,这样必然会多解。
2.(KEY[i] XOR C) * 0x5E各字节也都要同Code各字节XOR,这也有问题,同上,也只相当于(KEY[i] XOR C) * 0x5E所有字节(长度20)XOR后得到另一个C2再同Code各字节XOR运算,而这个对应关系也是多对一,这也导致必定多解。
3.对解密后的Code没有做有效的验证,也没有捕获异常,导致输入不正确的SN程序就会崩溃。
4.程序的验证代码在输入字符时进行,当输入SN长度不少于11时触发,不过有个问题就是验证正确的不一致性:当手动一个个输入字符时输到第11个就触发验证,不会再有11个字符以上的情况发生,而采用复制粘贴时一次可以粘贴多个字符,这将导致验证正确的不一致性,也就是说同一个注册码一个一个输入可能是错的,不过一次粘贴进去可能是对的(也可能反之)。比如注册码:00000000000N就在一次性粘贴进去是对的,一个个输是错的,相反的有注册码:00000000aQNN在一次性粘贴进去是错的,一个个的输入是正确的(其实只有11位有效,后面一位还没输入就验证了,是多出来的)。
OD中验证代码如下分析见注释:
00401570 . 6A FF push -1
00401572 . 68 D81B4000 push 00401BD8 ; Entry point
00401577 . 64:A1 00000000 mov eax, fs:[0]
0040157D . 50 push eax
0040157E . 64:8925 00000000 mov fs:[0], esp
00401585 . 83EC 70 sub esp, 70
00401588 . 55 push ebp
00401589 . 56 push esi
0040158A . 57 push edi
0040158B . 8BF1 mov esi, ecx
0040158D . 6A 01 push 1 ; /Arg1 = 1
0040158F . 897424 18 mov ss:[esp+18], esi ; |
00401593 . E8 5E030000 call <jmp.&MFC42.#6334> ; \MFC42.#6334
00401598 . 8D4C24 0C lea ecx, [esp+0C]
0040159C . E8 19030000 call <jmp.&MFC42.#540> ; Jump to MFC42.#307
004015A1 . 8B46 60 mov eax, ds:[esi+60]
004015A4 . 8D56 60 lea edx, [esi+60]
004015A7 . C78424 84000000 00000000 mov dword ptr ss:[esp+84], 0
004015B2 . 8B68 F8 mov ebp, ds:[eax-8]
004015B5 . 83FD 0B cmp ebp, 0B ; //SN长度不少于11,其实一个个字符输入就是11个,复制粘贴可以多于11个
004015B8 . 0F8C D8000000 jl 00401696
004015BE . B9 18000000 mov ecx, 18
004015C3 . 33C0 xor eax, eax
004015C5 . 8D7C24 19 lea edi, [esp+19]
004015C9 . C64424 18 00 mov byte ptr ss:[esp+18], 0
004015CE . F3:AB rep stos dword ptr es:[edi]
004015D0 . 66:AB stos word ptr es:[edi]
004015D2 . 6A 00 push 0 ; /Arg1 = 0
004015D4 . 8BCA mov ecx, edx ; |
004015D6 . AA stos byte ptr es:[edi] ; |
004015D7 . E8 14030000 call <jmp.&MFC42.#2915> ; \MFC42.#2915, //取SN
004015DC . 8BCD mov ecx, ebp
004015DE . 8BF0 mov esi, eax
004015E0 . 8BD1 mov edx, ecx
004015E2 . 8D7C24 18 lea edi, [esp+18]
004015E6 . C1E9 02 shr ecx, 2
004015E9 . F3:A5 rep movs dword ptr es:[edi], dword ptr ds:[esi ; //复制SN到BUFFER
004015EB . 8BCA mov ecx, edx
004015ED . 33D2 xor edx, edx
004015EF . 83E1 03 and ecx, 00000003
004015F2 . 85ED test ebp, ebp
004015F4 . F3:A4 rep movs byte ptr es:[edi], byte ptr ds:[esi]
004015F6 . 7E 17 jle short 0040160F
004015F8 > 8A4C14 18 mov cl, ss:[edx+esp+18]
004015FC . 33C0 xor eax, eax
004015FE > 3088 20304000 xor ds:[eax+403020], cl ; //对0x403020的KEY用SN依次XOR,其实仅相当于SN所以位XOR得到一个C,用C对KEY来XOR
00401604 . 40 inc eax ; //因为所有SN各位XOR只得到一个C,这一点在程序的设计上有问题,必定多解
00401605 . 83F8 14 cmp eax, 14
00401608 .^ 7C F4 jl short 004015FE ; //KEY共20字节,依次XOR
0040160A . 42 inc edx
0040160B . 3BD5 cmp edx, ebp ; //SN各字节依次XOR
0040160D .^ 7C E9 jl short 004015F8
0040160F > C74424 10 00000000 mov dword ptr ss:[esp+10], 0
00401617 . FF15 08204000 call ds:[<&KERNEL32.GetCurrentProcessId>] ; [KERNEL32.GetCurrentProcessId
0040161D . 50 push eax ; /ProcessID
0040161E . 6A 00 push 0 ; |InheritHandle = FALSE
00401620 . 68 FF0F1F00 push 1F0FFF ; |Access = PROCESS_ALL_ACCESS
00401625 . FF15 04204000 call ds:[<&KERNEL32.OpenProcess>] ; \KERNEL32.OpenProcess
0040162B . 8BF8 mov edi, eax
0040162D . 8D4424 10 lea eax, [esp+10]
00401631 . 50 push eax ; /pBytesWritten
00401632 . 8D4C24 1C lea ecx, [esp+1C] ; |
00401636 . 6A 2C push 2C ; |Size = 44., //Code总长度为0x2c
00401638 . 51 push ecx ; |Buffer
00401639 . 68 40154000 push 00401540 ; |BaseAddress = CrackMe2.401540, Entry point
0040163E . 57 push edi ; |hProcess
0040163F . FF15 14204000 call ds:[<&KERNEL32.ReadProcessMemory>] ; \KERNEL32.ReadProcessMemory, //读加密的Code代码到BUFFER
00401645 . 85C0 test eax, eax
00401647 . 74 4D je short 00401696
00401649 . 33F6 xor esi, esi
0040164B > 8A86 20304000 mov al, ds:[esi+403020] ; //Code[j] ^= (KEY[i] ^ C) * 0x5e
00401651 . B2 5E mov dl, 5E
00401653 . 33C9 xor ecx, ecx
00401655 . F6EA imul dl
00401657 > 8A540C 18 mov dl, ss:[ecx+esp+18]
0040165B . 32D0 xor dl, al
0040165D . 88540C 18 mov ss:[ecx+esp+18], dl
00401661 . 41 inc ecx
00401662 . 83F9 2C cmp ecx, 2C ; //对所有Code(总长度为0x2c)依次XOR
00401665 .^ 7C F0 jl short 00401657
00401667 . 46 inc esi
00401668 . 83FE 14 cmp esi, 14 ; //对所有KEY(总长度为20)依次XOR
0040166B .^ 7C DE jl short 0040164B
0040166D . 8D4424 10 lea eax, [esp+10]
00401671 . 8D4C24 18 lea ecx, [esp+18]
00401675 . 50 push eax ; /pBytesWritten
00401676 . 6A 2C push 2C ; |Size = 44.
00401678 . 51 push ecx ; |Buffer
00401679 . 68 40154000 push 00401540 ; |BaseAddress = CrackMe2.401540, Entry point
0040167E . 57 push edi ; |hProcess
0040167F . FF15 00204000 call ds:[<&KERNEL32.WriteProcessMemory>] ; \KERNEL32.WriteProcessMemory, //回写解密后的Code
00401685 . 85C0 test eax, eax
00401687 . 74 0D je short 00401696
00401689 . 8B5424 14 mov edx, ss:[esp+14]
0040168D . 52 push edx
0040168E . E8 ADFEFFFF call 00401540 ; //调用解密后的Code
00401693 . 83C4 04 add esp, 4
00401696 > 8D4C24 0C lea ecx, [esp+0C]
0040169A . C78424 84000000 FFFFFFFF mov dword ptr ss:[esp+84], -1
004016A5 . E8 26010000 call <jmp.&MFC42.#800> ; [MFC42.#800
004016AA . 8B4C24 7C mov ecx, ss:[esp+7C]
004016AE . 5F pop edi
004016AF . 5E pop esi
004016B0 . 5D pop ebp
004016B1 . 64:890D 00000000 mov fs:[0], ecx
004016B8 . 83C4 7C add esp, 7C
004016BB . C3 retn
加密的提示注册成功的函数:
00401540 AE scas byte ptr es:[edi]
00401541 C5ACF0 F484C4AC lds ebp, ds:[esi*8+eax+ACC484F4] ; Modification of segment register
00401548 F0:F4 lock hlt ; LOCK prefix is not allowed
0040154A 84C4 test ah, al
0040154C AE scas byte ptr es:[edi]
0040154D C43B les edi, ds:[ebx] ; Modification of segment register
0040154F D130 sal dword ptr ds:[eax], 1 ; Undocumented instruction or encoding
00401551 E5 84 in eax, 84 ; I/O command
00401553 C44F 88 les ecx, ds:[edi-78] ; Modification of segment register
00401556 E0 C0 loopnz short 00401518
00401558 AC lods byte ptr ds:[esi]
00401559 2C C7 sub al, 0C7
0040155B C4C4 les eax, esp ; Illegal use of register
0040155D 2C 4C sub al, 4C
0040155F C7C4 C4AEC44F mov esp, 4FC4AEC4 ; Suspicious use of stack pointer
00401565 0C 2C or al, 2C
00401567 BD C7C4C407 mov ebp, 7C4C4C7
由以上可以看出验证过程:
设输入注册码为SN
常量KEY在0x403020为:
BYTE KEY[20] = {0xCC,0xAA,0xBD,0xDD,0xCB,0xBA,0xB2,0x92,0xAF,0xBA,0xB4,0xB9,0xB0,0xAC,0xCB,0xBA,0xCE,0xD0,0xDF,0xDD};
常量Code在0x401540为:
BYTE Code[0x2c] = {0xAE,0xC5,0xAC,0xF0,0xF4,0x84,0xC4,0xAC,0xF0,0xF4,0x84,0xC4,0xAE,0xC4,0x3B,0xD1,0x30,0xE5,0x84,0xC4,0x4F,0x88,0xE0,0xC0,0xAC,0x2C,0xC7,0xC4,0xC4,0x2C,0x4C,0xC7,0xC4,0xC4,0xAE,0xC4,0x4F,0x0C,0x2C,0xBD,0xC7,0xC4,0xC4,0x07};
int lenSN = strlen(SN);
int i,j;
for(i = 0; i < lenSN; i++)
{
for(j = 0; j < sizeof(KEY); j++){
KEY[j] ^= SN[i];
}
}
for(i = 0; i < sizeof(KEY); i++){
for(j = 0; j < sizeof(Code); j++){
Code[j] ^= KEY[i] * 0x5e;
}
}
Code();
由上面的分析可以简化运算:
BYTE C = 0;
for(i = 0; i < lenSN; i++){
C ^= SN[i];
}
BYTE C2 = 0;
for(i = 0; i < sizeof(KEY); i++){
C2 ^= (KEY[i] ^ C) * 0x5e;
}
for(i = 0; i < sizeof(Code); i++){
Code[i] ^= C2;
}
Code();
由上面简化后的分析,不难想到此题的解法:
1.首先要找到一个字节常量C2,用其对Code解密后的Code将是正常的代码,能正确运行,并能显示注册成功的信息,当然,代码的内容是什么我们之前是不可知的怎么显示成功信息我们也不用太关心,因为代码解密只需要一个字节的数据C2,一个字节也就只有255种情况(不会为0,为0时代码不会变),写段代码枚举一下就行,不过当然需要人肉(人眼观察用C2解密后的代码是否是比较正常的代码):
BYTE Code2[0x2c];
for(BYTE C2 = 1 ; C2; C2++){
for(j = 0; j < sizeof(Code); j++){
Code2[j] = Code[j] ^ C2;
}
printf("%2c",C2); //在这儿看反汇编的Code解密后的结果Code2,要是看上去是乱七八糟的代码就继续。。。
}
运行以上程序,并观察结果,当C2为0xc4时,能得到看上去正常的代码(这是偶输入正确注册码时抓的代码,上面的Code2地址当然不是这个,不过代码看上去比较正常的就这一个,0xc4后面的偶就没试了):
00401540 6A 01 push 1
00401542 68 34304000 push offset 00403034 //'成功'
00401547 68 34304000 push offset 00403034 //'成功'
0040154C 6A 00 push 0
0040154E FF15 F4214000 call ds:[<&USER32.MessageBoxA>]
00401554 8B4C24 04 mov ecx, ss:[esp+4]
00401558 68 E8030000 push 3E8
0040155D E8 88030000 call <jmp.&MFC42.#3092> ; Jump to MFC42.#3092
00401562 6A 00 push 0
00401564 8BC8 mov ecx, eax
00401566 E8 79030000 call <jmp.&MFC42.#2642> ; Jump to MFC42.#2642
0040156B C3 retn
当然对于求上面的C2还有捷径可以走的:因为解密后的代码为一个函数,一般情况下函数最后应该是一条返回指令,在8086指令集中一般是这两条:
1). 一字节指令:
0xc3 RETN
2). 三字节指令:
0xc2 xx xx RETN xxxx
考察未解密的那段代码的最后三个字节0xc4, 0xc4, 0x07,对以上二种情况对比:
1).如果是一字节RETN指令,最后一字节XOR一个常数字节C2要得到0xc3,这个C2就是:
C2 = 0xc3 ^ 0x07 = 0xc4
2).如果是三字节RETN指令,最后三个字节XOR一个常数字节C2要得到 0xc2 xx xx,这个C2就是:
C2 = 0xc2 ^ 0xc4 = 0x06
最后三字节都XOR C 将是:
0xc2 0xc2 0x1 RETN 0x1c2 //看上去不太对哦,要弹出0x1c2个字节,能有这么多参数嘛?
这就得到了解密的常量字节C2=0xc4,当然函数最后也可能不是RETN指令,也可能RETN不在最后一个字节,这种方法就不一定正确了,正确与否只需要用它XOR所有的未解密代码,观察解密后的代码是否是乱码就知道了.
2.由之前的分析,要得到正确的C2=0xc4,就要去找前面那个C,这个也简单:
for(BYTE C = 1; C; C++){
BYTE C2 = 0;
for(j = 0; j < sizeof(KEY); j++){
C2 ^= (KEY[j] ^ C) * 0x5e;
}
if(C2 == 0xc4){ //0xc4是上一步得到的
printf("0x%x", C);
}
}
这样可以得到可用的C不只一个:
0x1e
0x3e
0x5e
0x7e
0x9e
0xbe
0xde
0xfe
以上这些都行,也就是说对于SN(不少于11位)各位XOR后的值为以上几个的所有SN都应该是能提示注册成功的SN(当然,长过11的SN有复制粘贴与一个个输入的差别)。
3.计算SN,SN各位XOR之后的值在C的列表中{0x1e,0x3e,0x5e,0x7e,0x9e,0xbe,0xde,0xfe}就行,这个要是写个枚举过程可以找出无数个,不过这儿手工找一两个就行了:
因为C是SN各位XOR来的,所以每相同的二个SN字节XOR后为0,先来几个双数个相同的,后面的XOR凑到上面的数就行了:
我选二个
00000000000N 12位的,因为前10个相同XOR结果为0,而 '0' ^ 'N' = 0x30 ^ 0x4e = 0x7e 在上面的C的列表中
00000000aQN 11位的,因为前8个相同XOR结果为0,而'a' ^ 'Q' ^ 'N' = 0x61 ^ 0x51 ^ 0x4e = 0x7e 在上面的C的列表中
不过正如前面我说提到的程序验证正确的不一致性,00000000000N 这个注册码只能复制粘贴,不能一个一个的输入!
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课