首页
社区
课程
招聘
[原创]看雪CTF.TSRC 2018 团队赛 第六题 追凶者也
2018-12-11 23:12 2023

[原创]看雪CTF.TSRC 2018 团队赛 第六题 追凶者也

2018-12-11 23:12
2023

看雪CTF.TSRC 2018 团队赛 第六题 追凶者也

朦胧初识

先试运行了下,果然win10运行不正常,没有退出,CPU立马上来了。

 

拖进ida,ida自动定位到了WinMain函数。但是,函数只调用了一个空函数。

int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  sub_401280();
  return 0;
}

void sub_401280()
{
  ;
}

按图索骥

事出反常必有妖。因为用户函数比较少,很容易就发现程序使用的tls回调函数,从exports中亦能看出。查看回调函数,其中有两个调用,和一个线程函数。

HANDLE __stdcall TlsCallback_0(int a1, int a2, int a3)
{
  HANDLE result; // eax
  int Parameter; // [esp+Ch] [ebp-8h]
  int v5; // [esp+10h] [ebp-4h]

  result = (HANDLE)0xCCCCCCCC;
  Parameter = 0xCCCCCCCC;
  v5 = 0xCCCCCCCC;
  if ( a2 == 1 )
  {
    smc();
    InitializeCriticalSection(&CriticalSection);
    hook_GetDlgItemTextA();
    result = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, &Parameter, 0, 0);
  }
  return result;
}

先看smc函数,明显是修改sub_401280偏移4字节处的5个字节数据,第一个字节修改为E9,对应jmp指令,后面4字节数据当然是跳转偏移,所以是跳到off_414014指向的函数处。

BOOL smc()
{
  signed int i; // [esp+0h] [ebp-18h]
  char v2[12]; // [esp+Ch] [ebp-Ch]

  *(_DWORD *)&v2[4] = 0xCCCCCCCC;
  *(_DWORD *)&v2[8] = 0xCCCCCCCC;
  v2[0] = 0xE9u;
  *(_DWORD *)&v2[1] = off_414014 - (off_414018 + 4) - 5;
  j_VirtualProtect(off_414018);
  for ( i = 0; i < 5; ++i )
    off_414018[i + 4] = v2[i];
  return j_VirtualProtect_re(off_414018);
}



.data:00414014 20 12 40 00             off_414014      dd offset set_diag_proc 
.data:00414018 80 12 40 00             off_414018      dd offset sub_401280

顺着往下走,看看主函数到底执行了什么。原来指定了窗口的消息回调函数DialogFunc,ID为1002的控件被点击时的响应函数为sub_401040

oid __noreturn set_diag_proc()
{
  HINSTANCE hInstance; // ST18_4

  hInstance = GetModuleHandleW(0);
  DialogBoxParamW(hInstance, (LPCWSTR)0x65, 0, DialogFunc, 0);
  exit(0);
}


BOOL __stdcall DialogFunc(HWND hWnd, UINT a2, WPARAM a3, LPARAM a4)
{
  if ( a2 == WM_CLOSE )
  {
    DestroyWindow(hWnd);
  }
  else if ( a2 == WM_COMMAND && a3 == 1002 )
  {
    sub_401040(hWnd);
  }
  return 0;
}


int __cdecl sub_401040(HWND hDlg)
{
  void *v1; // ST08_4
  signed int i; // [esp+Ch] [ebp-24h]
  signed int v4; // [esp+10h] [ebp-20h]
  char String[20]; // [esp+18h] [ebp-18h]

  strcpy(Text, "try again!");
  strcpy(Caption, "fail");
  String[0] = 0;
  *(_DWORD *)&String[1] = 0;
  *(_DWORD *)&String[5] = 0;
  *(_DWORD *)&String[9] = 0;
  *(_DWORD *)&String[13] = 0;
  *(_WORD *)&String[17] = 0;
  String[19] = 0;
  GetDlgItemTextA(hDlg, 1001, String, 20);
  v4 = 0;
  for ( i = 0; i < 20; ++i )
    v4 += String[i];
  if ( v4 > 0 && v4 < 4132 )
  {
    v1 = malloc(v4);
    sub_401020();
    j___free_base(v1);
  }
  return MessageBoxA(0, Text, Caption, 0);
}

sub_401040除了获取了文本框的输入,最后出了个弹窗,似乎什么都没做。此条线就到此为止了。回到tls回调函数,还有一个函数调用的一个线程函数没有分析。go on,看hook_GetDlgItemTextA函数。

BOOL hook_GetDlgItemTextA()
{
  char *v0; // edx
  signed int j; // [esp+0h] [ebp-Ch]
  signed int i; // [esp+8h] [ebp-4h]

  get_api_GetDlgItemTextA();
  g_GetDlgItemTextA = v0;
  j_VirtualProtect(v0);
  for ( i = 0; i < 5; ++i )
    g_GetDlgItemTextA_bak[i] = g_GetDlgItemTextA[i + 32];
  *(_DWORD *)&byte_414028[1] = (char *)sub_401A10 - (char *)(g_GetDlgItemTextA + 32) - 5;
  for ( j = 0; j < 5; ++j )
    g_GetDlgItemTextA[j + 32] = byte_414028[j];
  return j_VirtualProtect_re(g_GetDlgItemTextA);
}

.data:00414028 E9                      byte_414028     db 0E9h

先是通过get_api_GetDlgItemTextA函数获取GetDlgItemTextA的地址,然后修改GetDlgItemTextA函数偏移32字节处的5字节数据。与上次的smc类似,第一个字节修改为E9,后4字节为偏移,是jmpsub_401A10的偏移。下面列出了GetDlgItemTextA的代码,就是将754A6B56处本来要跳到返回的代码改成了跳到sub_401A10,也就是hook了GetDlgItemTextA的返回。

754A6B36 >  8BFF            mov edi,edi
754A6B38    55              push ebp
754A6B39    8BEC            mov ebp,esp
754A6B3B    FF75 0C         push dword ptr ss:[ebp+0xC]
754A6B3E    FF75 08         push dword ptr ss:[ebp+0x8]
754A6B41    E8 7486FCFF     call user32.GetDlgItem
754A6B46    85C0            test eax,eax                             
754A6B48    74 0E           je short user32.754A6B58
754A6B4A    FF75 14         push dword ptr ss:[ebp+0x14]
754A6B4D    FF75 10         push dword ptr ss:[ebp+0x10]
754A6B50    50              push eax                                 
754A6B51    E8 D394FAFF     call user32.GetWindowTextA
754A6B56    EB 0E           jmp short user32.754A6B66
754A6B58    837D 14 00      cmp dword ptr ss:[ebp+0x14],0x0
754A6B5C    74 06           je short user32.754A6B64
754A6B5E    8B45 10         mov eax,dword ptr ss:[ebp+0x10]
754A6B61    C600 00         mov byte ptr ds:[eax],0x0
754A6B64    33C0            xor eax,eax                              
754A6B66    5D              pop ebp
754A6B67    C2 1000         retn 0x10

再顺着往下走,看sub_401A10

int __usercall sub_401A10@<eax>(int a1@<ecx>, int a2@<ebp>, int a3@<esi>)
{
  unsigned int v3; // et0
  int v4; // eax
  _BYTE *v5; // ecx
  unsigned int v7; // [esp-24h] [ebp-24h]
  int v8; // [esp-18h] [ebp-18h]

  v8 = a1;
  v3 = __readeflags();
  v7 = v3;
  dword_414800 = a3;
  g_max_length = *(_DWORD *)(a2 + 20);
  g_input_copy = malloc(g_max_length);
  dword_4147F4 = a2 + 16;
  memmove(g_input_copy, *(const void **)(a2 + 16), g_max_length);
  *(_DWORD *)(a2 - 16) = g_input_copy;
  *(_DWORD *)(a2 - 20) = *(_DWORD *)(a2 - 16) + 1;
  do
    *(_BYTE *)(a2 - 21) = *(_BYTE *)(*(_DWORD *)(a2 - 16))++;
  while ( *(_BYTE *)(a2 - 21) );
  *(_DWORD *)(a2 - 28) = *(_DWORD *)(a2 - 16) - *(_DWORD *)(a2 - 20);// length-1
  if ( go_check((int)g_input_copy, *(_DWORD *)(a2 - 28)) )
  {
    v4 = len((int)g_input_copy);
    if ( hash((char *)g_input_copy, v4) == 0x5634D252 )
    {
      v5 = off_414030;
      *(_WORD *)off_414030 = 0;
      v5[2] = 0;
      for ( *(_WORD *)(a2 - 4) = 0; *(signed __int16 *)(a2 - 4) < 8; ++*(_WORD *)(a2 - 4) )
        *((char *)off_414030 - *(signed __int16 *)(a2 - 4)) = (39 - *(unsigned __int16 *)(a2 - 4)) ^ *((_BYTE *)&dword_41401C + 7 - *(signed __int16 *)(a2 - 4));
      *(_WORD *)off_414034 = 0;
      for ( *(_WORD *)(a2 - 8) = 0; *(signed __int16 *)(a2 - 8) < 3; ++*(_WORD *)(a2 - 8) )
        *((char *)off_414034 - *(signed __int16 *)(a2 - 8)) = (34 - *(unsigned __int16 *)(a2 - 8)) ^ *((_BYTE *)&dword_414024 + 2 - *(signed __int16 *)(a2 - 8));
    }
  }
  j___free_base(g_input_copy);
  dword_4147F8 = (int (__thiscall *)(_DWORD))(g_GetDlgItemTextA + 32);
  j_VirtualProtect(g_GetDlgItemTextA);
  for ( *(_DWORD *)(a2 - 12) = 0; *(_DWORD *)(a2 - 12) < 5; ++*(_DWORD *)(a2 - 12) )
    g_GetDlgItemTextA[*(_DWORD *)(a2 - 12) + 32] = g_GetDlgItemTextA_bak[*(_DWORD *)(a2 - 12)];
  j_VirtualProtect_re(g_GetDlgItemTextA);
  g_flag = 1;
  __writeeflags(v7);
  return dword_4147F8(v8);
}

咋一看,有点费事,动态跟下就出来了。基本过程是:

  1. 将输入复制到一个全局变量中
  2. 计算输入长度
  3. 如果过了go_checkhash的检查,则修改错误信息的数据为正确信息,直接影响到弹窗的内容
  4. 恢复对GetDlgItemTextA的修改,并全局标志位。
  5. 返回到GetDlgItemTextA处继续执行

所以检验点就出来了。
那线程函数是干嘛用的呢?

void __stdcall StartAddress(LPVOID lpThreadParameter)
{
  while ( 1 )
  {
    EnterCriticalSection(&CriticalSection);
    if ( g_flag == 1 )
    {
      hook_GetDlgItemTextA();
      g_flag = 0;
    }
    LeaveCriticalSection(&CriticalSection);
  }
}

原来是为了下次按键点击时作准备,继续hookGetDlgItemTextA

玩乐求解

这里的校验有两个,下面的hash函数其实在取api的时候用过,按道理讲是不可逆的,应该实际上的check只有一个,第一个满足条件了,第二个也应该就满足了。其实还有个隐含条件,就是输入应该是小于20字节的,可以从取输入的地方可以看出来,那第二个条件也可以说是防止多解。看go_check

bool __cdecl go_check(int a1, int a2)
{
  g_table_4147D0[0][0] = 4;
  g_table_4147D0[0][1] = 1;
  g_table_4147D0[0][2] = 3;
  g_table_4147D0[1][0] = 7;
  g_table_4147D0[1][1] = 2;
  g_table_4147D0[1][2] = 5;
  g_table_4147D0[2][0] = 8;
  g_table_4147D0[2][1] = 6;
  g_table_4147D0[2][2] = 0;
  return check((char *)a1, a2);
}

bool __cdecl check(char *a1, int a2)
{
  int i; // [esp+0h] [ebp-Ch]
  int v4; // [esp+8h] [ebp-4h]

  v4 = 0xCCCCCCCC;
  if ( a2 % 2 )                                 // length %2 == 0
    return 0;
  for ( i = 0; i < a2; i += 2 )
  {
    if ( a1[i] == 'w' )
      v4 = 0;
    if ( a1[i] == 'd' )
      v4 = 1;
    if ( a1[i] == 's' )
      v4 = 2;
    if ( a1[i] == 'a' )
      v4 = 3;
    if ( !move(v4, a1[i + 1] - 0x30) )
      return 0;
  }
  return g_table_4147D0[0][0] == 1
      && g_table_4147D0[0][1] == 2
      && g_table_4147D0[0][2] == 3
      && g_table_4147D0[1][0] == 4
      && g_table_4147D0[1][1] == 5
      && g_table_4147D0[1][2] == 6
      && g_table_4147D0[2][0] == 7
      && g_table_4147D0[2][1] == 8
      && !g_table_4147D0[2][2];
}

char __cdecl move(int op, int num)
{
  char result; // al
  signed int i; // [esp+8h] [ebp-8h]
  signed int v4; // [esp+Ch] [ebp-4h]

  if ( !num )
    return 0;
  v4 = 0;
LABEL_4:
  if ( v4 >= 3 )
    return 0;
  for ( i = 0; ; ++i )
  {
    if ( i >= 3 )
    {
      ++v4;
      goto LABEL_4;
    }
    if ( g_table_4147D0[v4][i] == num )
      break;
LABEL_6:
    ;
  }
  switch ( op )
  {
    case 0:                                     // w
      if ( v4 )
      {
        if ( g_table_4147D0[v4 - 1][i] )
        {
          result = 0;
        }
        else
        {
          g_table_4147D0[v4 - 1][i] = g_table_4147D0[v4][i];
          g_table_4147D0[v4][i] = 0;
          result = 1;
        }
      }
      else
      {
        result = 0;
      }
      break;
    case 1:                                     // d
      if ( i == 2 )
      {
        result = 0;
      }
      else if ( byte_4147D1[3 * v4 + i] )
      {
        result = 0;
      }
      else
      {
        byte_4147D1[3 * v4 + i] = g_table_4147D0[v4][i];
        g_table_4147D0[v4][i] = 0;
        result = 1;
      }
      break;
    case 2:                                     // s
      if ( v4 == 2 )
      {
        result = 0;
      }
      else if ( g_table_4147D0[v4 + 1][i] )
      {
        result = 0;
      }
      else
      {
        g_table_4147D0[v4 + 1][i] = g_table_4147D0[v4][i];
        g_table_4147D0[v4][i] = 0;
        result = 1;
      }
      break;                                    // a
    case 3:
      if ( i )
      {
        if ( byte_4147CF[3 * v4 + i] )
        {
          result = 0;
        }
        else
        {
          byte_4147CF[3 * v4 + i] = g_table_4147D0[v4][i];
          g_table_4147D0[v4][i] = 0;
          result = 1;
        }
      }
      else
      {
        result = 0;
      }
      break;
    default:
      goto LABEL_6;
  }
  return result;
}

go_check函数先初始化了一个33的全局数组,再进入check函数。check先检查输入长度为偶数,再以2字节步长遍历输入,进入move对33全局数组进行操作,最后检查全局数组值为1-8,最后为0。

 

细看move函数,以遍历的输入2字节的第1字节为控制方向,第2字节为控制对象,进行上下左右的移动,原位置0。仔细一想,这不就是拼图游戏,0为空位,考虑到输入长度的隐含限制,应该是最少步数完成结果,具体步骤如下:

w-上  s-下 a-左 d-右
4 1 3
7 2 5
8 6 0

d6:
4 1 3
7 2 5
8 0 6
d8:
4 1 3
7 2 5
0 8 6
s7:
4 1 3
0 2 5
7 8 6
s4:
0 1 3
4 2 5
7 8 6
a1:
1 0 3
4 2 5
7 8 6
w2:
1 2 3
4 0 5
7 8 6
a5:
1 2 3
4 5 0
7 8 6
w6:
1 2 3
4 5 6
7 8 0

所以最终flag为:d6d8s7s4a1w2a5w6
输入试试,果然成功。

无聊之举

我十分好奇,为什么在win10上不能运行。原来秘密藏在api的获取过程中,代码如下,将就用伪代码看看吧。

int get_api_GetDlgItemTextA()
{
  int v0; // edx

  get_user32_base(0x3BD696F4);
  return get_GetDlgItemTextA_addr(v0, 0x925DF53F);
}


int __stdcall get_user32_base(int a1)
{
  int v1; // ecx
  int v2; // esi
  int v3; // ST08_4
  char *v4; // ST04_4
  int v5; // eax
  int result; // eax
  int v7; // edx

  v1 = a1;
  v2 = (int)NtCurrentPeb()->Ldr->InInitializationOrderModuleList.Flink;
  do
  {
    v2 = *(_DWORD *)v2;
    v3 = v1;
    v4 = *(char **)(v2 + 0x18);
    v5 = unicode_len(*(_DWORD *)(v2 + 0x18));
    result = hash(v4, 2 * v5);
    v1 = v3;
  }
  while ( result != v3 );
  v7 = *(_DWORD *)(v2 + 8);
  return result
}



int __stdcall get_GetDlgItemTextA_addr(int a1, int a2)
{
  _DWORD *v2; // esi
  int i; // eax
  char *v4; // esi
  int v5; // eax
  int v7; // [esp-8h] [ebp-14h]
  int l_AddrofOrd; // [esp+0h] [ebp-Ch]
  int l_AddrofName; // [esp+4h] [ebp-8h]
  int l_AddrofFunc; // [esp+8h] [ebp-4h]

  v2 = (_DWORD *)(a1 + *(_DWORD *)(a1 + *(_DWORD *)(a1 + 0x3C) + 0x78));// ExportDir
  l_AddrofFunc = v2[7];
  l_AddrofName = v2[8];
  l_AddrofOrd = v2[9];
  for ( i = 0; ; i = v7 + 1 )
  {
    v4 = (char *)(a1 + *(_DWORD *)(a1 + l_AddrofName + 4 * i));
    v7 = i;
    v5 = len(a1 + *(_DWORD *)(a1 + l_AddrofName + 4 * i));
    if ( hash(v4, v5) == a2 )
      break;
  }
  return a1 + *(_DWORD *)(a1 + l_AddrofFunc + 4 * *(unsigned __int16 *)(a1 + l_AddrofOrd + 2 * v7));
}


int __stdcall hash(char *a1, int a2)
{
  int i; // [esp+0h] [ebp-8h]
  int v4; // [esp+4h] [ebp-4h]

  v4 = a2;
  for ( i = 0; i < a2; ++i )
    v4 = a1[i] ^ (v4 >> 28) ^ 16 * v4;
  return v4;
}

先取dll基址,具体实现是:通过PEB找到PEB_LDR_DATA指针,再找到InInitializationOrderModuleList双向链表,遍历dll的全路径名称,进行hash计算与预设hash值比对。这里问题就来了,win10和win7 64位环境中user32.dll的全路径名称并不一致,就算基本路径一致,大小写也不一样。程序中预设的全路径名称为C:\Windows\syswow64\USER32.dll,所以即使是在win7 32位系统中也是运行不正常的,会一直遍历双向链表。要想通用,只能取BaseDllName,并且hash前统一大小写。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回