2016kxctf第7题

2016kxctf第7题

Posted by gxkyrftx on January 9, 2019

0.前言

本题是一个通过seh执行关键代码的题目,关于seh,参考 http://gxkyrftx.xyz/2019/01/07/SEH%E5%AD%A6%E4%B9%A0/ 其实类似于前面提到的tls,都差不多,大概理解即可,seh的重点是分析其跳转到的核心代码,分析其调用的程序。

1.流程分析

ida动态调试

1.1 主函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // ecx
  int v4; // eax
  int v5; // ecx
  int v6; // ST00_4
  int *v7; // eax
  int v9; // [esp+10h] [ebp-7Ch]
  char v10; // [esp+14h] [ebp-78h]
  char v11; // [esp+64h] [ebp-28h]
  int v12; // [esp+88h] [ebp-4h]

  InitializeCriticalSection(&CriticalSection);  // 初始化,为线程分配临界锁

  sehcode_init(&v10);                           // seh代码初始化,v8是空的

  v12 = 0;
  seh_settrigger(&v10, v9);                     // 设置触发的事件,v8是‘0’,v7是seh安全检查,线程相关

  seh_settreatfuncation(&v10, v3);              // 此函数里有反调试,两个线程需要pacth,一个线程check输入sn

  sehcode_run_1(&v10);                          // 此处直接开始创建线程,一个输入,两个反调试

  if ( (unsigned __int8)check2() )              // 对输入的sn第二次检测

  {
    v4 = dec_str((int)&v11);                    // 把字符串移出来

    LOBYTE(v12) = 1;
    v6 = v5;
    v7 = print_str(v5, v4);                     // 打印验证之后的字符串

    print_enter(v7, v6);
    LOBYTE(v12) = 0;
    free_seh(&v11);
  }
  j___fgetchar();
  j___fgetchar();
  v12 = -1;
  sub_402E9F(&v10);
  return 0;
}

1.2 sub_40266B

即sehcode_init,这个函数里面四层嵌套,但是什么事情也没做,最后只是返回了空,故判断应该为初始化。

int __thiscall sub_40266B(_DWORD *this)
{
  int v1; // edx

  int v2; // edx

  sub_403194(this);
  sub_403194((_DWORD *)(v1 + 40));
  return v2;
}

1.3 seh_settrigger

这个函数主要是设置触发的事件,v8是‘0’,v7是seh安全检查,与线程相关。与sn无关,不再展开分析。

1.4 seh_settreatfuncation

void *__thiscall seh_settreatfuncation(char *this, int a1)
{
  char *v2; // esi

  void *v3; // eax

  char v5; // [esp+4h] [ebp-Ch]

  DWORD (*v6)(); // [esp+Ch] [ebp-4h]


  v2 = this;
  v6 = three_thread;                            // 创建三个线程,只有第一个执行

  v3 = ret_arg1((int)&v5, &v6);
  return sub_4046FA(v2 + 40, v3);
}

1.4.1 three_thread

其中两个反调试,已经patch过了,具体patch的方法,进入函数,修改汇编代码为ret,函数会变为locret。再次动态调试时,就不会自动断下来了。

DWORD three_thread()
{
  hHandle = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)StartAddress, 0, 0, 0);
  dword_42E1E0 = CreateThread(0, 0, locret_402E51, 0, 0, 0);// 反调试

  hThread = CreateThread(0, 0, locret_402FFA, 0, 0, 0);// 反调试

  return WaitForSingleObject(hHandle, 0xFFFFFFFF);// 等待输入验证码

}

1.4.2 StartAddress

这个函数,主要就是生成字符,然后打印字符串,随后对输入的字符串,进行初步检查。在check函数。

int __stdcall StartAddress()
{
  int v0; // eax
  int v1; // ecx
  int v2; // ST04_4
  int *v3; // eax
  int v4; // ecx
  char v6; // [esp+10h] [ebp-78h]
  char v7; // [esp+60h] [ebp-28h]
  int v8; // [esp+84h] [ebp-4h]

  v0 = dec_str((int)&v7);
  v8 = 0;
  v2 = v1;
  v3 = print_str(v1, v0);
  print_enter(v3, v2);
  v8 = -1;
  free_seh(&v7);
  init_str(&unk_42CB50, 0xEu);
  sehcode_init(&v6);
  v8 = 1;
  sub_403D75(&v6, (unsigned int)check);
  sub_403D97((char)ExitProcess, v4);
  sehcode_run_1(&v6);
  EnterCriticalSection(&CriticalSection);
  hHandle = 0;
  LeaveCriticalSection(&CriticalSection);
  v8 = -1;
  return sub_402E9F(&v6);
}

1.4.3 check

其中rdtsc_antidbg函数的内容是rdtsc函数,它的作用也是反调试,通过判断程序在cpu执行的事件来判断,如果执行时间过长,则可证明是在调试,因为是指令的执行很快,两条指令之间几乎没有时间差。 check2函数,是对输入sn的一个验证。

int check()
{
  unsigned __int64 v0; // kr00_8
  char v2; // [esp+10h] [ebp-28h]
  int v3; // [esp+34h] [ebp-4h]

  sub_4033C3(&v2);
  v3 = 0;
  sub_403DC4();
  v0 = rdtsc_antidbg();
  sub_40284B((int)&v2);
  if ( rdtsc_antidbg() - v0 > 10000000000i64 || !(unsigned __int8)check2() )
    ExitProcess(0);
  v3 = -1;
  return free_seh(&v2);
}

1.4.3.1 sub_40284B

对sn做出限制,要求输入sn长度为85,并且第10位为F。

v1 = sub_403251((void *)a1) == 85 && *(_BYTE *)sub_40325E((void *)a1, 9) == 'F'

1.4.3.2 check2

返回值由5个条件决定,由于byte_13BEE70经过动态调试后的值为1

1

所以当这四个函数都返回为1时,check2为true,任何一个不满足,都为0。

BOOL check2()
{
  return sub_4029D3()
      && sub_402930()
      && (unsigned __int8)check_constant_variable()
      && (unsigned __int8)check_xor()
      && byte_13BEE70;                          // 满足5个条件
}

1.4.3.2.1 sub_4029D3

这个函数主要检查了表,没有实质性的数据变换,不深入讨论了

1.4.3.2.2 sub_402930

通过动态调试可以看到0x3F2是数组初始化的个数,只要一个条件不满足,返回为0,都不满足则返回为1,也就是说,数组个数要为0x3F2个,序列号转换为数字为0x3C9=969,循环0x3C9次。

char sub_402930()
{
  char v0; // cl

  if ( DWORD2(xmmword_42E210) != 0x3F2
    || HIDWORD(xmmword_42E210) != 0x3C9
    || (v0 = 1, (_DWORD)xmmword_42E210 - DWORD1(xmmword_42E210) != 1) )
  {
    v0 = 0;
  }
  return v0;
}

1.4.3.2.3 check_constant_variable

输入的sn成为两个变量,然后与原来的常量相加,验证是不是相等。

BOOL check_constant_variable()
{
  return constant_1 + variable2 - variable1 - constant_2 == 0;
}

1.4.3.2.4 check_xor

异或校验

BOOL check_xor()
{
  return constant_2 == (constant_1 ^ variable2 ^ variable1);
}

以上是关于sn的所有限制条件,其他的函数不在深入分析,可以通过动态调试看结果,或者追踪栈和寄存器的值的变化看出功能。

2.破解思路

2.1 sn要求:

1.sn为85个字符,转化为数字之后和为969,第10个字符为‘F’
2.预置的常量与输入sn满足constant_1 + variable2 - variable1 - constant_2 == 0

2.2 两种破解思路:

1.直接把计算常量的算法拿出来,计算sn,肯定是满足的。
2.穷举满足条件的解。

两种思路都需要计算常量的算法。算法如下:

__int64 __thiscall dp_array(_DWORD *this, int a2, int a3, int a4)
{
  __int64 result; // rax
  int v5; // edx
  unsigned __int64 v6; // kr00_8
  __int64 v7; // rax
  _DWORD *v8; // [esp+8h] [ebp-8h]
  int v9; // [esp+Ch] [ebp-4h]

  v8 = this;
  if ( a2 > a3 )                                // a2>a3返回零
    return 0i64;
  v5 = a3 + 0x3F2 * a2;
  v9 = v5;
  if ( *(_QWORD *)(a4 + 8 * v5) != -1i64 )
    return *(_QWORD *)(a4 + 8 * v5);
  if ( a2 == a3 )                               // a2=a3返回本身
  {
    LODWORD(result) = *(_DWORD *)(this[1] + 4 * a2);
    *(_QWORD *)(a4 + 8 * HIDWORD(result)) = (unsigned int)result;
    HIDWORD(result) = 0;
  }
  else
  {
    v6 = dp_array(this, a2 + 1, a3, a4);
    if ( dp_array(v8, a2, a3 - 1, a4) >= v6 )
      v7 = dp_array(v8, a2 + 1, a3, a4);
    else
      v7 = dp_array(v8, a2, a3 - 1, a4);
    result = sum(v8, a2, a3, (int)&unk_7C8660 + (signed int)v8) - v7;
    *(_QWORD *)(a4 + 8 * v9) = result;
  }
  return result;
}

其中的sum函数

__int64 __thiscall sum(_DWORD *this, int a2, int a3, int a4)
{
  __int64 result; // rax
  int v5; // ebx

  if ( a2 > a3 )                                // a2>a3返回0
    return 0i64;
  v5 = a3 + 0x3F2 * a2;
  if ( *(_QWORD *)(a4 + 8 * v5) != -1i64 )      // a[i][j]不为-1,返回a[i][j]
    return *(_QWORD *)(a4 + 8 * v5);
  if ( a2 == a3 )
    result = *(unsigned int *)(this[1] + 4 * a2);
  else
    result = *(unsigned int *)(this[1] + 4 * a2) + sum(this, a2 + 1, a3, a4);
  *(_QWORD *)(a4 + 8 * v5) = result;
  return result;
}

该算法描述了这样的一个问题,两个人在一个很大的数组中取数,只能从两头开始取,每个人都很贪婪,他当前取出的数,能够使对手以后的取数之和最小,是一个博弈问题,需要动态规划解决。

2.3 编码方式

构造大数组 部分数组内容如图:

2

一人取一次,只能在两头取,左边取的记为1,右边取的记为0,再记一个数组。使这两个人的取数和,a应该为0x000000FEF635D82A,b为0x000000FAD883E964,动态调试得到如图所示

3

把这个1010个01序列编码,编码规则:若出现两个相连的1或者0,证明换了一个人,数组截断。 最后要求得到的没有相连的01串的01串,总共85个。 然后以他们的长度为索引,对应 “0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz”的序号,例如“0101”,长度为4,对应字符‘4’.这里参考的了 https://bbs.pediy.com/thread-213966.htm 中的一个解。

    flag[0x3F2] = 01011010101010101010101010101010101010101010101010101010110101010101010101010101010101010101010101010101010101010101010101010101010101010110101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010110101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101001010101010101010101010101010101010101010101010010101010101010101010101010101010101010101010101010101010101010101010100101010101

经过手动分割之后的到sn 941rPYOWMF3C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C2C69BKAKAKAKAKAKAKAKAKAKAKAKAKK8AE614 成功通过验证:

4


本文访问量: