首页
社区
课程
招聘
[原创]看雪 2023 KCTF 年度赛 第六题 至暗时刻 解题过程(数独)
2023-9-14 23:06 8440

[原创]看雪 2023 KCTF 年度赛 第六题 至暗时刻 解题过程(数独)

2023-9-14 23:06
8440

一、反调处理

直接扔调试器运行,运行后黑框框没有内容。换终端里运行,提示请输入。
意味着有反调试。

于是打开反反调插件,SyllaHide默认配置随便选第1个、第2个或者SharpOD全部打勾。
这时在调试器运行,能执行到提示请输入。

二、初步分析

静态分析加载完,进去main就能看见入口sub被创建线程。
跟进去,四处查看,初步情况如下:

  1. 字符串异或加密,解密密钥带在解密函数(0x1400013B0)入参。

  2. 有一些重要函数是放了一些4字节常量然后走到syscall,且无法输出伪代码。

1. 字符串处理

考虑到字符串不多,先对解密函数返回的ret指令(0x14000144E)下日志断点,输出明文字符串查看。

1
dec_str    {utf8@cax}

测试运行一次,字符串并不多:

1
2
3
4
dec_str    Please enter your key:
dec_str    kctf
dec_str    kernel32.dll
dec_str    RtlFillMemory

2. syscall处理

将那个syscall指令的函数设置成__fastcall 4个参数,这时基本都正常输出伪代码。如果其他函数出现参数特别多的情况,也先给他降到4个。

进一步分析syscall涉及的函数:

1
2
3
1层:0x140002A10:call_syscall,通过syscall指令发起系统调用。
2层:0x1400029C4:get_syscall_index 该函数入参为先前赋值的4字节常量,用来获取对应的系统调用号。
3层:0x140002818:populate_syscall_index 动态从ntdll.dll建立好系统调用索引查询表。

考虑到涉及函数也不是很多,还是采用日志断点输出的方法。
在0x140002818:populate_syscall_index函数中,
下2个日志断点:

  1. 0x1400028F6:开始计算函数hash的地方,用于输出函数名字符串。
1
fname = {utf8@r9}
  1. 0x140002911:保存函数名hash的地方,输出hash。
1
hash = 0x{x:ebx}

重来运行一次,日志中就得到了全部函数名与hash的对应。
复制日志到文本备用。

根据对应关系,重命名部分函数:
ZwAllocateVirtualMemory
ZwWriteVirtualMemory
ZwCreateThreadEx
ZwQueueApcThread_140002E4E

同时调整这些函数的参数数量,重点是ZwQueueApcThread,它有5个参数。

3. 初步流程

把syscall函数都重命名之后,流程变得清晰。

  1. 分配了一块内存BYTE* pMem。
  2. 复制了1个字符串进去,字符串里面包含了输入串。
  3. 通过ZwQueueApcThread RtlFillMemory1个字节1个字节地把一块shellcode复制到pMem+500位置。
  4. 再通过ZwQueueApcThread pMem+500执行shellcode。
  5. 执行完之后,根据pMem + 67处的字符串作出ok与no判断。

三、Dump Shellcode

在ZwQueueApcThread pMem+500 call处(0x140001D9C)下断点,断下后对第2个参数即RDX寄存器跳转到反汇编窗口,对要执行的shellcode入口下断点,运行即断在shellcode入口。
移除入口处断点,单步步进一次,那个call很神奇,call到的自己的最后一个机器码。
把下一条指令地址压栈备用的同时,使得一般的反汇编器不能从call那个地方反汇编下去。
因而需要从call指令的最后一个字节开始反汇编。

此时用savedata保存shellcode单独分析,从pMem处开始,长度500+2347 = 0xb1f。
比如:

1
savedata c:\temp\sc.bin,0x0000023676EB0000,0xb1f

然后把sc.bin按dump时的pMem地址作为基址加载到分析工具。

后来发现在此处dump还不合适,加载之后发现call目标不再范围内。
单步研究后发现,紧接着代码还有一处循环自修改。

改为在循环之后dump,得到的shellcode即可全部正常加载出代码。

四、Shellcode分析

1. 动态定位API

shellcode加载之后,发现有动态定位API的call。
shellcode需要动态定位API才能位置无关。
无需要跟进分析,步过call,根据返回值显示的函数名称信息,重命名局部函数指针即可。
感兴趣的可以参考很久以前写过的动态定位API文章:
https://bbs.kanxue.com/thread-203319.htm

2. 主要流程

重命名局部函数指针之后,逻辑就很清晰了。

  1. 通过进程快照遍历进程,找到自己进程,然后找到pMem
  2. 将pMem+4传给校验函数,姑且叫checkall
  3. 根据checkall返回值,赋值主程序用于判断ok或no的字符串。

3. 验证算法

checkall里面依次调用check1,check2,check3,且3个函数都调用同一个公共check函数。

分析后check原型大致如下:

1
__int64 __fastcall check(char *Sz, int _shiwei, int __gewei)

后两个入参为十位、个位,返回百位为正确分支。

看了check1 2 3的检查逻辑之后,发现加起来刚好就是数独的规则。
代码还原如下:

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
95
96
97
98
99
100
101
102
103
104
105
106
char __fastcall checkall(char *Sz)
{
  unsigned int i;
  int j;
  int k;
 
  i = 0;
  while ( check1(Sz, i) && check2(Sz, i) )
  {
    if ( (int)++i >= 9 )
    {
      j = 0;
label_cont:
      k = 0;
      while ( check3(Sz, (unsigned int)j, (unsigned int)k) )
      {
        k += 3;
        if ( k >= 9 )
        {
          j += 3;
          if ( j < 9 )
            goto label_cont;
          return 1;
        }
      }
      return 0;
    }
  }
  return 0;
}
 
 
char __fastcall check1(char *Input, __int64 i)
{
  int j;
  int i_;
  unsigned int oi;
  unsigned int dws[9];
 
  memset(dws, 0, sizeof(dws));
  j = 0;
  i_ = i;
  while ( 1 )
  {
    oi = check(Input, i_, j) - 1;
    if ( oi > 8 || dws[oi] )
      break;
    ++j;
    dws[oi] = 1;
    if ( j >= 9 )
      return 1;
  }
  return 0;
}
 
char __fastcall check2(char *Sz, __int64 i)
{
  int j;
  int i_;
  unsigned int oi;
  unsigned int dws[9];
 
  memset(dws, 0, sizeof(dws));
  j = 0;
  i_ = i;
  while ( 1 )
  {
    oi = check(Sz, j, i_) - 1;
    if ( oi > 8 || dws[oi] )
      break;
    ++j;
    dws[oi] = 1;
    if ( j >= 9 )
      return 1;
  }
  return 0;
}
 
char __fastcall check3(char *Input, __int64 j, __int64 k)
{
  int addj;
  int k_;
  int j_;
  int addk;
  unsigned int oi;
  unsigned int dws[9];
 
  memset(dws, 0, sizeof(dws));
  addj = 0;
  k_ = k;
  j_ = j;
  while ( 2 )
  {
    for ( addk = 0; addk < 3; ++addk )
    {
      oi = check(Input, addj + j_, addk + k_) - 1;
      if ( oi > 8 || dws[oi] )
        return 0;
      dws[oi] = 1;
    }
    if ( ++addj < 3 )
      continue;
    break;
  }
  return 1;
}

并且通过对check的分析,得知先前被写到pMem开头的字符串结构与每段的处理:

1
kctf + 63个字符为21组已知的3位数 + 需要输入180个字符(6030位数) + 120个字符(可得出前面60组数的个位与十位)。

现在就缺少60个百位,并且这60个百位与已知的21个百位,
按所在3位数的十位与个位作为二维索引,建立出一个数独。
求解出数独,再把求出的60个百位与各自对应的十位、个位组成3位数。即可得出flag。

如下python代码实现。

五、Python Keygen

1
pip install sudokutools more-itertools

安装三方库后可直接运行,输出flag。

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
from more_itertools import chunked
from sudokutools.solve import bruteforce
from sudokutools.sudoku import Sudoku
 
 
head = '3201382652D139C0E22132DF1BC2212EA0991650A229B36436823D0B13D51E6'
tail = '677116575313142309154604431859253431473963507533496829080645035455771774602058076430276921790210013736267644383505517280'
 
# 头部63个字符 每3个字符作为1个16进制数转到10进制
nums = [int(''.join(nstr), 16) for nstr in chunked(head, 3)]
 
sd = [[0]*9 for _ in range(9)]
 
# 构成数独
for n in nums:
    sd[n//10%10][n%10] = n//100
     
for r in sd:
    print(r)
     
sudoku = '\n'.join(''.join(map(str, r)) for r in sd)
print('\nsudoku:\n', sudoku, sep='')
 
# 求数独解
sudoku = Sudoku.decode(sudoku)
for solution in bruteforce(sudoku):
    print('\nans:\n', solution, sep='')
    ans = list(chunked(solution.encode(), 9))
     
    finvals = []
    # 尾部120个字符 每2个按10进制转成整数后再转成9进制 最后会把9进制的位当十进制的位使用
    for dec in chunked(tail, 2):
        d = int(''.join(dec))
        ge = d % 9
        shi = d // 9
        # 以十位、个位去取出对应的百位,组成3位数
        finvals.append(int(ans[shi][ge])*100+shi*10+ge)
    # 最后将60个3位数 转16进制后拼接 即得到flag
    print('\nflag =', ''.join('{:03X}'.format(f) for f in finvals))

输出:

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
[8, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 3, 6, 0, 0, 0, 0, 0]
[0, 7, 0, 0, 9, 0, 2, 0, 0]
[0, 5, 0, 0, 0, 7, 0, 0, 0]
[0, 0, 0, 0, 4, 5, 7, 0, 0]
[0, 0, 0, 1, 0, 0, 0, 3, 0]
[0, 0, 1, 0, 0, 0, 0, 6, 8]
[0, 0, 8, 5, 0, 0, 0, 1, 0]
[0, 9, 0, 0, 0, 0, 4, 0, 0]
 
sudoku:
800000000
003600000
070090200
050007000
000045700
000100030
001000068
008500010
090000400
 
ans:
8 1 2 | 7 5 3 | 6 4 9
9 4 3 | 6 8 2 | 1 7 5
6 7 5 | 4 9 1 | 2 8 3
------+-------+------
1 5 4 | 2 3 7 | 8 9 6
3 6 9 | 8 4 5 | 7 2 1
2 8 7 | 1 6 9 | 5 3 4
------+-------+------
5 2 1 | 9 7 4 | 3 6 8
4 3 8 | 5 2 6 | 9 1 7
7 9 6 | 3 1 8 | 4 5 2
 
flag = 11230A2CD3C31CA32E0D707D38E0743531F80F726C1D133B3A914E2F034B1D63BB17F34428E2A31B038C25E0FA2BF2301053752062AA16E20A2FC1971730E90823D01A724B0CA19B0652811541480B80943AE27E13122C30C120

[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

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