引言
第三届字节跳动“安全范儿”高校挑战赛正式开启,赛事由字节跳动安全与风控团队发起并主办,分为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 了???
(溜了溜了喵