CTF | 2021 ByteCTF Misc Writeup


引言

第三届字节跳动“安全范儿”高校挑战赛正式开启,赛事由字节跳动安全与风控团队发起并主办,分为ByteCTF和安全AI挑战赛两个赛道,同时有“安全范儿”沙龙辅助引领,全面促进高校网络安全新力量的挖掘、提升和激励。

ByteCTF 赛道的比赛形式为CTF解题赛,赛题包含Web、Pwn、Reversing、Crypto、Mobile、Misc等多种题型,赛题与字节跳动产品业务相结合,给同学们营造字节跳动攻防场景的沉浸式体验。

线上初赛时间:2021年10月16日-17日

https://2021bytectf.xctf.org.cn/

发现好久没水博客了,之前有几篇咕掉了,喵喵要上课要搬砖啊,喵呜……

噢,这周末 ByteCTF 来了,那就来摸鱼打一打吧!

这篇就写几题 Misc 方向的吧,有的还是赛后复现的,这比赛太顶啦!

Checkin

字节跳动安全系列活动主题名字是什么?你造吗?关注【字节跳动安全中心】公众号并回复本次大赛主题(4字),会有意外惊喜!

发 安全范儿

Survey

填问卷

HearingNotBelieving

Hearing is not believing

https://2021bytectf-g.xctf.org.cn/media/uploads/task/a1fe791c76c245279317da2230fdc639.zip

解压出来一个 wav,一看频谱,很明显前半段有个二维码。

拼成一个二维码,然后手撸一下

m4yB3_

后半段是 SSTV,又想到 喵喵新年解谜闯关 出过一题包含 SSTV 的了(

一共有好几张图,拼成一张图,里面有个二维码

然后再手撸一下……

U_kn0W_S57V}

得到 flag

ByteCTF{m4yB3_U_kn0W_S57V}

frequently

Someone wants to send secret information through a surreptitious channel. Could you intercept their communications?

https://2021bytectf-g.xctf.org.cn/media/uploads/task/27bf98ffacef41d6b605fe2534b0a2ab.zip

一个流量包

这堆 dns 查询的域名感觉有戏

找一下发现.bytedanec.topdns隧道流量。

dns && dns.qry.name contains "bytedanec.top" && ip.src == 10.2.173.238

有戏了!

这个导出 json

import base64
import json


with open('dns.json', 'r', encoding='utf-8') as fin:
    s = fin.read()

data_raw0 = json.loads(s)


data_raw = sorted(
    data_raw0, key=lambda x: x['_source']['layers']['frame']['frame.time_epoch'])


data_extract = ''
for d in data_raw:
    queries = d['_source']['layers']['dns']['Queries']
    for i in queries:
        x = queries[i]["dns.qry.name"].split('.')[0]
        print(x)
        if x not in 'io':
            data_extract += x
print(data_extract)


data_decode = base64.b64decode(data_extract)
with open('decode.png', 'wb') as fout:
    fout.write(data_decode)

(后来发现还不如直接导出 CSV,然后删除冗余信息,复制粘贴到 CyberChef 处理。。

然后发现改来改去都不对,这解析出来的 png 图片怎么都是损坏的,虽然说勉强能看到点东西,但感觉哪里锅了啊……

后来才想到应该是有丢包导致重传,所以源地址为发包机器的时候可能会存在冗余的,那就换成 8.8.8.8 返回的就好了吧。

dns && dns.qry.name contains "bytedanec.top" && ip.src == 8.8.8.8

出来也不大对劲,但能大概看到说 You find the DNS tunnel.

然后队友说去重之后出了正常的图……

就是比如这种 Transaction ID 相同的,很明显是重复的了。

或者也可以拿 tshark 导出,然后匹配字符串来处理

tshark -T fields -r frequently.pcap -e dns.qry.name -e dns.id > 1.txt

观察发现base64编码的数据返回的a记录都是10.0.0.1oi的返回a记录都是10.0.0.2

io

单独看 i o

提取o i,发现刚好为 360 个,转为 01 然后转 ascii 得到前半段 flag

dns && dns.qry.name matches "^[io].bytedanec.top" && ip.src == 8.8.8.8

The first part of flag: ByteCTF{^_^enJ0y&y0ur

后半段 udp.stream eq 1 得到另一半 flag

最后得到

ByteCTF{^_^enJ0y&y0urse1f_wIth_m1sc^_^}

Lost Excel

Please find out who leaked this document asap

https://2021bytectf-g.xctf.org.cn/media/uploads/task/41f3f4157b1a43ebb7baf662fc9b5604.zip

HInt: Block size = 8. Notice repeating patterns.

Excel 里的这个背景图片有猫腻。

LSB 有奇奇怪怪的隐写。

R0 G0 B0 都是一样的,提取一份出来。

red0

每个方块占 4*4 pixel,很明显图里的信息是冗余的,或者说是有大量相同规律重复的色块。

比赛的时候盲猜这是 0 1 二进制编码,然而导出来发现啥也不是。

后来咱都想不出来这是啥了……

赛后问了其他师傅,说是四进制编码,绝了。难道他每个方块占 4*4 pixel 有这个提示作用吗(

噢,Block size = 8 指的是 8pixel 啊!

也就是说类似于下面这样,8*8pixel 作为一个小格子,每个格子里只存在5种情况:

  • 全空

  • 左上一个 => 00

  • 右上一个 => 01

  • 左下一个 => 10

  • 右下一个 => 11

根据位置按照四进制进行编码。

Exp:

这里试了试发现得按照从左到右先读第一行,然后再从上到下读第二行这样。

from Crypto.Util.number import long_to_bytes
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

img = Image.open('red0.png')
# img.show()
img.size
# (615, 607)

data = []
result = ''
for j in range(0, img.size[1], 8):
    for i in range(0, img.size[0], 8):
        pixels = [img.getpixel((i, j)), img.getpixel(
            (i+4, j)), img.getpixel((i, j+4)), img.getpixel((i+4, j+4))]
        # print(pixels)
        s = ''.join([str(x[0]//255) for x in pixels])
        # '0010'.index('1') ==> 2
        if '1' in s:
            idx = s.index('1')
            result += str(idx)
        data.append(s)
# print(data)
print()
print(result)

print(long_to_bytes(int(result, 4)))

flag

ByteCTF{ExcelHiddenWM}

BabyShark

https://2021bytectf-g.xctf.org.cn/media/uploads/task/cb5a835747374f52920e5878de657406.zip

(这题喵喵看了看不会做,赛后来复现一下

又是个流量包,刚开始看这一堆 Windowsupdate,盲猜这堆 url 编码里面藏着啥东西。

但队友试了发现没啥东西。

后来发现 tcp.stream eq 0 是一个 adb install xxx.apk 的流量,可以把 apk 抠出来。

根据 ADB 的数据包格式,需要把 WRTE 相关的部分给去除掉。

另外也找到了 ACTF Misc300 抓包 一道题和这部分比较类似,下面是他的官方 wp:

https://blog.flanker017.me/wp-content/uploads/2014/04/misc300-official-writeup.pdf

也可以参考 ADB Protocol Documentation (Better documentation of the ADB protocol, specifically for USB uses.)

总之就是要去掉这 24bytes 的数据,提取 payload 部分。

(当然也可以试试上面 wp 里的脚本

虽然文件可能还有点问题,但也能把这个 dex 给解压出来了。

然后直接进行一个逆向。

package com.bytectf.misc1;

import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.Environment;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy.Builder;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import dalvik.system.DexClassLoader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class MainActivity
  extends AppCompatActivity
{
  public long getAESKey(byte[] paramArrayOfByte, Method paramMethod)
  {
    try
    {
      paramArrayOfByte = paramMethod.invoke(null, new Object[] { paramArrayOfByte });
      long l = ((Long)paramArrayOfByte.getClass().getMethod("getKey", new Class[0]).invoke(paramArrayOfByte, new Object[0])).longValue();
      return l;
    }
    catch (Exception paramArrayOfByte)
    {
      paramArrayOfByte.printStackTrace();
    }
    return -1L;
  }
  
  public String getPBClass()
  {
    String str = "";
    if (ActivityCompat.checkSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE") != 0) {
      ActivityCompat.requestPermissions(this, new String[] { "android.permission.READ_EXTERNAL_STORAGE" }, 1);
    } else if (ActivityCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") != 0) {
      ActivityCompat.requestPermissions(this, new String[] { "android.permission.WRITE_EXTERNAL_STORAGE" }, 1);
    } else if (Environment.getExternalStorageState().equals("mounted")) {
      str = Environment.getExternalStorageDirectory().getAbsolutePath() + "/PBClass.dex";
    }
    return str;
  }
  public byte[] getPBResp()
  {
    Object localObject1 = new byte[0];
    OkHttpClient localOkHttpClient = new OkHttpClient();
    Object localObject2 = new Request.Builder().url("http://192.168.2.247:5000/api").build();
    try
    {
      localObject2 = localOkHttpClient.newCall((Request)localObject2).execute().body().bytes();
      localObject1 = localObject2;
    }
    catch (IOException localIOException)
    {
      localIOException.printStackTrace();
    }
    return (byte[])localObject1;
  }
  
  public Class loadPBClass(String paramString)
  {
    File localFile = getDir("dex", 0);
    paramString = new DexClassLoader(new File(paramString).getAbsolutePath(), localFile.getAbsolutePath(), null, getClassLoader());
    try
    {
      paramString = paramString.loadClass("com.bytectf.misc1.KeyPB").getClasses()[0];
      return paramString;
    }
    catch (Exception paramString)
    {
      paramString.printStackTrace();
    }
    return null;
  }
  
  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2131427356);
    if (Build.VERSION.SDK_INT > 9) {
      StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());
    }
    paramBundle = loadPBClass(getPBClass()).getMethods()[37];
    AesUtil.decrypt(getAESKey(getPBResp(), paramBundle), "8939AA47D35006FB2B5FBDB9A810B25294B5D4D76E4204D33BA01F7B3F9D99B1");
  }
}

大概的逻辑是:

  • Environment.getExternalStorageDirectory().getAbsolutePath() + "/PBClass.dex" 这个文件里加载类 loadClass("com.bytectf.misc1.KeyPB")
  • 请求 http://192.168.2.247:5000/api 这个接口,获得一段 bytes
  • 利用这段 bytes,结合这个类里的方法进行一些处理,获得 AES Key(getAESKey 函数)
  • 利用 AES Key 解密 8939AA47D35006FB2B5FBDB9A810B25294B5D4D76E4204D33BA01F7B3F9D99B1

于是最后的结果应该就是 flag 了。

那现在就去找这段 bytes,直接看流量里的 /api 部分就完事了。

088bb7bdf5dbd53711a1f831e6d61cc840

其实当时队友已经做到这里了,然而并不知道这个 PB 是啥玩意。。

赛后才知道是 protobuf,看人家队伍经典 fuzz……(摊手

又想到了 CISCN 2021 初赛的 tiny traffic 那题,也接触了 protobuf

然后丢去 赛博厨子 解密一下

得到俩数字

{
    "1": 244837809871755,
    "2": 11671133301835090000
}

根据源码可以看出这个就是 AesUtil.decrypt 函数的参数 paramLong,于是应该取的是 244837809871755

另外参考官方 wp,也可以用 python 库blackboxprotobuf进行解析。

data = b'\x08\x8b\xb7\xbd\xf5\xdb\xd5\x37\x11\xa1\xf8\x31\xe6\xd6\x1c\xc8\x40'
blackboxprotobuf.decode_message(data)
>>>
({'1': 244837809871755, '2': 4668012723080132769},
 {'1': {'type': 'int', 'name': ''}, '2': {'type': 'fixed64', 'name': ''}})

然后回去解密就完事了。

AES/CFB/NoPadding

(可恶,怎么你们都会 Java 啊,呜呜呜

import com.bytectf.misc1.AesUtil;

public class exp {
    public static void main(final String[] args) {
        final String flag = AesUtil.decrypt(244837809871755L,
                "8939AA47D35006FB2B5FBDB9A810B25294B5D4D76E4204D33BA01F7B3F9D99B1");
        System.out.println(flag);
    }
}

就这样吧(

小结

字节和心脏只有一个能跳动(

太顶了啊!

有意思的是周六去给新生做 CTF 宣讲,喵喵现场就在看这个 CTF,23333。

最后喵喵被队友带飞,咱进前十了啊!

看咱有没有机会去线下决赛旅游了

(线下推迟了,疫情好烦啊,喵呜呜

官方 writeup 也出来了,好耶!

2021 ByteCTF 初赛部分题目官方Writeup

可恶的部分 wp,由于线下赛还有所以初赛这里不放 wp 了???

(溜了溜了喵


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录