去年没参加
今年又赶上考试月
只能先整理之前的题目了
0x01 Helllo-CTF
签到题:
IDA -> sheft+F12打开字符串界面
即能看到passwod
0x02 ctf2017_Fpc
接收输入流使用scanf读取,存在栈溢出,测试后发现可以直接跳转到成功处,但存在不可见字符,猜测不是正解:
sub_401050:
.text:00401050 var_C = dword ptr -0Ch
.text:00401050
.text:00401050 sub esp, 0Ch
.text:00401053 push offset aCodedByFpc ; " Coded by Fpc.\n\n"
.text:00401058 call sub_413D42
.text:0040105D add esp, 4
.text:00401060 push offset aPleaseInputYou ; " Please input your code: "
.text:00401065 call sub_413D42
.text:0040106A add esp, 4
.text:0040106D lea eax, [esp+0Ch+var_C]
.text:00401071 push eax
.text:00401072 push offset aS ; "%s"
.text:00401077 call _scanf
.text:0040107C lea eax, [esp+14h+var_C]
.text:00401080 add esp, 14h
.text:00401083 retn
接着调用两个函数检测输入字符串,提取其中的判断式:
v1 && v0 && v1 != v0 && 5 * (v1 - v0) + v1 == 0x8F503A42&& 13 * (v1 - v0) + v0 == 0xEF503A42
v1 && v0 && v1 != v0 && 17 * (v1 - v0) + v1 == 0xF3A94883 && 7 * (v1 - v0) + v0 == 0x33A94883
即:
v1!=0
v0!=0
5 * (v1 - v0) + v1 = 0x8F503A42
13 *(v1 - v0) + v0 = 0xEF503A42
17 *(v1 - v0) + v1 = 0xF3A94883
7 * (v1 - v0) + v0 = 0x33A94883
无解
接着看程序:
发现可疑点:
.text:00413131 db 83h, 0C4h, 0F0h
.text:00413134 dd 20712A70h, 0F1C75F2h, 28741C71h, 2E0671DDh, 870F574h
.text:00413134 dd 74F17169h, 0DC167002h, 0EA74C033h, 0DC261275h, 0F471E771h
.text:00413134 dd 6903740Fh, 0EB75EB70h, 0FDF7069h, 22712C70h, 0B8261F7Dh
.text:00413134 dd 2B741E71h, 3E067169h, 870F57Ch, 7CF17169h, 0DC197002h
.text:00413134 dd 41B034A3h, 75E77400h, 0E571DC12h, 7CDCF271h, 0E9706903h
.text:00413134 dd 6965E97Dh, 70B8DC70h, 3E1D7127h, 710F1971h, 0DD257019h
.text:00413134 dd 0F6700571h, 71DD0870h, 700270F2h, 70580F14h, 0F1171ECh
............
.text:00413131 后跟着一堆花指令
猜测这里是真正验证的地方
而且恰巧可以借助scanf字符串11A(可见字符)覆盖返回地址来跳转到413131处,返回地址与栈中存储输入流的距离是0xc,我们输入:
aaaaaaaaaaaa11A
动态调试忽略无关跳转
得到真正的验证代码:
add esp, 0FFFFFFF0h
mov dword_41B034, eax
pop eax ;前四位
mov ecx, eax
pop eax ;中四位
mov ebx, eax
pop eax ;后四位
mov edx, eax
mov eax, ecx
sub eax, ebx
//判断相等
shl eax, 2
add eax, ecx
//判断正负
add eax, edx
sub eax, 0EAF917E2h
//判断相等
add eax, ecx
sub eax, ebx
mov ebx, eax
shl eax, 1
add eax, ebx
add eax, ecx
mov ecx, eax
add eax, edx
sub eax, 0E8F508C8h
//判断相等
mov eax, ecx
sub eax, edx
//判断相等
sub eax, 0C0A3C68h
//判断相等
可得:
前三位:v1
中三位:v2
后三位:v3
(v1-v2)*4+v1+v3=0xEAF917E2
(v1-v2)*3+v1+v3=0xE8F508C8
(v1-v2)*3+v1-v3=0xC0A3C68
即得:
v0=7473754A
v1=726F6630
v2=6E756630
拼接后再加上溢出用的11A即得:
Just0for0fun11A
0x02 crackMe
定位WinMain函数:
找到了窗口处理函数DialogFunc:
.text:00434CC8 push 0 ; dwInitParam
.text:00434CCA push offset DialogFunc ; lpDialogFunc
.text:00434CCF push 0 ; hWndParent
.text:00434CD1 push 65h ; lpTemplateName
.text:00434CD3 mov eax, [ebp+hInstance]
.text:00434CD6 push eax ; hInstance
.text:00434CD7 call ds:DialogBoxParamA
跳转到DialogFunc
开始是一大堆反调试函数,先抛去不看
看到GetDlgItemTextA,主要流程在这里开始:
.text:0043505C add esp, 0Ch
.text:0043505F mov esi, esp
.text:00435061 push 401h ; cchMax
.text:00435066 lea eax, [ebp+String]
.text:0043506C push eax ; lpString
.text:0043506D push 3E9h ; nIDDlgItem
.text:00435072 mov ecx, [ebp+hDlg]
.text:00435075 push ecx ; hDlg
.text:00435076 call ds:GetDlgItemTextA
base64decode
接下来进入第一个函数:sub_42D267:
依然调用很多反调试函数:
查看伪代码,发现这里类似一个base64解码操作
简单测试一下:
dump下来,除去反调试改写成可编译C文件:
#include<stdio.h>
int main()
{
signed int result;
unsigned int v4;
int v5;
unsigned int i;
int code[1024]={77,84,73,122}; // "123".encode("base64")='MTIz'
int a2=1024;
unsigned char chars[] =
{
0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65,
0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
0x7A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x39, 0x2B, 0x2F, 0x3E, 0xFF, 0xFF, 0xFF, 0x3F, 0x34, 0x35,
0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x1A, 0x1B, 0x1C,
0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26,
0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30,
0x31, 0x32, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
int answer[1024]={0};
v5 = 0;
for ( i = 0; i <1024 ; ++i )
{
v4 = chars[code[i]];
switch ( i % 4 )
{
case 0:
answer[v5] = 4 * v4;
break;
case 1:
answer[v5++] += (v4 >> 4) & 3;
if ( i < a2 - 3 || code[a2 - 2] != 61 )
answer[v5] = 16 * (v4 & 0xF);
break;
case 2:
answer[v5++] += (v4 >> 2) & 0xF;
if ( i < a2 - 2 || code[a2 - 1] != 61 )
answer[v5] = (v4 & 3) << 6;
break;
case 3u:
answer[v5++] += v4;
break;
default:
continue;
}
}
printf("%d %d %d %d\n",answer[0],answer[1],answer[2]);
return 0;
}
//gcc 1.c
// ./a.out
//49 50 51 (1 2 3)
发现这是标准的base64解码算法
不过在判断解码结尾时类似规避了报错
Morsedecode
两次base64解码后
解码后的字符串进入sub_435DE0
看到关键处:
if ( a1[v9] == 32 || a1[v9] == 47 )
{
if ( a1[v9] != 32 || a1[v9 - 1] == 47 )
{
if ( a1[v9] == 47 )
*(_BYTE *)(v7++ + a2) = 32;
}
else
{
if ( (unsigned __int8)sub_42D0DC(v10, &v5) != 1 || v8 >= 5 )
{
if ( (unsigned __int8)sub_42D7B2(v10, &v5) == 1 )
{
*(_BYTE *)(v7++ + a2) = v5;
}
else if ( sub_42E414((int)v10, (int)&v5) == 1 )
{
*(_BYTE *)(v7++ + a2) = v5;
}
else
{
j__printf("error !\n");
}
}
else
{
*(_BYTE *)(v7++ + a2) = v5;
}
v8 = 0;
j__memset(v10, 42, 8u);
}
}
else
{
*((_BYTE *)v10 + v8++) = a1[v9];
}
++v9;
}
以及解码用到的数据表:
显然是一个摩斯密码解密,且以空格为分隔符
且"空格"前有"/"视为转义,解密为空格
单一"/"会被解密为空格
SM3
接下来两次base64解码后的字符串进入sub_437E70:
开始时其调用了一个函数初始化了一段数据:
.text:0043673B mov dword ptr [eax+8], 7380166Fh
.text:00436742 mov eax, [ebp+arg_0]
.text:00436745 mov dword ptr [eax+0Ch], 4914B2B9h
.text:0043674C mov eax, [ebp+arg_0]
.text:0043674F mov dword ptr [eax+10h], 172442D7h
.text:00436756 mov eax, [ebp+arg_0]
.text:00436759 mov dword ptr [eax+14h], 0DA8A0600h
.text:00436760 mov eax, [ebp+arg_0]
.text:00436763 mov dword ptr [eax+18h], 0A96F30BCh
.text:0043676A mov eax, [ebp+arg_0]
.text:0043676D mov dword ptr [eax+1Ch], 163138AAh
.text:00436774 mov eax, [ebp+arg_0]
.text:00436777 mov dword ptr [eax+20h], 0E38DEE4Dh
.text:0043677E mov eax, [ebp+arg_0]
.text:00436781 mov dword ptr [eax+24h], 0B0FB0E4Eh
而后接着对字符串前三位进行了加密
搜索这些数据
发现这里是SM3加密
迷宫算法
而后将Morsedecode后的结果传入sub_435400
可以看出这是一个10*10的迷宫:
0 1 1 1 1 1 1 1 1 0
0 0 1 1 1 1 1 0 0 0
1 0 0 0 0 0 1 0 1 1
1 1 1 1 1 0 1 0 0 1
1 0 0 0 1 0 1 0 0 1
1 0 1 0 0 0 1 0 1 1
1 0 1 1 1 1 1 0 0 1
1 0 0 0 0 1 1 1 0 0
1 1 1 1 0 0 0 0 1 0
1 1 1 1 1 1 1 0 0 0
其中:
0为可走点
q z分别表示向上和向下
p l分别表示向左和向右
其次有一个注意点,程序有一处判断:
v5 != 8 || v4 != 3
故而第3行第8列的0不能走
不过这个算法在判断时可以绕过:
程序开始检测:
while ( *a2 != 32 )
当我们走任意可行步(包括0步)都可以在结尾加上一个空格构造绕过
反调试
可以把文件的重定位表地址rv清0便于调试
利用CheckRemoteDebuggerPresent、ZwQueryInformationProcess和IsDebuggerPresent等API调用判断调试状态,检查调试类驱动,判断打开程序窗体名,检查进程,抛出异常
不过都很好绕过,直接全部nop掉,或者调试断点时绕过,或者在对应函数修改第一条指令为ret即可
序列号
尽量简化过程我们最终构造的字符串"code"经解密传入迷宫时开头为空格
且sm3(base64decode(base64decode(code))[:3])=code[-64:]
考虑到base64以及摩斯解密时的容错性(因为解码时字符串结尾需要有64位sm3加密后的字符串)
我们构造base64解密后为"///"的字符串,摩斯解密时其会自动生成三个空格(也可以一个"/",不过sm3时需要补\x00以及考虑base64时及时截断问题,为了简化,直接"///"即可),满足最后的迷宫算法的需求
最终序列号:
THk4dgo9aa4f168af9fcb372825d2e817379ab6ad4a7da973a38c44a0ec56a788dfb89b
网友评论