首页
社区
课程
招聘
[原创]kctf2023 第六题 至暗时刻
2023-9-15 11:56 8586

[原创]kctf2023 第六题 至暗时刻

2023-9-15 11:56
8586

windows10 或者 windows7 的运行环境,ida打开发现程序逻辑比较少(以下结果基于ida7.7)。

过反调试

由于函数较少,扫了一眼看到了两个TLS回调函数,发现是反调试,逐一过掉(其实后续代码里还有个反调试,后文会提到)。

  1. IsDebuggerPresent,把跳转patch掉就行了,改为jmp,不管结果如何都不进入if语句
    图片描述
    图片描述
  2. NtQueryInformationProcess,0x7功能码查调试器信息(这个函数有好几个功能码都可以查调试器),一样的方法,if那里的跳转改为jmp就不会进入if语句了
    图片描述
    图片描述

程序分析

main函数比较简单,就启动了一个线程,主要来看线程里面的逻辑sub_7FF6D6431630;

1
2
3
v3 = operator new(8ui64);
*v3 = sub_7FF6D6431630;
*ThrdAddr = beginthreadex(0i64, 0, StartAddress, v3, 0, &ThrdAddr[2]);

初始化

将一些内置字符串拷贝到变量中,后续用到的时候会将字符串使用异或来解密,就能看到明文,然后打印提示语句,分配空间接受输入的值。
图片描述

字符串拼接

将我们的输入,头尾都拼接上一些字符串:

1
2
3
4
5
6
# 头部拼接上这串字符串:
3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6
# input尾部拼接上这串字符串:
677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280
# 然后在最头部再次拼接字符串
kctf

图片描述

shellcode填充

后续执行的都是一些上文中的字符串异或解密操作,然后就会来到关键函数

1
2
3
ModuleHandleA = GetModuleHandleA(v22);
off_7FF61D229A48 = GetProcAddress(ModuleHandleA, v21);
if ( !off_7FF61D229A48 || (v31 = Source, sub_7FF61D221450(CurrentProcess, (Source + 500))) ){...}

我们进入sub_7FF61D221450函数会发现里面还有个反调试CheckRemoteDebuggerPresent,一样的方法,跳转的位置改为jmp即可
图片描述
接下来我们可以看到主要逻辑是三个函数的调用(sub_7FF61D223529,sub_7FF61D222E4E,sub_7FF61D222A8E),这三个函数都很类似,大概都是长下面这样的:
图片描述
说明核心逻辑在sub_7FF61D222A10函数里面,这个函数跟进去之后会发现其实就是函数指针处理+syscall系统调用的过程,我们在syscall的地方下断点,根据syscall函数原型查看寄存器就可以看到他的参数是什么了。
可以看出来他创建了句柄,然后一个大的for循环将从0x7FF61D228050开始长度为0x92B的内存拿去调用了kernel32_RtlFillMemory函数,这个函数的作用是使用指定数据填充内存块,填充的目的地址在r8寄存器存着,是0x261CD0301F4
图片描述
然后我们继续往后执行,来到一个创建线程的位置,这些函数都是通过上述系统调用的形式执行的,这里创建了新线程,如果继续往后执行他就会比较字符串是110还是120,然后打印换行符,程序结束。这里可以推测这个线程就是执行刚才填充的那段内存的地方。
图片描述

shellcode分析

据上述推测新起来的线程就是执行shellcode的地方,并且返回结果应该是110和120有关的值,这里有两种做法:
第一个是将0x7FF61D228050位置的shellcode给dump下来,然后扔到ida中静态分析,我尝试了一下,虽然代码量不大,但是还是有点复杂的,而且需要自己修复堆栈,比较麻烦。
第二种做法就是调试上述创建的那个线程了:
在sub_7FF61D222A8E执行系统调用之前,线程已经准备好了但是没有执行,这个时候我们可以在ida的线程模块中双击多创建的线程,就可以进入到该线程的领空。
图片描述
他停在这里等我们执行,我们直接按g跳转到0x261CD0301F4(这是上述分析中shellcode被填充的地址),可以确认与0x7FF61D228050的内容是一致的,然后我们使用ida快捷键P创建函数,在函数里下个断点,直接f9执行,执行到断点处会发现堆栈平衡了,然后就可以f5大法了。

初始化

通过一个sub_261CD030563函数找到了一系列的函数指针(名字是我重命名的),调试之后发现v17和v19就是我们输入的字符串被拼接之后的结果,从上述字符串判断中可以得出,我们需要程序返回110,说明unk_261CD03062F和unk_261CD030AA3要返回true:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
__int64 __fastcall sub_261CD03020A(__int64 a1)
{
  do
  {
    v2 = *v1;
    if ( *v1 == 23 )
      v2 = 0;
    *v1++ = v2;
    --a1;
  }
  while ( a1 );
  v26 = 0i64;
  v25 = v1;
  kernel32_CreateToolhelp32Snapshot = sub_261CD030563(-124919994);
  kernel32_OpenProcess = sub_261CD030563(-49588825);
  kernel32_VirtualQueryEx = sub_261CD030563(37938943);
  kernel32_Process32First = sub_261CD030563(1060402837);
  kernel32_Process32Next = sub_261CD030563(-1813961927);
  kernel32_CloseHandle = sub_261CD030563(480663025);
  kernel32_GetCurrentProcessId = sub_261CD030563(55981281);
  v7 = 0i64;
  v23 = 568;
  v8 = 0;
  v9 = kernel32_CreateToolhelp32Snapshot(2i64, 0i64);
  v10 = v9;
  if ( v9 == -1 )
    return 0xFFFFFFFFi64;
  v12 = kernel32_Process32First(v9, &v23);
  v13 = kernel32_Process32Next;
  v14 = kernel32_OpenProcess;
  while ( v12 )
  {
    if ( v24 == kernel32_GetCurrentProcessId() )
    {
      v7 = v14(0x2000000i64, 0i64);
      if ( v7 )
      {
        v15 = 0i64;
        while ( 1 )
        {
          do
          {
            if ( !kernel32_VirtualQueryEx(v7, v15, &v19, 48i64) )
            {
              v13 = kernel32_Process32Next;
              v14 = kernel32_OpenProcess;
              goto LABEL_23;
            }
            v15 = v19 + v21;
          }
          while ( v22 != 4096 || v20 != 64 );
          v16 = kernel32_GetCurrentProcessId();
          v17 = v19;
          if ( v24 == v16 )
            v8 = (unk_261CD03062F)(*v19);
          if ( v8 )
            break;
          *v17 = 'm';
          v17[1] = 'j';
          v17[2] = ')';
          v17[3] = '\0';
          v17[67] = '1';
          v17[68] = '2';
          v17[69] = '0';
        }
        v18 = v17 + 4;
        if ( (unk_261CD030AA3)(v17 + 4) )
        {
          *(v18 - 4) = 'i';
          *(v18 - 3) = 'o';
          *(v18 - 2) = ' ';
          *(v18 - 1) = 0;
          v18[63] = '1';
          v18[64] = '1';
        }
        else
        {
          *(v18 - 4) = 'm';
          *(v18 - 3) = 'j';
          *(v18 - 2) = ')';
          *(v18 - 1) = '\0';
          v18[63] = '1';
          v18[64] = '2';
        }
        v18[65] = '0';
        break;
      }
    }
LABEL_23:
    v12 = v13(v10, &v23);
  }
  kernel32_CloseHandle(v10);
  return (kernel32_CloseHandle)(v7);
}

unk_261CD03062F函数

这个函数的逻辑很简单,就是判断开头是不是kctf,而kctf是程序帮我们拼接的,所以恒成立

1
2
3
4
bool __fastcall sub_261CD03062F(int a1)
{
  return a1 == 'ftck';
}

unk_261CD030AA3函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
char __fastcall sub_261CD030AA3(__int64 a1)
{
  unsigned int v2; // ebx
  int v3; // ebx
  int v4; // edi
 
  v2 = 0;
  while ( (unk_261CD03093B)(a1, v2) && (unk_261CD0309A7)(a1, v2) )
  {
    if ( ++v2 >= 9 )
    {
      v3 = 0;
LABEL_6:
      v4 = 0;
      while ( (unk_261CD030A13)(a1, v3, v4) )
      {
        v4 += 3;
        if ( v4 >= 9 )
        {
          v3 += 3;
          if ( v3 < 9 )
            goto LABEL_6;
          return 1;
        }
      }
      return 0;
    }
  }
  return 0;
}

这段主要调用了三个函数,每个函数需要返回true才行。
其中unk_261CD03093B的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
char __fastcall sub_261CD03093B(__int64 a1, unsigned int a2)
{
  int v2; // ebx
  signed int v5; // eax
  __int128 v7[2]; // [rsp+20h] [rbp-38h] BYREF
  int v8; // [rsp+40h] [rbp-18h]
 
  memset(v7, 0, sizeof(v7));
  v8 = 0;
  v2 = 0;
  while ( 1 )
  {
    v5 = (unk_261CD03063B)(a1, a2, v2) - 1;
    if ( v5 > 8 || *(v7 + v5) )
      break;
    ++v2;
    *(v7 + v5) = 1;
    if ( v2 >= 9 )
      return 1;
  }
  return 0;
}

他会遍历v2的0-8然后要求unk_261CD03063B函数的返回值,并且不能重复
图片描述
结合上图和之前的函数可以得出如下结论:

  1. 我们输入的字符串拼接上上文的两个数字之后要等于243+120
  2. 我们输入的字符串三个为一组,组成的数字的个位十位百位要满足:十位和个位要与传进来的第二和第三个参数相等,返回值是百位的值,并且返回值不能重复

我们再返回去分析那个大的while循环(即sub_261CD030AA3),他调用的三个函数实际上都和我们分析过的sub_261CD03093B函数类似,通过unk_261CD03063B函数来控制输入的个位十位百位的值

那么我们结合while循环和函数本身来分析这三个函数的参数会发现如下规律

  1. 第一个函数(unk_261CD03093B)的接受while循环中的v2作为十位数传给子函数,对于每个个位都要求返回值不能重复

  2. 第二个函数(unk_261CD0309A7)的接受while循环中的v2作为个位数传给子函数,对于每个十位都要求返回值不能重复

  3. 第一个函数(unk_261CD030A13)的接受v3和v4,他要求十位和个位每三个数为一组,组成的的结果传递给子函数,要求返回值不能重复,可以罗列一下第三个函数的值如下,要求类似如下九个数的返回值不能重复(v3,v4会+3):

    1
    2
    3
    00 01 02
    10 11 12
    20 21 22

我们将这些规则整合一起看就像是一个数独游戏,十位和个位是坐标,百位是数独里的值。

1
2
3
4
5
6
7
8
9
10
11
00, 01, 02, | 03, 04, 05, | 06, 07, 08,
10, 11, 12, | 13, 14, 15, | 16, 17, 18,
20, 21, 22, | 23, 24, 25, | 26, 27, 28,
----------------------------------------
30, 31, 32, | 33, 34, 35, | 36, 37, 38,
40, 41, 42, | 43, 44, 45, | 46, 47, 48,
50, 51, 52, | 53, 54, 55, | 56, 57, 58,
----------------------------------------
60, 61, 62, | 63, 64, 65, | 66, 67, 68,
70, 71, 72, | 73, 74, 75, | 76, 77, 78,
80, 81, 82, | 83, 84, 85, | 86, 87, 88,

第一条规则对于每个十位要求返回值不能重复即每一行不能有重复的值
第二条规则对于每个个位要求返回值不能重复即每一列不能有重复的值
第三条规则对应的就是每一宫不能有重复的值
回顾我们的输入会和他提供的一串头部字符串拼接,那串字符串同样满足这些条件,那我们就将他的字符串转换为数字,按照十位和个位是横纵坐标,百位是值的规律,填充到数独列表中就是个完整的数独游戏了
字符串如下:3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6
找一个在线链接填充一下即可,也可以使用chatgpt:
图片描述
我们得到了数独的数,也就是程序中的百位,我们将其和坐标放在一起组成一个完整的三位数。

1
2
3
4
5
6
7
8
9
800, 101, 202, 703, 504, 305, 606, 407, 908,
910, 411, 312, 613, 814, 215, 116, 717, 518,
620, 721, 522, 423, 924, 125, 226, 827, 328,
130, 531, 432, 233, 334, 735, 836, 937, 638,
340, 641, 942, 843, 444, 545, 746, 247, 148,
250, 851, 752, 153, 654, 955, 556, 357, 458,
560, 261, 162, 963, 764, 465, 366, 667, 868,
470, 371, 872, 573, 274, 675, 976, 177, 778,
780, 981, 682, 383, 184, 885, 486, 587, 288

那么最后一个问题就是顺序怎么办,前20个整数的顺序程序已经给我们了,那后60个呢,其实就是子函数(unk_261CD03063B)中判断的逻辑,对于长度大于21(v7>21)的数字,要和程序拼接的后一段字符串进行比较,比较的规则是上图代码中的if ( v24 + v22 + 8 * v24 == v32 * v27 ),所以后60个每一个数(个位*9+十位 == v32),我们写个脚本逆推一下即可

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def generate_hex_string(know):
    string = ''
    for x in know:
        string += ''.join(['{:03x}'.format(x).upper()])
         
    return string
 
k_l = []
 
 
a = [800, 101, 202, 703, 504, 305, 606, 407, 908, 910, 411, 312, 613, 814, 215, 116, 717, 518, 620, 721, 522, 423, 924, 125, 226, 827, 328, 130, 531, 432, 233, 334, 735, 836, 937, 638, 340, 641, 942, 843, 444, 545, 746, 247, 148, 250, 851, 752, 153, 654, 955, 556, 357, 458, 560, 261, 162, 963, 764, 465, 366, 667, 868, 470, 371, 872, 573, 274, 675, 976, 177, 778, 780, 981, 682, 383, 184, 885, 486, 587, 288]
 
ch = "677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280"
ch_list = [int(ch[x:x+2]) for x in range(0, len(ch), 2)]
two = []
for i in ch_list:
    for j in a:
        if j % 100 == (i//9)*10 + (i % 9):
            two.append(j)
 
one = []
for x in a:
    if x not in two:
        one.append(x)
k_l = one + two
 
print(len(k_l))
print(k_l)
 
hex_string = generate_hex_string(k_l)
print(len(hex_string))
print(hex_string)
 
# 3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E611230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120
# 提交的时候只需要提交后半段即可,因为前半段是程序帮我们添加的:11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120

图片描述


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2023-9-15 12:01 被Zero*/编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回