首页
社区
课程
招聘
看雪CTF.TSRC 2018 团队赛 第十二题 移动迷宫
2018-12-25 04:33 6264

看雪CTF.TSRC 2018 团队赛 第十二题 移动迷宫

2018-12-25 04:33
6264

第十二题 移动迷宫

观察

题目给了一个带一个很滑稽的图标的程序。运行一下会发现它长得像某种安装器。strings一下可以看到一些像是

testsoft Install System v2.48
XXXXsofttestS.exehead
http:/tests.sf.net/test_Error

 

强行打了打码。Google 搜一下 "Install System v2.48" 即可得知打码之前应该是 Nullsoft Install System v2.48。
所以这看起来外面包了一层 NSIS,考虑到运行起来之后的那个输入的界面长的也很像 NSIS 本体,估计是写了一些 NSIS 脚本或者插件来执行验证。

分析

NSIS 安装包 7-Zip 是可以解压的,不过直接打开会报错,先使用任意你最喜欢的十六进制编辑器打开 CrackMe.exe,观察一下,发现文件尾部附带的数据头部的 NullsoftInst 被改成了 XXXXsoftInst,改回来之后用 7-Zip 就可以打开啦,可以把包含 NSIS 脚本的反汇编结果和插件dll的所有内容提取出来。记得使用一个早一点版本的 7-Zip,例如 9.38,新版本移除了反汇编 NSIS 脚本的功能。

 

7-Zip 并没有把所有的函数偏移识别出来,不过这不大影响我们分析。可以看到显示 Page 0 的函数是 func_747,其调用 SetTimer 跑了一个反调试的 timer,每秒扫一次进程,kill 名字长得像调试器的,对于我们静态分析选手没有影响。

 

验证逻辑在按钮的 OnClick 函数里,如下,稍微手动整理一下之后也很清晰,其中 Bamer::P 之类的函数是在 Bamer.dll 里实现的,可以找一个 NSIS 插件的头文件对着看。

Function OnClick
  System::Call user32::GetWindowText(p$_1_,t.s,i1024)
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\System.dll
    ; SetDetailsPrint lastused
    ; Push user32::GetWindowText(p$_1_,t.s,i1024)
    ; CallInstDLL $PLUGINSDIR\System.dll Call
  Pop $0
  StrCpy $_3_ $0
  Bamer::P $0                  ;; Is Alphanumeric?
    ; Call Initialize_____Plugins
    ; AllowSkipFiles off
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $0
    ; CallInstDLL $PLUGINSDIR\Bamer.dll P
  Pop $R0
  StrCmp $R0 0 0 label_658
  Goto boom
label_658:
  StrLen $1 $0
  IntCmp $1 100 0 boom boom     ;; Input size should be 100
  Bamer::B $0 $1                ;; Modified Base64 decode
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $1
    ; Push $0
    ; CallInstDLL $PLUGINSDIR\Bamer.dll B
  Pop $2                        ;; $2 = decoded data
  StrCpy $3 $2 11 0             ;; $3 = first 11 char of decoded data
  Push $3
  Call CheckPart1               ;; Must return true
  Pop $R0
  StrCmp $R0 False 0 label_673
  Goto boom
label_673:
  Push $3
  Call func_125                 ;; $3 = $3 + "00010", pad 11 bytes key to 16 bytes
  Pop $3
  StrCpy $4 $2 64 11            ;; data[11:11+64]
  StrLen $R0 $4
  StrCmp $R0 64 label_680
  Goto boom
label_680:
  Bamer::A $4 64 $3             ;; ModifiedAESDecrypt(data[11:11+64], 64, $3)
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $3
    ; Push 64
    ; Push $4
    ; CallInstDLL $PLUGINSDIR\Bamer.dll A
  Pop $R0
  StrCmp $R0 0 0 label_690      ;; Must return 1
  Goto boom
label_690:
  Pop $R1                       ;; Decrypted data[11:11+64]
  StrCpy $5 $R1 11 0            ;; first 11 bytes
  StrCpy $_6_ $5
  Push $5
  Call func_133                 ;; CheckPart2First11Bytes, Answer: WelcomeHave
  Pop $R0
  StrCmp $R0 False 0 label_698
  Goto boom
label_698:
  StrCpy $6 $R1 53 11           ;; next 53 bytes
  Bamer::C 36 4 $6
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $6
    ; Push 4
    ; Push 36
    ; CallInstDLL $PLUGINSDIR\Bamer.dll C
  Pop $R2
  Push $_6_                    ;; $_6_ = "WelcomeHave"
  Call func_615
  Pop $5                       ;; $5 = "WelcomeToHaveFun" (len = 16)
  Bamer::G $5 $R2
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $R2
    ; Push $5
    ; CallInstDLL $PLUGINSDIR\Bamer.dll G
  Pop $R0
  StrCmp $R0 0 0 label_719
  Goto boom
label_719:
  Bamer::F $_3_                  ;; FinalCheck: FNVHash(entire_input) == 0x400D49F3
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $_3_
    ; CallInstDLL $PLUGINSDIR\Bamer.dll F
  Pop $R0
  StrCmp $R0 0 0 win
  Goto boom
win:
  StrLen $R1 XX+2IHcragE=          ;; You win!
  Bamer::B XX+2IHcragE= $R1
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $R1
    ; Push XX+2IHcragE=
    ; CallInstDLL $PLUGINSDIR\Bamer.dll B
  Pop $R2
  MessageBox MB_OK $R2
  Return

boom:
  StrLen $R1 U0JtakdiZX6wc1UxIR==
  Bamer::B U0JtakdiZX6wc1UxIR== $R1 ;; Wrong answer!
    ; Call Initialize_____Plugins
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $R1
    ; Push U0JtakdiZX6wc1UxIR==
    ; CallInstDLL $PLUGINSDIR\Bamer.dll B
  Pop $R2
  MessageBox MB_OK|MB_ICONINFORMATION $R2
FunctionEnd

Bamer 中除 G 以外的函数大致功能如下,分析过程略去,都是看看代码就能看出来的:

  1. P 是验证输入字符串是否仅由数字、字母构成。
  2. B 是一个魔改过的 Base64 decode,记某字符在原来的字母表对应的数值为 x,则新数值为 x ^ (x >> 4),简单处理一下即可解码。
  3. C 是进制转换,前两个参数分别为原进制和目标进制,最多支持 36 进制。超过 10 的部分输出时用 A-Z 来表示,但处理输入的时候大小写不敏感,即 A、a 均表示 10
  4. F 是检查输入串的 FNV-1a hash 是否等于 0x400D49F3。
  5. A 是一个略有修改的 Rijndael 算法的解密。修改了 Key Schedule 的过程、Key Schedule 得出的轮密钥在内存中的表示和将 ShiftRows 改为了 ShiftCols (即先转置再 ShiftRows 再转回来)。

可以看到在 NSIS 脚本中和插件中均有验证逻辑,梳理一下:

  1. 先判断输入是不是恰好100个字符,是不是仅由数字、字母构成。
  2. 将输入进行魔改 Base64 解码 (Bamer 中的 B 函数)。
  3. 调用一段 NSIS 脚本 (CheckPart1) 中的验证逻辑验证前 11 个字符。
  4. 通过后将前 11 个字符后面跟上 00010 作为密钥,用 Bamer 里的 A 函数解密接下来的 64 个字节。
  5. 调用另一段 NSIS 脚本 (func_133) 中的验证逻辑验证这 64 个字节中的前 11 个字节。
  6. 通过后将这 11 个字节胡乱扩充一下(第七个字符后面加上To,最后加上Fun)到 16 字节。
  7. 将输入里剩下的 53 字节进行 36 进制转 4 进制的操作。
  8. 将以上两步得到的字符串传入 Bamer 中的 G 函数进行验证。
  9. 若通过的话,再调用 Bamer 中的 F 函数验证一开始输入的 100 个字符的 FNV-1a hash 是否是特定值。
  10. 都通过的话,提示 You win!,否则提示 Wrong answer!

NSIS 脚本中的验证逻辑有两处,分别是:

Function CheckPart1
  Exch $R8
    ; Push $R8
    ; Exch
    ; Pop $R8
  StrLen $8 JTZmLD/8Sh6MOmd=
  Bamer::B JTZmLD/8Sh6MOmd= $8
    ; Call Initialize_____Plugins
    ; AllowSkipFiles on
    ; File $PLUGINSDIR\Bamer.dll
    ; SetDetailsPrint lastused
    ; Push $8
    ; Push JTZmLD/8Sh6MOmd=
    ; CallInstDLL $PLUGINSDIR\Bamer.dll B
  Pop $R9
  StrLen $R7 $R8
  IntCmp $R7 11 label_99 label_120 label_120 ;; input size must be 11
label_99:
  IntOp $R6 0 + 0 ;; $R6 is loop index
label_100:
  StrCpy $R1 $R8 1 $R6 ;; $R1 = input[$R6]
  Push $R1
  Call Ord
  Pop $R1
  IntOp $R2 $R1 ^ 0x17
  IntOp $R3 $R2 - $R6  ;; (input[i] ^ 0x17) - i should equal to predefined answer
  StrCpy $R4 $R9 1 $R6
  Push $R4
  Call Ord
  Pop $R4
  IntCmp $R3 $R4 0 label_120 label_120
  StrCmp $R6 10 0 label_113
  Goto label_115
label_113:
  IntOp $R6 $R6 + 1
  Goto label_100
label_115:
  StrCpy $0 True
  Exch $0
    ; Push $0
    ; Exch
    ; Pop $0
  Return

label_120:
  StrCpy $0 False
  Exch $0
    ; Push $0
    ; Exch
    ; Pop $0
FunctionEnd

这里就是简单的稍微写的别扭了一点的和固定串的比较,可以直接还原,答案是 2018TSCRCTF(腾讯的人要哭了,TSCR 是什么?)。

 

另一处是:

Function func_133
  Exch $0
    ; Push $0
    ; Exch
    ; Pop $0
  StrLen $1 $0
  IntCmp $1 11 0 label_fail label_fail
  StrCpy $1 $0 1 0
  StrCpy $2 $0 1 1
  StrCpy $3 $0 1 2
  StrCpy $4 $0 1 3
  StrCpy $5 $0 1 4
  StrCpy $6 $0 1 5
  StrCpy $7 $0 1 6
  StrCpy $8 $0 1 7
  StrCpy $9 $0 1 8
  StrCpy $_4_ $0 1 9
  StrCpy $_5_ $0 1 10
  Push $1
  Call Ord
  Pop $1
  Push $2
  Call Ord
  Pop $2
  Push $3
  Call Ord
  Pop $3
  Push $4
  Call Ord
  Pop $4
  Push $5
  Call Ord
  Pop $5
  Push $6
  Call Ord
  Pop $6
  Push $7
  Call Ord
  Pop $7
  Push $8
  Call Ord
  Pop $8
  Push $9
  Call Ord
  Pop $9
  Push $_4_
  Call Ord
  Pop $_4_
  Push $_5_
  Call Ord
  Pop $_5_
  IntOp $R2 $1 * 18334
  IntOp $R3 $2 * 19371
  IntOp $R2 $R2 + $R3
  IntOp $R4 $3 * 15568
  IntOp $R3 $4 * 19321
  IntOp $R4 $3 * 17784
  IntOp $R2 $R2 - $R4
  IntOp $R5 $R2 * 21534
  IntOp $R5 $4 * 21534
  IntOp $R3 $4 * 18321
  IntOp $R2 $R2 + $R5
  IntOp $R4 $R4 * 11321
  IntOp $R3 $9 * 16158
  IntOp $R6 $5 * 23633
  IntOp $R5 $_5_ * 18278
  IntOp $R7 $6 * 16027
  IntOp $R8 $7 * 18430
  IntOp $R2 $R2 + $R3
  IntOp $R4 $_4_ * 15917
  IntOp $R9 $8 * 24544
  IntOp $R2 $R2 - $R6
  IntOp $R2 $R2 + $R7
  IntOp $R3 $R3 * 25621
  IntOp $R2 $R2 + $R5
  IntOp $R2 $R2 - $R8
  IntOp $R5 $R2 * 33321
  IntOp $R2 $R2 + $R4
  IntOp $R4 $R3 * 25321
  IntOp $R2 $R2 - $R9
  IntOp $R3 $R2 * 12345
  IntOp $R2 $1 * 19292
  IntOp $R4 $3 * 17677
  IntOp $R5 $4 * 18327
  IntOp $R9 $8 * 20472
  IntOp $R6 $5 * 19344
  IntOp $R3 $2 * 21770
  IntOp $R7 $6 * 16593
  IntOp $R8 $7 * 20094
  IntOp $R2 $R2 - $R8
  IntOp $R2 $R2 + $R9
  IntOp $R2 $R2 + $R3
  IntOp $R2 $R2 + $R4
  IntOp $R2 $R2 - $R5
  IntOp $R2 $R2 + $R6
  IntOp $R2 $R2 + $R7
  IntOp $R3 $9 * 19029
  IntOp $R4 $_4_ * 16001
  IntOp $R5 $_5_ * 20980
  IntOp $R2 $R2 - $R3
  IntOp $R2 $R2 + $R4
  IntOp $R2 $R2 - $R5
  IntCmp $R2 5295553 0 label_fail label_fail
  ... 略 ...

乍一看吓人,仔细一看会发现是个线性方程组,可以解出答案是 WelcomeHave

 

考虑到要把输入喂回去,得能进行 Bamer::A 的反向操作(即加密),看一下其相比标准的 AES-128 改动的地方:

 

Key Schedule 的过程翻译成 Python 代码如下:

Sbox = (...) # Ordinary Rijndael S-Box

def xor(lhs, rhs):
  return tuple(a ^ b for a, b in zip(lhs, rhs))

def key_schedule(key):
  expanded = []
  expanded.extend(map(ord, key))
  for i in range(4, 4 * 11):
    t = expanded[(i-1)*4:i*4]
    if i % 4 == 0:
      t[3] ^= Sbox[t[3]]
      t[2] ^= (Sbox[t[2]] + 1) % 256
      t[0] ^= (Sbox[t[0]] + 2) % 256
      t[1] ^= (Sbox[t[1]] + 3) % 256
      t[0], t[1] = t[1], t[0]
      t = t[::-1]
      # t[3] ^= AES.Rcon[i // self.nk] & 0
    expanded.extend(xor(t, expanded[(i-4)*4:(i-4+1)*4]))
  return expanded

可以看到基本是改的面目全非,而且写在内存里的 layout 也是特意扭了一下,后面 AddRoundKey 的时候有专门处理,估计是为了防止别人直接把轮密钥 dump 出来用吧。另外这里面有个可能是 bug 的东西,Rcon and 上的值没有移到高位去,因此这里总是 0。将正常 layout 的轮密钥算出来备用,之后只要随便找一个实现改一下 ShiftRows 即可用来加密/解密:

# Dump in normal layout
roundkeys = map(chr, key_schedule('2018TSCRCTF00010'))
prepared_roundkeys = ''
for i in xrange(0, len(roundkeys), 16):
  SHUFFLE = [3, 2, 0, 1, 7, 6, 4, 5, 11, 10, 8, 9, 15, 14, 12, 13]
  prepared_roundkeys += ''.join([roundkeys[i+SHUFFLE[j]] for j in xrange(16)])

print 'unsigned char kRoundKeys[11][16] = {'
for i in xrange(0, len(prepared_roundkeys), 16):
  print '  {' + ', '.join(map(lambda x: '0x%02X' % ord(x), prepared_roundkeys[i:i+16])) + '},'
print '};'

接下来看 G 这个验证函数是在干啥,主要逻辑在 10002309 中。大致看一下会发现其先用传入的 16 字节作为 key 解密了 144 字节的数据:

...........B
.R...Y..A...
............
.....DG..R..
............
......B.S...
............
.......D....
.......P....
......Y.....
.......G..S.
.....P...A..

然后根据解密出来的数据生成了三个 8 字节的 buffer,接下来把输入的另一个四进制数当作“指令”开始进行以下操作:

  1. 将解密出的 144 字节当作 12x12 的棋盘。
  2. 三个 8 字节的 buffer 中的其中一个是 ABDGPRSY,另外两个是棋盘上对应的字母所在的两个坐标(高4位为x,低4位为y),不妨认为是起点和终点。
  3. 每次验证一个字母,从起点出发,读取“指令”,0 => y-1,1 => y+1,2 => x-1,3 => x+1,往对应方向走,走到的地方必须是空的,走到之后会填充上当前的字母,到终点结束。
  4. 棋盘最后必须填满。

总结一下,就是棋盘上的八对字母之间要连起来,连线不能交叉,最后必须占满整个棋盘。这听上去就像是一种非常经典的逻辑谜题,几乎可以肯定不是这个 CrackMe 的作者自己想出来的,也几乎可以肯定只要我们知道它叫什么,就能搜到现成的求解器。于是我们去看看 Nikoli 出版过的著名 logic puzzle 列表,翻着翻着可以翻到一种叫 Numberlink 的东西,看起来跟上面描述的规则一模一样。搜一下 "Numberlink solver",可以找到一个 跑的比香港记者还快的求解器 和一个 能告诉我们解的个数的求解器

解决

这里我们用前者试试:

$ cat inp_thomasahle.txt
12 12
...........B
.R...Y..A...
............
.....DG..R..
............
......B.S...
............
.......D....
.......P....
......Y.....
.......G..S.
.....P...A..
$ time solver/numberlink/bin/numberlink < inp_thomasahle.txt
12 12
PPPPPPPBBBBB
PRYYYYPBAAAA
PRYPPPPBSSSA
PRYPDDGBSRSA
PRYPDGGBSRSA
PRYPDGBBSRSA
PRYPDGGGGRSA
PRYPDDDDGRSA
PRYPPPPPGRSA
PRYYYYYGGRSA
PRRRRRRGRRSA
PPPPPPRRRAAA

solver/numberlink/bin/numberlink < inp_thomasahle.txt  0.00s user 0.00s system 113% cpu 0.002 total

果然跑的比香港记者还快,接下来只要简单写几行代码将这个解转换为上文提到的“指令”即可:

names = 'ABDGPRSY'
fromXY = [0x18, 0x0B, 0x35, 0x36, 0x87, 0x11, 0x58, 0x15]
toXY  = [0xB9, 0x56, 0x77, 0xA7, 0xB5, 0x39, 0xAA, 0x96]

def unpack(val):
  return (val >> 4, val & 0xF)

fromXY = map(unpack, fromXY)
toXY = map(unpack, toXY)

with open('solution.txt') as fp:
  board = fp.read().strip().split('\n')[1:]

vis = set()

answer = ''
for i in xrange(len(names)):
  ch = names[i]
  candidates = [fromXY[i]]
  while candidates:
    assert len(candidates) == 1
    x, y = candidates.pop()
    vis.add((x, y))
    for d, (dx, dy) in enumerate([(0, -1), (0, 1), (-1, 0), (1, 0)]):
      nx, ny = x + dx, y + dy
      if (nx, ny) in vis: continue
      if nx < 0 or nx >= 12 or ny < 0 or ny >= 12: continue
      if board[nx][ny] == ch:
        candidates.append((nx, ny))
        answer += str(d)
  assert (x, y) == toXY[i], 'Failed at {}, finished @ ({}, {})'.format(ch, x, y)

print answer

运行即可得到答案 1113333333333000000333330033331113033111333030000222222111220000003333333333311111333333333111113112122222222221133333333000333333331111。利用 Bamer.dll 自身将其转换为 36 进制,得到 32WSFUPIFV9TYJWWPH14NZZ85YDHXOLO37ATG4IYC4ZCDIKCA7EJ9
至此,我们已经得到构造序列号的所有要素了。

 

将之前得到的答案前面拼上 WelcomeHave,加密,前面再拼上 2018TSCRCTF,编码即可得到最后的序列号 MhAyOFQSR2JDUEb0OUopUQkh5Ax23nEnCLxoBT06JRd7EdLrwooWsXQG68wLcneAqDy3UU78AgdrYnabVL0M9vd852girNqF9a3F

多解

由于 Bamer 中的 C 函数对于输入的大小写不敏感,因此翻转作为上面的小游戏答案中的字母之后前面的验证依然都能过,但问题在于魔改 Base64 编码后必须没有 +/ 以及 FNV-1a hash 验证必须过。注意到小游戏答案中有 40 个可以翻转大小写的字母,因此这部分可能的解有 2 ** 40 个,不是很多,我们可以枚举一遍。由于 FNV-1a hash 有 32 位,假设它性质比较良好的话,不考虑 +/ 的问题最后应该能得到接近 256 个答案,写个程序试一试:

#include <bits/stdc++.h>
#include <x86intrin.h>

#include "chromiumbase64.h"

using namespace std;

constexpr int B64_INPUT_LENGTH = 75;
constexpr int B64_OUTPUT_LENGTH = 100;
constexpr unsigned int kExpectedHash = 0x400D49F3;
constexpr int kLetterOffset[] = {
  13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 25, 26, 27, 28, 31, 32, 33, 36, 37,
  38, 39, 40, 41, 42, 45, 46, 47, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 61, 62
};
const char kAnswerBase[] = "WelcomeHave32WSFUPIFV9TYJWWPH14NZZ85YDHXOLO37ATG4IYC4ZCDIKCA7EJ9";

unsigned char kRoundKeys[11][16] = {
  {0x38, 0x31, 0x32, 0x30, 0x52, 0x43, 0x54, 0x53, 0x30, 0x46, 0x43, 0x54, 0x30, 0x31, 0x30, 0x30},
  {0x0F, 0x07, 0x06, 0xC9, 0x5D, 0x44, 0x52, 0x9A, 0x6D, 0x02, 0x11, 0xCE, 0x5D, 0x33, 0x21, 0xFE},
  {0x4F, 0xD9, 0x17, 0x3E, 0x12, 0x9D, 0x45, 0xA4, 0x7F, 0x9F, 0x54, 0x6A, 0x22, 0xAC, 0x75, 0x94},
  {0xFE, 0x33, 0xA6, 0x00, 0xEC, 0xAE, 0xE3, 0xA4, 0x93, 0x31, 0xB7, 0xCE, 0xB1, 0x9D, 0xC2, 0x5A},
  {0x65, 0xD6, 0xDF, 0xC2, 0x89, 0x78, 0x3C, 0x66, 0x1A, 0x49, 0x8B, 0xA8, 0xAB, 0xD4, 0x49, 0xF2},
  {0x1B, 0xA2, 0x16, 0x5F, 0x92, 0xDA, 0x2A, 0x39, 0x88, 0x93, 0xA1, 0x91, 0x23, 0x47, 0xE8, 0x63},
  {0x86, 0xD7, 0x13, 0xB9, 0x14, 0x0D, 0x39, 0x80, 0x9C, 0x9E, 0x98, 0x11, 0xBF, 0xD9, 0x70, 0x72},
  {0xB7, 0xF4, 0xA4, 0x56, 0xA3, 0xF9, 0x9D, 0xD6, 0x3F, 0x67, 0x05, 0xC7, 0x80, 0xBE, 0x75, 0xB5},
  {0xDA, 0x1E, 0xE9, 0x47, 0x79, 0xE7, 0x74, 0x91, 0x46, 0x80, 0x71, 0x56, 0xC6, 0x3E, 0x04, 0xE3},
  {0x2D, 0xEE, 0x9B, 0xCA, 0x54, 0x09, 0xEF, 0x5B, 0x12, 0x89, 0x9E, 0x0D, 0xD4, 0xB7, 0x9A, 0xEE},
  {0xE8, 0xCE, 0x07, 0xD7, 0xBC, 0xC7, 0xE8, 0x8C, 0xAE, 0x4E, 0x76, 0x81, 0x7A, 0xF9, 0xEC, 0x6F}
};

inline void BrokenRijndaelEncryptSingleBlock(unsigned char* input, unsigned char* output)
{
  __m128i transpose_mask = _mm_setr_epi8(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15);
  __m128i shift_fix_mask = _mm_setr_epi8(0, 12, 8, 4, 5, 1, 13, 9, 10, 6, 2, 14, 15, 11, 7, 3);
  __m128i data = _mm_load_si128((const __m128i*) input);

  data = _mm_xor_si128(data, _mm_load_si128((const __m128i*) kRoundKeys[0]));
  #pragma unroll 9
  for (int i = 1; i < 10; i++) {
    data = _mm_shuffle_epi8(data, shift_fix_mask);
    data = _mm_aesenc_si128(data, _mm_load_si128((const __m128i*) kRoundKeys[i]));
  }
  data = _mm_shuffle_epi8(data, shift_fix_mask);
  data = _mm_aesenclast_si128(data, _mm_load_si128((const __m128i*) kRoundKeys[10]));

  _mm_store_si128((__m128i*)output, data);
}

void BrokenRijndaelEncrypt(void* data, size_t length)
{
  unsigned char* _data = (unsigned char*) data;
  for (size_t i = 0; i < length; i += 16) {
    BrokenRijndaelEncryptSingleBlock(_data + i, _data + i);
  }
}

unsigned int FNV1a(const void* data, size_t size)
{
  unsigned int h = 0x811C9DC5;
  const unsigned char* _data = (const unsigned char*)data;
  for (size_t i = 0; i < size; i++) {
    h ^= _data[i];
    h *= 0x1000193;
  }
  return h;
}

bool CheckAnswerMask(unsigned long long mask)
{
  char answer[64];
  char final_answer[80];
  char encoded[128];
  memcpy(answer, kAnswerBase, 64);
  for (unsigned long long i = 0; i < 40; i++) {
    if ((mask >> i) & 1) {
      // Flip case
      answer[kLetterOffset[i]] ^= 0x20;
    }
  }
  BrokenRijndaelEncrypt(answer, 64);
  memcpy(final_answer, "2018TSCRCTF", 11);
  memcpy(final_answer+11, answer, 64);
  chromium_base64_encode(encoded, final_answer, B64_INPUT_LENGTH);
  unsigned long h;
  h = FNV1a(encoded, B64_OUTPUT_LENGTH);
  bool okay = h == kExpectedHash;
  if (okay) {
    encoded[100] = 0;
    puts(encoded);
  }
  return okay;
}

int main(int argc, char* argv[])
{
  int id = 0;
  if (argc > 1) {
    id = atoi(argv[1]);
    fprintf(stderr, "Job %d\n", id);
  }
  unsigned long long step_size = (1ull << 32);
  for (unsigned long long mask = id * step_size; mask < id * step_size + step_size; mask++) {
    if ((mask & 0xFFFFFF) == 0) {
      fprintf(stderr, "Progress: %llu\n", mask);
    }
    CheckAnswerMask(mask);
  }
  return 0;
}

开上 256 个核,跑个 20 分钟即可得到所有解,共 237 个,去掉包含 +/ 的之后还剩下 24 个,如下,其中第一行的是“预期解",但其他的输入原来的程序也会得到 You win! 提示。

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

吐槽

  1. ntpd 是个好东西。
  2. 上面的暴力代码里跑的最慢的部分其实是算 FNV-1a hash……占总时间的 60~70%,按理说改成用 AVX2 一次算八个串的 hash 就能让它不再是瓶颈了,不过 10 倍以内的差距靠堆核都能解决……

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
免费 5
打赏
分享
最新回复 (3)
雪    币: 3003
活跃值: (464)
能力值: ( LV15,RANK:1395 )
在线值:
发帖
回帖
粉丝
lacoucou 12 2018-12-25 14:26
2
0
牛逼了!!!!
1.2018TSCRCTF  是因为换成 TSRC异或之后不是可见字符。
2.中间的进制转换并不是必须的,加这个东西只是为了压缩长度,没有想到造成了多解。
3.改版的base64和AES其实还是出自腾讯之手,如果你能猜到这个,估计用时更短。
4.最后的小游戏是微信小程序里边的连线世界。
5.大佬懂的真多。。。 。。。
雪    币: 6435
活跃值: (436)
能力值: ( LV12,RANK:831 )
在线值:
发帖
回帖
粉丝
Riatre 8 2018-12-25 23:36
3
0
_
最后于 2020-4-17 02:32 被Riatre编辑 ,原因:
雪    币: 10845
活跃值: (1054)
能力值: (RANK:190 )
在线值:
发帖
回帖
粉丝
看场雪 3 2018-12-27 22:27
4
0
在题目里设计个游戏 确实很好玩
学了 下次我也设计一个 一个没见过的
游客
登录 | 注册 方可回帖
返回