
引言
第三届字节跳动“安全范儿”高校挑战赛正式开启,赛事由字节跳动安全与风控团队发起并主办,分为ByteCTF和安全AI挑战赛两个赛道,同时有“安全范儿”沙龙辅助引领,全面促进高校网络安全新力量的挖掘、提升和激励。
ByteCTF 赛道的比赛形式为CTF解题赛,赛题包含Web、Pwn、Reversing、Crypto、Mobile、Misc等多种题型,赛题与字节跳动产品业务相结合,给同学们营造字节跳动攻防场景的沉浸式体验。
线上初赛时间:2021年10月16日-17日
发现好久没水博客了,之前有几篇咕掉了,喵喵要上课要搬砖啊,喵呜……
噢,这周末 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.top的dns隧道流量。

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.1,o和i的返回a记录都是10.0.0.2。

单独看 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 都是一样的,提取一份出来。

每个方块占 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)))

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 也出来了,好耶!
可恶的部分 wp,由于线下赛还有所以初赛这里不放 wp 了???
(溜了溜了喵
