去年没参加
今年又赶上考试月
只能先整理之前的题目了
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;
  }
以及解码用到的数据表:
图片.png
显然是一个摩斯密码解密,且以空格为分隔符
且"空格"前有"/"视为转义,解密为空格
单一"/"会被解密为空格
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
          






网友评论