「MiniLCTF2021」Writeup by cuttl3fish

警告
本文最后更新于 2021-05-16,文中内容可能已过时。

Mini-L2021

URL: https://ctf.xidian.edu.cn/#/index

Team: cuttl3fish——kyriota | track | TBMK

Start Time: 5.06 20:00

End Time: 5.12 20:00

不会 java, 枯嘞.

1
New java.io.BufferedReader(New java.io.FileReader("/flag")).readLine()

hint:kawaii neko chan says that : what doesn’t kill u makes u stronger.

so what u should do is follow what she said, 然后同时连俩 bot 让他们对线, 看看谁更腻害.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import base64
f = open("story.txt", "rb").read()
while 1:
    fll=flast
    if b'{' in f and b'}' in f:
        print(f[::-1])
        break
    try:
        flast=f
        f=base64.b64decode(f)
    except:
        flast=fll
        try:
            f=base64.b64decode(f[::-1])
        except:
            f=base64.b64decode(flast[::-1])

把题目中的 game 函数拿出来研究, 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from tukuai import game
cheat=[[9,[1,0]],[9,[0,1]]]
init_state = [0] * 4
coin1 = randint(0, 1)
coin2 = randint(0, 1)
temp = coin1 * 2 + coin2
init_state[temp] = 1
servercoin,qc = game(cheat, init_state)
print(coin1)
print(coin2)
print(init_state)
print('my coin is ' + str(servercoin) + ' your coin is?')
from qiskit.tools.visualization import plot_bloch_multivector
display(plot_bloch_multivector(init_state))
simulator=Aer.get_backend('qasm_simulator')
result=execute(qc,backend=simulator).result()
from qiskit.tools.visualization import plot_histogram
display(qc.draw(output='mpl'))
display(plot_histogram(result.get_counts(qc)))

画出几个图出来看看, 只要绘出 bot 的输出 = my coin 的量子电路即可, 如下:

1
2
3
4
5
6
7
       ┌──────────────────────┐┌───┐        
q12_0: ┤0                     ├┤ X ├──■─────
       │  initialize(0,0,1,0) │└─┬─┘┌─┴─┐┌─┐
q12_1: ┤1                     ├──■──┤ X ├┤M├
       └──────────────────────┘     └───┘└╥┘
 c1: 1/═══════════════════════════════════╩═
                                          0 

这道题涉及到 windows 的异常处理机制 SEH

这个题不涉及太多 SEH的 底层, 大概有以下几个点需要了解的:

  • SEH 实际包含两个主要功能: 结束处理 (termination handling) 和异常处理 (exception handling)

  • 每当你建立一个 try 块, 它必须跟随一个 __finally 块或一个 __except

  • 一个 try 块之后不能既有 finally 块又有 except 块. 但可以在 try-except 块中嵌套 try-finally 块, 反过来也可以

  • __try, __finally 关键字用来标出结束处理程序两段代码的轮廓

    不管 try 块是如何退出的. 不论你在保护体中使用 return, 还是 goto, 或者是 longjump, 结束处理程序 (finally 块) 都将被调用

  • 在 try 使用 __leave 关键字会引起跳转到 try 块的结尾

  • ms_exc.registration.TryLevel 赋值是用于处理嵌套的 try

学习自 HAPPY 师傅的博客

然后看看题, main 函数直接看发现异常, 于是看汇编, 定位到伪代码异常处. IDA 的分析结果如下:

1
2
3
4
5
6
7
8
9
.text:00412330 loc_412330:                             ; CODE XREF: _main_0+15C↑j
.text:00412330 ;   __try { // __except at loc_412377
.text:00412330                 mov     [ebp+ms_exc.registration.TryLevel], 0
.text:00412337                 lea     ebx, [ebp+Str]
.text:0041233D                 xor     eax, eax
.text:0041233F                 db      3Eh
.text:0041233F                 mov     dword ptr [eax], 0
.text:00412346                 mov     edx, 0
.text:0041234B                 div     edx

发现非常明显的除零异常还有 eax 清零后却试图访问它的内存, 以及 SEH 结构. 不需要对它进行任何 patch, 因为必须让程序捕获到这个异常, 才会去执行 __except_filter, 也就是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
text:00412356 loc_412356:                             ; DATA XREF: .rdata:stru_41A238↓o
.text:00412356 ;   __except filter // owned by 412330
.text:00412356                 mov     eax, [ebp+ms_exc.exc_ptr]
.text:00412359                 mov     ecx, [eax]
.text:0041235B                 mov     edx, [ecx]
.text:0041235D                 mov     [ebp+var_1BC], edx
.text:00412363                 mov     eax, [ebp+ms_exc.exc_ptr]
.text:00412366                 push    eax
.text:00412367                 mov     ecx, [ebp+var_1BC]
.text:0041236D                 push    ecx
.text:0041236E                 call    sub_411131
.text:00412373                 add     esp, 8
.text:00412376                 retn

稍微看一看 sub_411131 函数的内部逻辑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __cdecl sub_411DD0(int a1, _EXCEPTION_POINTERS *a2)
{
  unsigned int i; // [esp+D0h] [ebp-40h]
  char v4[40]; // [esp+DCh] [ebp-34h] BYREF
  char *v5; // [esp+104h] [ebp-Ch]

  __CheckForDebuggerJustMyCode(&unk_41D015);
  if ( a2->ExceptionRecord->ExceptionCode != 0xC0000094 ) // 除零异常相应的异常代码
    return 0;
  v5 = (char *)(a2->ContextRecord->Ebx + 9);
  qmemcpy(v4, "!V -}VG-bp}m-nG!b|ra GyGE|Drp D", 31);
  for ( i = 0; i < 0x1F; ++i )
  {
    if ( v4[i] != ((unsigned __int8)a2->ContextRecord->Eip ^ ((v5[2 * i + 1] ^ 0x4D) - 4) ^ 0x13) )
    {
      a2->ContextRecord->Eip += 54;
      return -1;
    }
  }
  a2->ContextRecord->Eip += 63;
  return -1;
}

可以发现将运算结果存在 v5 中, 但是只有奇数位, 不妨试着还原一下:

1
2
3
4
5
char magic_1[] = "!V -}VG-bp}m-nG!b|ra GyGE|Drp D";
    for(int i = 0; i < 0x1f; i++) {
        // printf("%c", ((magic_1[i] ^ 0x13 ^ errr_addr)+4)^0x4d);
        flag[2 * i + 1] = ((magic_1[i] ^ 0x13 ^ errr_addr)+4)^0x4d;
    }

并且如果满足条件, 将会改变 eip 的值, 将进程从异常中跳出来, 不妨看看跳到了哪里:

1
print(hex(0x41234B + 63))

那里是 congratulations 的提示信息,但很明显我们还没拿到完整的 flag.

查看 IDA 的 Exports 窗口可以看到 TlsCallback_0_0

TLS (Thread Local Storage) 线程局部存储, TLS 回调函数的调用运行要先于PE代码执行, 该特性使它可以作为一种反调试技术使用.

TLS 是各线程的独立的数据存储空间, 使用 TLS 技术可在线程内部独立使用或修改进程的全局数据或静态数据.

return 了一个奇怪的函数

 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
PVOID __stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
  __CheckForDebuggerJustMyCode(&unk_41D015);
  return AddVectoredExceptionHandler(1u, Handler);
}

// attributes: thunk
LONG __stdcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
  return sub_411BD0(ExceptionInfo);
}

int __stdcall sub_411BD0(_EXCEPTION_POINTERS *a1)
{
  unsigned int i; // [esp+D0h] [ebp-40h]
  char v3[40]; // [esp+DCh] [ebp-34h]
  DWORD v4; // [esp+104h] [ebp-Ch]

  __CheckForDebuggerJustMyCode(&unk_41D015);
  if ( a1->ExceptionRecord->ExceptionCode != 0xC0000005 ) // 不可访问地址相应的异常代码
    return 0;
  v4 = a1->ContextRecord->Ebx + 9;
  v3[0] = 16;
  v3[1] = 4;
  v3[2] = 24;
  v3[3] = 11;
  v3[4] = 24;
  v3[5] = 16;
  v3[6] = 4;
  v3[7] = 21;
  v3[8] = 11;
  v3[9] = 5;
  v3[10] = 31;
  v3[11] = 46;
  v3[12] = 33;
  v3[13] = 46;
  v3[14] = 72;
  v3[15] = 21;
  v3[16] = 6;
  v3[17] = 46;
  v3[18] = 17;
  v3[19] = 69;
  v3[20] = 5;
  v3[21] = 62;
  v3[22] = 46;
  v3[23] = 24;
  v3[24] = 21;
  v3[25] = 72;
  v3[26] = 46;
  v3[27] = 69;
  v3[28] = 33;
  v3[29] = 31;
  v3[30] = 10;
  for ( i = 0; i < 0x1F; ++i )
  {
    if ( v3[i] != (((*(char *)(v4 + 2 * i) ^ 0x37) + 4) ^ 0x42) )
    {
      a1->ContextRecord->Eip += 66;
      return -1;
    }
  }
  a1->ContextRecord->Eip += 7;
  return -1;
}

(Handler显然是第二段解密), 因为我实在是太菜了, 又查了一下这个函数的功能, 发现了另一个异常处理机制 VEH

VEH 处理流程:

  • CPU 捕获异常信息
  • 通过 KiDispatchException 进行分发 (EIP = KiUserExceptionDispatcher)
  • KiUserExceptionDispatcher 调用 RtIDispatchException
  • RtIDispatchException 查找 VEH 处理函数链表并调用相关处理函数
  • 代码返回到 KiUserExceptionDispatcher
  • 调用 ZwContinue 再次进入 0 环 (ZwContinue 调用 NtContinue, 主要作用就是恢复 TRAPFRAME 然后通过 _KiServiceExit 返回到 3 环)
  • 线程再次返回 3 环后, 从修正后的位置开始执行

学习自:https://blog.csdn.net/weixin_42052102/article/details/83540134

这样一来整个流程:

  • VEH 抓到 0xC0000005
  • SEH 抓到 0xC0000094
  • 分别的 flag 在各自的 handler 里面

脚本如下, 写的比较乱:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
    char flag[100] = {0};
    unsigned char errr_addr = 0x30234B;
    char magic_1[] = "!V -}VG-bp}m-nG!b|ra GyGE|Drp D";
    for(int i = 0; i < 0x1f; i++) {
        // printf("%c", ((magic_1[i] ^ 0x13 ^ errr_addr)+4)^0x4d);
        flag[2 * i + 1] = ((magic_1[i] ^ 0x13 ^ errr_addr)+4)^0x4d;
    }
    printf("\n");
    char magic_2[] = {16,4,24,11,24,16,4,21,11,5,31,46,33,46,72,21,6,46,17,69,5,62,46,24,21,72,46,69,33,31,10};
    for(int i = 0; i < 0x1f; i++) {
        // printf("%c", ((magic_2[i]^0x42)-4)^0x37);
        flag[2 * i] = ((magic_2[i]^0x42)-4)^0x37;
    }
    printf("%s", flag);
    // miniLctf{y0u_a1r4ady_und4rstand_th4_w1nd0ws_exc4pt1On_handl1e_m4chan1sm}
    return 0;
}

傀儡进程

这题居然让我电脑报毒了,让我康康! (康不懂,爬了

main 函数很混乱, 但仔细看能看出一点东西, 貌似是创建一个进程, 尝试把另一个文件读进来, 然后开始执行? 还看见一个熟悉的 SMC.

看了 hint 之后搜到了傀儡进程, 一个最基本傀儡进程的实现如下

  • CreateProcess 创建进程, 传入参数 CREATE_SUSPENDED 使进程挂起
  • NtUnmapViewOfSection 清空新进程的内存数据
  • VirtualAllocEx 申请新的内存
  • WriteProcessMemory 向内存写入 payload
  • SetThreadContext 设置入口点
  • ResumeThread 唤醒进程, 执行 payload

emmm 直接调的话, 由于各种奇怪的反调试, 好像没法成功, 于是我打开了010editor, 直接把傀儡进程在运行前全都异或回去, 并dump出来单独分析.

清晰的 main 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  size_t input_len; // eax
  char v5; // [esp+0h] [ebp-10Ch]
  char v6; // [esp+0h] [ebp-10Ch]
  char input[56]; // [esp+D0h] [ebp-3Ch] BYREF

  __CheckForDebuggerJustMyCode(&unk_40C015);
  memset(input, 0, 0x32u);
  printf("Please input your flag: ", v5);
  scanf("%s", (char)input);
  input_len = strlen(input);
  if ( (unsigned __int8)off_40A040(input, input_len) )
    printf("Congratulation~~~", v6);
  else
    printf("Try again~~~", v6);
  getchar();
  getchar();
  return 0;
}

off_40A040 一路往下点,就看到加密逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
char __cdecl sub_4014F0(int a1, int a2)
{
  int i; // [esp+DCh] [ebp-8h]

  __CheckForDebuggerJustMyCode(&unk_40C015);
  if ( a2 != 32 )
    return 0;
  for ( i = 0; i < 32; ++i )
  {
    if ( (char)(((*(_BYTE *)(i + a1) ^ 0x66) + 4) ^ 0x55) != byte_40A020[i] )
      return 0;
  }
  return 1;
}

挺常规的, 直接还原:

1
2
3
4
5
6
7
8
magic_1 = [0x5A,0x46,0x59,0x46,0x7B,0x5C,0x43,0x51,0x74,0x63,0x47,0x0E,0x4C,0x68,0x0E,0x4C,0x68,0x43,0x47,0x03,0x68,0x51,0x5E,0x44,0x03,0x68,0x51,0x0E,0x5E,0x50,0x1E,0x4A]
flag = ''

for i in range(len(magic_1)):
    flag += chr(((magic_1[i] ^ 0x55) -4) ^ 0x66)

print(flag)
# miniLctf{Th1s_1s_th4_fak4_f1ag!}

我 aklsjdaiwjdawijdaiwdjqw (

又是上一题一样的, 在 Exports 里面有 TlsCallback_0_0. 里面有对于 off_40A040的 指向进行处理,然后就没啥问题了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
magic_1 = [0x5A,0x46,0x59,0x46,0x7B,0x5C,0x43,0x51,0x74,0x63,0x47,0x0E,0x4C,0x68,0x0E,0x4C,0x68,0x43,0x47,0x03,0x68,0x51,0x5E,0x44,0x03,0x68,0x51,0x0E,0x5E,0x50,0x1E,0x4A]
fake_flag = ''

for i in range(len(magic_1)):
    fake_flag += chr(((magic_1[i] ^ 0x55) -4) ^ 0x66)

print(fake_flag)

magic_2=[0x5A,0x26,0x59,0x26,0x7B,0x5C,0x43,0x51,0x54,0x6D,0x52,0x68,0x0E,0x4C,0x68,0x4C,0x0F,0x68,0x0E,0x59,0x43,0x03,0x4D,0x03,0x4C,0x43,0x0E,0x59,0x50,0x1E,0x1E,0x4A]
flag = ''

for i in range(len(magic_2)):
    flag += chr(((magic_2[i] ^ 0x66) - 4) ^ 0x55)

print(flag)

FIN

还是太菜了, 虽然是校内rank3, 但这分数太惨了, 加油加油!