D^3CTF 2022 Summary

这次比赛又莫名其妙打到两点多出了个题,不过体验还是不错的,尤其是蛮让人长见识。几道题做下来至少知道了两三个以往没有注意的知识点。

BadW4ter

「Dive into」the w3ter, deeper and deeper.

源文件名字为 WATER - xxx.wav,但是却无法直接打开为音频文件,拖进 010Editor,发现有奇奇怪怪的文件头,使用 file 也只能得到 data 的反馈。

但是又看到了后文的数据排布、data出现的位置,找来一个 wav 文件对比后,可以修改一下文件头,修改后便可以成功打开了,是一首听起来不错的曲子,后经交流得知这首曲子就是屁股肉的歌((

之后使用 silenteye 并没有得到什么信息,又从别的地方看不到什么 hint,卡了蛮久,于是回过头去看了看题目描述,deeper and deeper,于是又搜了搜相关的隐写,找到了 DeepSound

音频放进去,果然是有东西的,那密码是什么呢……想起来被篡改的文件头:CUY1nw31lai ——初音未来,以此作为密钥果然解密出了一个 flag.png

直接扫描出来的结果是 never gonna give you up...使用 VScode 无法正常打开此 png 图片,有所怀疑文件格式……而且文件像素中有些灰色,怀疑有猫腻……于是用 file 尝试:

$ file .\flag.png
.\flag.png: TIFF image data, little-endian, direntries=20, height=300, bps=254, compression=none, PhotometricIntepretation=RGB, width=300

但是无论用什么工具查看像素,这个里面的隐写总是让人摸不到头脑。于是又转而关注了 TIFF 这个文件格式:

使用无损格式存储图像的能力使TIFF文件成为图像存档的有效方法。与JPEG不同,TIFF文件可以编辑然后重新存储而不会有压缩损失。其它的一些TIFF文件选项包括多层或者多页

难不成这图片是多层的?于是拖进了 PS ……

果然,下面一层的是可以去除的,灰度+透明度实现的黑白背景下不同呈现效果的图片早在 hackergame 2019 的白与夜就见过,我也用此做过一些谜题,于是直接换背景,得到 flag。

D3CTF{M1r@9e_T@nK_1s_Om0sh1roiii1111!!!!!Isn't_1t?}

OHHHH!!! SPF!!!

Server: xxx.xxx.xxx.xxx

  • L2TP Tunnel
    User: D3CTF
    Password: AFZc……
    IPsec Secret: M99i……

Create a L2TP tunnel and launch a OSPFv3 Instance to get flag
Server OS: RouterOS v6.49.4 CHR

要建立隧道的话就打算直接也装个 RouterOS 了,毕竟镜像也不大,也就 20MB 左右,可以从这里下载到。然后用 VMware 安装,为了方便就直接选了全部功能。

打开后要根据 VMwareVnet 进行相关配置,我这里的网段是 192.168.231.0/24 网关是 192.168.231.2,这些需要配置好,否则 RouterOS 是无法访问外部网络的。

具体的配置文档可以参照官网,此处不再赘述。另外默认密码是空密码……有点坑人。

而后按照所给配置连接 L2TP 隧道,并且配置下 OSPFv3。可以使用 winbox 进行连接,而且使用 IP 连接似乎要比使用硬件地址连接稳定很多。

由于 OSPFv3 是一种路由协议,于是它传输的信息——也就是 flag 理应藏在路由表里……怎么藏呢?也就是这里奇奇怪怪的 ipv6 路由了。

最开始其实在想会不会是在日志等信息里,翻看了二十多次这几条 ipv6 的路由而浑然不知。直到突然想到会不会是 v6 地址作为 bytes,然后检查了一下 d3ctf 的字节,发现正好在里面,于是也就通过它解码了:

 # DST-ADDRESS                                 STATE          COST
 0 2053:6134:406c:7574:6520:536f:6861:2120/128 intra-area     20
 1 b9a7:6433:6374:667b:3472:655f:794f:b7a2/128 intra-area     20
 2 ccf4:6d40:3574:3352:5f69:4e5f:5930:c1cb/128 intra-area     20
 3 cfb2:755f:615f:6e33:7477:3052:6b5f:cfd6/128 intra-area     20
 4 d5bd:7572:5f37:3361:4d5f:7748:6f5f:c3d8/128 intra-area     20
 5 d5df:6b6e:3077:355f:3073:7046:3f7d:c3dc/128 intra-area     20

>>> data = bytes.fromhex('20536134406c75746520536f68612120b9a764336374667b3472655f794fb7a2ccf46d40357433525f694e5f5930c1cbcfb2755f615f6e33747730526b5fcfd6d5bd75725f3733614d5f77486f5fc3d8d5df6b6e3077355f307370463f7dc3dc')
>>> bytes([i for i in data if int(i) < 128])
b' Sa4@lute Soha! d3ctf{4re_yOm@5t3R_iN_Y0u_a_n3tw0Rk_ur_73aM_wHo_kn0w5_0spF?}'

大概是直接解码不行的,细细一看这些地址是乱序的,于是把每一条中的可用信息先提取出来,按照语意进行拼接,得到结果:

b' Sa4@lute Soha! '
b'\xb9\xa7d3ctf{4re_yO\xb7\xa2' => 'd3ctf{4re_yO'
b'\xcc\xf4m@5t3R_iN_Y0\xc1\xcb' => 'm@5t3R_iN_Y0'
b'\xcf\xb2u_a_n3tw0Rk_\xcf\xd6' => 'u_a_n3tw0Rk_'
b'\xd5\xbdur_73aM_wHo_\xc3\xd8' => 'ur_73aM_wHo_'
b'\xd5\xdfkn0w5_0spF?}\xc3\xdc' => 'kn0w5_0spF?}'

PS: 如上的部分字节可以通过 GBK 解码出中文(

b'\xb9\xa7d3ctf{4re_yO\xb7\xa2' => '恭d3ctf{4re_yO发'
b'\xcf\xb2u_a_n3tw0Rk_\xcf\xd6' => '喜u_a_n3tw0Rk_现'
b'\xcc\xf4m@5t3R_iN_Y0\xc1\xcb' => '挑m@5t3R_iN_Y0了'
b'\xd5\xbdur_73aM_wHo_\xc3\xd8' => '战ur_73aM_wHo_秘'
b'\xd5\xdfkn0w5_0spF?}\xc3\xdc' => '者kn0w5_0spF?}密'
b' Sa4@lute Soha! '

d3ctf{4re_yOu_a_n3tw0Rk_m@5t3R_iN_Y0ur_73aM_wHo_kn0w5_0spF?}

d3thon

这是道基于 python 的逆向题,也就是因为这个才让我有信心进一步进行。

可以看到的是题目引用了一个 byte_analizer.so,这是由 python 编译得到的,类似于 pyd 文件,我在搜索中无意看到了 [原创]python编译后的pyd爆破 这篇文章,也提供了一些帮助。

直接逆汇编、用IDA这一套我是不太擅长的,于是先用 vscode 起个动态调试,断点打在输入之后的位置:

可以看到有 Functions 和 Variables 的字典。进一步对输入进行测试,当输入一些字符的时候发现:

因此可以推断,文件中的代码是将输入转换为二进制,并在最后将其转换为一个整数。结合这些推断,可以得出如下的符号表:

  • ZOAmcoLkGlAXXqf: 定义函数
  • RDDDZUiIKbxCubJEN: 执行函数
  • kZslMZYnvPBwgdCz: 执行输出
  • oGwDokoxZgoeViFcAF: 定义变量
  • OuGFUKNGxNLeHOudCK: 比较判断,进行分支
  • todeVDuRkYSIITaT: 转换对应ascii码的字符到其二进制
  • ……

得知这些之后我们可以通过如下的形式进行 okokokok 内函数的尝试:

...
ZOAmcoLkGlAXXqf:test:['kuhisCvwaXWfqCs:flag']
RDDDZUiIKbxCubJEN:start
RDDDZUiIKbxCubJEN:okokok
RDDDZUiIKbxCubJEN:test

在做测试时候可以用一些大一些的数字来鉴别异或和加减法,并结合输出代码进行函数类型的推断:

with open("bcode.lbc", "r") as fi:
  statmts = fi.read().split("\n")
  for i in statmts:
    ba.analize(i)
    if 'flag' in ba.Variables:
      print(ba.Variables)

最后可以得出如下的函数对照表,对 okokokok 内部进行格式化及字符串替换后,便可通过程序进行 flag 的反解了:

  • kuhisCvwaXWfqCs : not , x = ~x
  • IEKMEDdrPpzpdKy : add, x += y
  • OcKUQCYqhwHXfAgGZH : xor, x ^= y
  • FLNPsiCIvICFtzpUAR : sub, x -= y
flag = -194952731925593882593246917508862867371733438849523064153861650948471779982880938

statms = eval(open('encode.lbc', 'r').read())[::-1]

for s in statms:
    if s.startswith('sub'):
        operand = int(s[9:])
        flag += operand
    elif s.startswith('add'):
        operand = int(s[9:])
        flag -= operand
    elif s.startswith('xor'):
        operand = int(s[9:])
        flag ^= operand
    elif s.startswith('not'):
        flag = ~flag


allow = '23456789abcdefghijklmnopqrstuvwxyz'
flag = '00' + bin(flag)[2:] # 这里的补 0 是测试后的结果,可以发现有六位二进制在开头,无法被正常解出

result = ''

while len(flag) > 0:
    a = flag[:8]
    if chr(int(a, 2)) in allow:
        result += chr(int(a, 2))
        flag = flag[8:]
    else:
        result += flag[0]
        flag = flag[1:]

print(f'd3ctf{{{result}}}')

d3ctf{4729a4a6bbdd4d78c94e6229257af35e}