2.19 解题记录

成功出线!

CSGO

一个 Go 编写的程序,不过似乎没法调试(一调试就会卡住),尝试不使用调试器运行,然后 attach 到进程上去,这样才能动调。

先静态分析。

main_main() 函数 75 行处,fmt_Fscanf() 读取我们输入的内容;79-103 行对输入内容进行操作。104-125 行判断输入内容是否正确并给出答复。

如上图,值得注意的是,在第 88 行处出现 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',疑似是 Base64 编码。

现在在进入判断前的语句处打断点调试,发现如下字符串:

'kx8skC4EXSgqkuQ5kQI4XAIEmCgqnuX/mR8EiB45mCoqjfU6oicqk/HsTi/='

看来这里使用的是 Base64 无疑了。不过这一串怎么看怎么不像正常编码编码出来的 flag。

尝试后也确实是这样。众所周知,正常 Base64 编码的 'fla' 三个字符是 'Zmxh',这四个字符在编码表中的相对位置分别是 13、11、-16,而此编码前四个字符的相对位置也分别是 13、11、-16,因此我们大胆猜测换过的编码表只是将原先的编码表循环位移,根据偏移量知道换过的编码表为 'LMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJK='

flag{y0u_f1nd_m3_you_r34lly_know_aBout_gO!!}

ezvm

程序相当简洁。程序读取输入的 34 个字节并存储到 program 中。fetch() 函数读取 program 中的操作码,eval() 函数则会根据操作码执行程序。虚拟机的核心,也就是虚拟机执行部分的伪代码如下:

 1switch ( a1 )
 2{
 3  case 0:
 4    ++dword_404024;
 5    ++dword_408030;
 6    result = dword_404024;
 7    stack[dword_404024] = program[dword_408030];
 8    break;
 9  case 1:
10    stack[++dword_404024] = program[++dword_408030];
11    v3 = dword_404024--;
12    v12 = stack[v3];
13    v4 = dword_404024--;
14    v11 = stack[v4];
15    result = ++dword_404024;
16    stack[dword_404024] = v11 + v12;
17    break;
18  case 2:
19    v2 = dword_404024--;
20    result = stack[v2];
21    break;
22  case 3:
23    running = 0;
24    result = puts("done");
25    break;
26  case 4:
27    stack[++dword_404024] = program[++dword_408030];
28    v5 = dword_404024--;
29    v10 = stack[v5];
30    v6 = dword_404024--;
31    v9 = stack[v6];
32    result = ++dword_404024;
33    stack[dword_404024] = v10 ^ v9;
34    break;
35  case 5:
36    stack[++dword_404024] = program[++dword_408030];
37    v7 = dword_404024--;
38    v8 = stack[v7];
39    result = v8;
40    if ( v8 != stack[dword_404024] )
41      exit(0);
42    return result;
43  default:
44    return result;
45}

这里又使用 stack 模拟栈操作。为了方便后面表述,我们称高地址为栈顶,低地址为栈底。

虚拟机中一共定义六种操作码:

操作码 描述
0x00 program 中该操作码后的一个值压入栈顶
0x01 将栈顶的两个值相加,存储到其中较低的地址
0x02 栈顶指针向低地址移动。虽然栈以外的值没有被丢弃,但是也不会再被使用了,因此该题中“栈顶”是以栈指针确定的
0x03 结束运行,并给出完成运行的提示,也就是我们想要的提示
0x04 对栈顶的两个值做异或操作,存储到其中较低的地址
0x05 比较栈顶两个值是否相等,如果相等则继续运行,如果不相等则退出

下面开始分析操作码,即 program 中的数据。

导出 program 中的操作码并且略加整理(例如经过调试后知道栈中只保留一个字节,故 07E8h 只保留 0xE8;)后,操作码如下所示:

 100 66 00 6C 00 61 00 67
 200 66 00 6C 00 61 00 67
 300 66 00 6C 00 61 00 67
 400 66 00 6C 00 61 00 67
 500 66 00 6C 00 61 00 67
 600 66 00 6C 00 61 00 67
 700 66 00 6C 00 61 00 67
 800 66 00 6C 00 61 00 67
 900 66 00 6C 
10
1101 E8 04 03 05 66 
1202 01 E8 04 03 05 56 
1302 01 E8 04 03 05 5D 
1402 01 E8 04 03 05 44 
1502 01 E8 04 03 05 4F 
1602 01 E8 04 03 05 55 
1702 01 E8 04 03 05 1F 
1802 01 E8 04 03 05 5F 
1902 01 E8 04 03 05 58 
2002 01 E8 04 03 05 39 
2102 01 E8 04 03 05 4E 
2202 01 E8 04 03 05 4F 
2302 01 E8 04 03 05 55 
2402 01 E8 04 03 05 3E 
2502 01 E8 04 03 05 44 
2602 01 E8 04 03 05 5E 
2702 01 E8 04 03 05 54 
2802 01 E8 04 03 05 62 
2902 01 E8 04 03 05 44 
3002 01 E8 04 03 05 18 
3102 01 E8 04 03 05 50 
3202 01 E8 04 03 05 1A 
3302 01 E8 04 03 05 57 
3402 01 E8 04 03 05 44 
3502 01 E8 04 03 05 58 
3602 01 E8 04 03 05 50 
3702 01 E8 04 03 05 1B 
3802 01 E8 04 03 05 54 
3902 01 E8 04 03 05 57 
4002 01 E8 04 03 05 60 
4102 01 E8 04 03 05 4C 
4202 01 E8 04 03 05 4A 
4302 01 E8 04 03 05 57 
4402 01 E8 04 03 05 4D 
4502 01 02 03 

经过格式化的处理,可以大体看出程序分为两部分:

第一部分存储入栈指令以及我们输入的数据(在此次调试中。我输入的是 'flagflagflagflagflagflagflagflagfl'),程序依次将我们输入的字符压入栈。在静态分析中,这一部分在未经初始化时全部由 0 填充。

程序的第二部分对全部 34 个字符做了相同的操作并进行判定,使用代码表示就是 ((input + 0xE8) % 0x100) ^ 3 == dest。其中,input 是我们输入的字符,dest 是上面每一行的最后一个操作码,对 0x100 取模的原因依然是 stack 中仅能保留一个字节。上述过程在动态调试中可以非常清晰地展现出来。此外,除了第一个字符,程序在对后续每一个字符进行操作的时候都有一个栈顶指针向低地址移动的过程,即字符串是从后向前处理的,因此我们在恢复字符串的过程中也要倒序处理:

1c = [0x66, 0x56, 0x5D, 0x44, 0x4F, 0x55, 0x1F, 0x5F, 0x58, 0x39, 0x4E, 0x4F, 0x55, 0x3E, 0x44, 0x5E, 0x54, 0x62, 0x44, 0x18, 0x50, 0x1A, 0x57, 0x44, 0x58, 0x50, 0x1B, 0x54, 0x57, 0x60, 0x4C, 0x4A, 0x57, 0x4D]
2for i in c[::-1]:
3    print(chr((i ^ 3) + 0x100 - 0xE8), end = '')
flag{lo0ks_l1k3_you_UndeRst4nd_vm}

maze

程序是用 Python 编写的,先拆包。反编译得到的 .pyc 文件:

1from maze import run
2run()

程序的主要逻辑不在这一段 Python 代码里,而是在 maze.run() 方法中。而先前在 ELF 文件中拆出来的恰好有一个 maze.so,因此对这个动态链接库展开分析。

先将它作为模块导入 Python 看一看里面有什么东西:

1import sys
2sys.path.append('./maze.so')
3import maze
4help(maze)

根据这些可以得到

1DATA
2    EqdU3uQNCi= [18, 17, 15, 0, 27, 31, 10, 19, 14, 21, 25, 22, 6, 3, 30, 8, 24, 5, 7, 4, 13, 29, 9, 26, 1, 2, 28, 16, 20, 32, 12, 23, 11]
3    UJ9mxXxeoS= 'IyMgIyMgIyMgIyMgIyMgIyMgIyMKIyMgIyMgIyMgXl4gIyMgXl4gIyMKIyMgIyMgIyMgLi4gIyMgSVogIyMgIyMgIyMgIyMKIyMgJVIgLi4gJUQgIyMgJUQgLi4gLi4gJUwgIyMKIyMgPj4gIyMgLi4gIyMgRUEgKiogUFAgJVUgIyMKIyMgJVUgSUEgVEEgIyMgRUIgKiogUFAgJVUgIyMKIyMgJVUgSUIgVEIgIyMgRUMgKiogUFAgJVUgIyMKIyMgJVUgSUMgVEMgIyMgRUQgKiogUFAgJVUgIyMKIyMgJVUgSUQgVEQgIyMgRUUgKiogUFAgJVUgIyMKIyMgJVUgSUUgVEUgIyMgRUYgKiogUFAgJVUgIyMKIyMgJVUgSUYgVEYgIyMgJVIgKiogSVogJVUgIyMKIyMgJVUgSUcgJUwgIyMgIyMgIyMgIyMgIyMgIyMKIyMgIyMgIyMgIyMgIyMgIyMKClBQIC0+ICs9MQpNTSAtPiAtPTEKSVogLT4gPTAKRUEgLT4gSUYgPT0wIFRIRU4gJVIgRUxTRSAlRApFQiAtPiBJRiA9PTEgVEhFTiAlUiBFTFNFICVECkVDIC0+IElGID09MiBUSEVOICVSIEVMU0UgJUQKRUQgLT4gSUYgPT0zIFRIRU4gJVIgRUxTRSAlRApFRSAtPiBJRiA9PTQgVEhFTiAlUiBFTFNFICVECkVGIC0+IElGID09NSBUSEVOICVSIEVMU0UgJUQKVEEgLT4gSUYgKiogVEhFTiAlTCBFTFNFICVECklBIC0+ID03MgpUQiAtPiBJRiAqKiBUSEVOICVMIEVMU0UgJUQKSUIgLT4gPTczClRDIC0+IElGICoqIFRIRU4gJUwgRUxTRSAlRApJQyAtPiA9ODQKVEQgLT4gSUYgKiogVEhFTiAlTCBFTFNFICVECklEIC0+ID04MApURSAtPiBJRiAqKiBUSEVOICVMIEVMU0UgJUQKSUUgLT4gPTY3ClRGIC0+IElGICoqIFRIRU4gJUwgRUxTRSAlRApJRiAtPiA9ODQKSUcgLT4gPTcwCkxUIC0+IElGID09NiBUSEVOICVEIEVMU0UgJUwK'
4    c2VjcmV0= [7, 47, 60, 28, 39, 11, 23, 5, 49, 49, 26, 11, 63, 4, 9, 2, 25, 61, 36, 112, 25, 15, 62, 25, 3, 16, 102, 38, 14, 7, 37, 4, 40]
5    regexes= {'wall': '##|``', 'path': '\\.\\.', 'splitter': '<>', 'pause': '[0-9]{2}', 'start': '\\^\\^', 'hole': '\\(\\)', 'out': '>>', 'in': '<<', 'one-use': '--', 'direction': '%[LRUDNlrudn]', 'signal': '(?<=\\*)[\\*A-Za-z0-9]', 'function': '[A-Za-z][A-Za-z0-9]'}

在 IDA 中直接搜索 'run',可以得到 maze.run() 方法的伪代码。

在这个函数里,发现调用了 _pyx_mstate_global_static.__pyx_n_s_c29sdmU 进行加密。'c29sdmU' 使用 Base64 解码是 solve。

随后查找 c29sdmU。在 _pyx_pw_4maze_3c29sdmU() 函数中进行了加密操作。同时找到 maze.mazeLang 作为 maze 语言的解释器:

在这道题中的 Maze,是一种编程语言:

1##,##,##
2##,^^,## //Car Starts
3##,AA,## //Do AA to Car
4##,>>,## //Print Car
5##,(),## //Destroy Car
6##,##,##
7
8AA-> ="Hello World!"

而我们最开始 help() 中得到的 UJ9mxXxeoS,就是 Base64 编码后的 mazeLang。现在根据已知的信息写出一下脚本:

 1import sys
 2sys.path.append('./maze.so')
 3import maze
 4import base64
 5maze.aW5pdF9zZWNyZXQ()  # 初始化
 6flag = ''
 7c = maze.TWF6ZUxhbmc(base64.b64decode(maze.UJ9mxXxeoS).decode())  # 解码 maze 代码
 8for i in range(33):
 9    flag += c.cnVuX3RpbGxfb3V0cHV0() ^ maze.c2VjcmV0[i]  # 执行 maze 代码并且还原明文
10print(flag)
flag{yOu_@re_m@sT3r_OF_mAZElaN6}

孩子们,我回来了

程序使用了 Base64 编码,不过显然是换过表的。动调程序的时候程序会抛出异常,根本看不到换表的过程

不过程序还是给我们留下了破绽。不管我们输入什么内容,程序都会输出我们输入内容的编码和正确的编码。

既然只要编码相同原字符串就相同,那我们可以尝试单字节爆破。仅展示爆破最后一个字符的脚本(当然我们都知道最后一个字符是啥):

 1import subprocess
 2
 3known = "flag{s3e_y0u_4ga1in?you_did_1t!!"
 4
 5for i in range(32, 128):
 6    flag = known + chr(i)
 7    p = subprocess.Popen(["孩子们,我回来了.exe"], stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
 8    p.stdin.write(flag.encode())
 9    p.stdin.close()
10    out = p.stdout.read()
11    if out[0:44] == out[-67:-23]:
12        print(flag)

前面字符爆破的方式差不多,只是由于经过 Base64 编码的文本原文和编码是不等长的,所以有时会出现多个字符都满足条件的情况,这时需要调整匹配字符串长度以及根据语义分析来缩小范围。

flag{s3e_y0u_4ga1in?you_did_1t!!}