HGAME 2021 Week 1 Writeup
头一次正式参加CTF
Web
第一周的题目目前看上去还好,HTTP请求走私是新学到的
有些过程实在很难回忆起来了
Hitchhiking_in_the_Galaxy
点开题目页面,看到我要搭顺风车点进去发现没反应,浏览器一看302,那果断Postman请求或者Fiddler抓包走起
方便起见,这里用Postman,注意,需要把Settings
选项卡下的Automaticially follow redirects
关了,不跟进302跳转
回显405 Method Not Allowed
1 | <html> |
猜想使用POST,返回
1 | 只有使用"无限非概率引擎"(Infinite Improbability Drive)才能访问这里~ |
然后就stuck了,甚至百度了下这个无限非概率引擎
,后来突然想到是不是可以改UA,将UA改成Infinite Improbability Drive
POST提交
1 | 你知道吗?<a href="https://github.com/wuhan005">茄子</a>特别要求:你得从他的<a href="https://cardinal.ink/">Cardinal</a>过来 |
尝试添加Referer头Referer: https://cardinal.ink/
,POST提交
1 | flag仅能通过本地访问获得 |
然后甚至想到了SSRF,week1就上SSRF,我打的怕是HCTF?
后来想着要不加X-Forwarded-For
头试一下(其实是有合理原因的,因为返回405
的那个地方提示存在nginx
反代)
直接拿到flaghgame{s3Cret_0f_HitCHhiking_in_the_GAl@xy_i5_dOnT_p@nic!}
watermelon
托司机的大西瓜,u1s1,这游戏挺好玩的
PC上显示直接不全,没有那个到2000分拿flag
的hint,一开始我以为分数直接就是写在DOM里的,后来发现Cocos引擎直接在Canvas上画东西,前端一窍不通,没了解过,不知道该在哪下断,甚至开始觉得考察的是Cocos游戏的引擎的bug,我打的怕是HCTF?
这题学到Cocos引擎的前端主要业务代码在project.js
里
这种题预期解应该是改分数,随便玩了几下,没发现有发什么请求,猜测分数是存本地的
当时我简单看了下,在游戏开始加载MainJS
时下断改GameConfig.gameScore
无效,直接stuck
搜索gameScore
这个字符串,一个个跟进,发现有一个地方很可疑,在project.js
的2087-2092
行
1 | gameOverShowText: function (e, t) { |
判断得分e
是否大于1999,如果大于就弹个框,window.atob
函数是base64 decode,得到flaghgame{do_you_know_cocos_game?}
写wp时想到或许可以通过js的反射进作用域或者暴露一个GameConfig
的引用,但不知道js的=
是不是引用传递
还好gameScore
没被字符串加密,混淆或者JSFuck之类的,要不然这题估计没戏,我的静态分析能力实在是有待提高,这题基本是碰巧做出来的
宝藏走私者 走私者的愤怒
这题估计师傅们一看就知道是HTTP请求走私
,因为题目标题,ATS服务器,和DOM中的CDN
1 | <head> |
后面想基本上已经算是明示了
但是拿到这个题的时候没听说过这个技巧,于是开始看请求
浏览器访问/secret
返回了这个
1 | ONLY LOCALHOST(127.0.0.1) CAN ACCESS THE SECRET_DATA! |
尝试Postman请求加个Client-IP
头
1 | ONLY LOCALHOST(127.0.0.1) CAN ACCESS THE SECRET_DATA!<br>YOUR Client-IP([CURRENT IP]) IS NOT ALLOWED! |
发现并没有什么用,于是开始想是不是考察服务器本身的问题,因为返回头里有一个之前没见过的Server: ATS/7.1.2
于是百度,偶然发现一篇D3CTF的wp里提到了这个服务有HTTP请求走私的问题,再一看题目标题,答案就已经很显然了(此处自行脑补高考出场接受采访表情包,虽然我跟别人差远了)
简单来说就是反代和后端服务器处理请求的方式不同,而HTTP协议的解析依赖换行\r\n
,导致原本请求的Body被当成了请求头
具体的东西我其实也没完全搞明白,根据协议层的攻击:HTTP请求走私这篇文章,这题是CL-TE
的情况
这种情况的利用要求不正确的Content-Length
,Postman已经不够用了,祭出许久没用的Burp,抓包后丢进Repeater里修改
这里有坑,Burp默认也会帮你计算Content-Length
,需要手动取消勾选菜单栏的Repeater
->Update Content-Length
项
payload里的host甚至都是原题,当时懒得改
1 | GET /secret HTTP/1.1 |
注意,chunked
模式要求一个chunk
的结束要用\r\n\r\n0\r\n\r\n
来标识,这是一个坑,建议打开Burp的\n
按钮确认一下
最后快速点击两次Send,发送请求,得到flag
1 | WELCOME LOCALHOST. HERE IS THE SECRET:<br>hgame{Fe3l^tHe~4N9eR+oF_5mu9gl3r!!} |
后面Liki姐姐更新的题目走私者的愤怒
也是同样的考点,payload甚至都通用
后来又看到一篇文章,HTTP协议的解析依赖换行\r\n
这个细节还引出了另一个技巧,说反弹xss的时候也可以根据\r\n
来截断请求头,从而避免了不可控的跳转,或者加入自己想要的请求头比如CSP策略等等,最后直接加两个\r\n
让后面的内容均被解析成Body
智商检测鸡
一开始以为跟watermelon
那道题差不多,因为看到了fuckmath.js
里
1 | function getFlag(){ |
直接访问,然后反手就被嘲讽了
1 | {"flag":"\u9898\u90fd\u505a\u4e0d\u5b8c\u8fd8\u60f3\u8981Flag?\u5927\u5b66\u751f\u5c31\u8fd9\u5c31\u8fd9\u5c31\u8fd9\u5c31\u8fd9\uff1f"} |
Unicode解码得到题都做不完还想要Flag?大学生就这就这就这就这?
,意识到后端有判断,只能先尝试做题了
考察的是经典爬虫题
但是问题来了,外院学生不学高数,我连这种最简单ax+b
的定积分都不会算
百度了一圈发现scipy
这个python库提供了这个功能,无奈只能抛弃用了很久的gorequest
,改用弱类型语言(弱类型语言类型坑真的多)
1 | def func(x): |
python我只停留在调调库的阶段,所以并不会写闭包,bs4也是对着文档整的,没有goquery
用的方便
常数a,b
以及上下界down
和up
都是全局变量,写的很丑
1 | import math |
这里手动解析html会保险一些,因为上回李哥出了道eval()
函数造成的关机题,真实环境甚至有可能被SQL注入或者直接nc弹shell,清数据,取证之后律师函直接面向监狱编程
后来biubiubiu
学长提供了个更好的库sympy
,能更直观的支持多个未知量的情况
回想了下自己高考拿微积分基本定理跟别人现的黑历史,发现ax+b
的积分形式不就是(a/2)x^2+bx
嘛,还是可以用go实现的
Re
前面提到了,静态分析是我的弱项,很多技巧都比较局限,一旦题目复杂了或者反调一多我只能GG
感谢r3n0
和mezone
学长
apacha
这个题目一开始没去碰,因为底下有个helloRe
,看标题觉得比较简单(不要学,easyRE
这种标题的题目通常都是比较难的)
后来直接上了findcrypt
这个插件,插件在github上,这个插件是mezone
学长介绍给我用的,给了我这种不了解算法的解简单题的可能,感谢
clone下来之后把findcrypt3.py
和对应的findcrypt3.rules
丢进IDA安装目录/plugins
文件夹里
一下就识别出在函数sub_1447
中有TEA算法
我一开始没直接跟进sub_1447
函数,而是去看后面的部分,main
函数最后调用了sub_1550
判断加密后的flag与密文是否相等
这里IDA对密文数组的识别因为有一个开头DWORD比较出了点问题,并不是很直观
1 | __int64 __fastcall sub_1550(_DWORD *a1, int a2) |
实际上0x501c+0x4=0x5020
,所以从0x5020
到0x50ab
这140长度的内容都是密文
打开IDA View,用Array
将这些数据认为是数组
我直接dump成一个txt,长度140,刚好符合
1 | 23 B3 4E E7 36 28 A7 B7 E2 6F CA 59 C1 C5 7C 96 |
然后回到sub_1447
函数,TEA一般有这么几个特征:一个循环,特定的DELTA=0x9E3779B9
(变形0x61C88647
)然后循环中间两个看上去很长的位运算
但是sub_1447
函数显然不符合,有两个循环,然后又看到一个常数0x4AB325AA
和一些52
相关的除法运算
根据网上资料TEA系列加解密算法详解猜测是XXTEA
拿C复制网上的解密函数,直接乱码,开始和r3n0
学长进行交 流
确认了这是标准的XXTEA
后直接stuck,还是r3n0
学长提醒我注意这个比较函数,这里涉及到字节序的问题,linux编译的ELF是little
的,简单讲就是和你的直觉反着来,也让我发现了我对_DWORD
的定义有问题,长度应该是4个字节而不是8个
理了一遍程序逻辑
输入flag
每隔4个字节填充一个原文
1 | v3 = malloc(140uLL); |
丢进xxtea,密钥{1,2,3,4},
little
编码和密文逐4个字节比较
后来放弃了C的脚本,直接转python,xxtea
库一把梭
1 | import os |
解密出来因为有填充,会带一堆\x00
,直接替换,得到flaghgame{l00ks_1ike_y0u_f0Und_th3_t34}
有的时候真的挺佩服那些算法好的不用findcrypt也能做题,甚至能直接手写逆向算法,tql
helloRe
mezone
学长的题,这题估计用的是非预期解,直接patch了do{}while
循环里的jnz
指令->jz
指令,这样异或按位与密文比较的错误时候就不会退出,直接IDA动态调试,再一次暴露我的静态分析能力,里面一些跟时间相关的函数根本看不懂
上来开IDA,一开始没发现什么,findcrypt也没发现什么,看了下main函数里面有个循环
1 | do |
发现是经典异或,密文在byte_140003480
里
1 | 97 99 9C 91 9E 81 91 9D 9B 9A 9A AB 81 97 AE 80 83 8F 94 89 99 97 00 00 |
这个密文实际只有22位,IDA的数组识别日常出问题
key通过sub_140001430
得到,但是这个函数里调用的sub_140001290
里面一堆时间相关的API
一想哦豁完蛋,时间反调?还是那种不影响程序本身运行只影响结果的那种,我打的怕是HCTF?
后来抱着随便试试看的心态调了一波,因为IDA还原的C代码get key和比较都在一行里,无奈打开汇编下断
这是个64位程序,根据fastcall
调用约定,sub_140001430
的返回值,也就是这一位的key会被存在RAX寄存器里,我们动态调试一波
顺便,IDA7.5泄露版本的动态调试有坑,不仅key是错的,而且还闪退,建议静态分析用7.5,windows调试还是用IDA7.0或者别的更靠谱的
图里是已经patch过的版本
发现第一位的key(图里RAX寄存器的值)不论我在断点处停多久,都是0xff
,那没事了
但是又有一个问题,flag第一位不相等,程序直接走sub_140001480
退出
突然想到可以通过改跳转指令,把jnz改成jz,让程序在cmp不为0时继续执行
但是查jnz的机器码很麻烦,百度日常返回一堆无关结果,想起之前玩unicorn的时候用的capstone
汇编引擎,直接一把梭
1 | import keystone |
输出
1 | f 84 c7 ed ff ff |
在IDA的Hex View里比较原指令,把85
改成84
,保存到输入文件,完成patch,之后就是一路F9按下去
遇到非法HANDLE的信号的提示直接Yes即可
发现key是有规律的,从0xff递减,那直接C解密
1 | BYTE enc[24] = { 0x97,0x99 ,0x9C ,0x91 ,0x9E ,0x81,0x91 ,0x9D ,0x9B ,0x9A ,0x9A ,0xAB,0x81 ,0x97 ,0xAE ,0x80 ,0x83,0x8F ,0x94 ,0x89 ,0x99 ,0x97 ,0x00 ,0x00 }; |
得到flaghgame{hello_re_player}
希望官方wp能解释下产生key的流程,根据时间产生也太秀了
pypy
这道题没啥技巧,估计有方法把字节码重新编译回去,但是我看逻辑还不算很长就直接手逆,不能动调太难受了
参考资料:[dis
]Python 字节码反汇编器,里面有字节码的作用解释,也包含了调用约定,注意python解释器的传参顺序是符合直觉的,和C调用约定不一样,是从左到右压栈,从左到右拿参数
感谢r3n0
学长的帮助,帮我看出了异或循环里我把长度3看成i导致乱码的问题,要不然直接stuck
直接上逆出来的结果
1 | import dis |
逆向算法
1 | res=list() |
输出G00dj0&_H3r3-I$Y@Ur_$L@G!~!~
,因为前面程序通过cipher=list(raw_flag[slice(6,-1)])
去掉了hgame{}
头
所以提交的时候要加上,flaghgame{G00dj0&_H3r3-I$Y@Ur_$L@G!~!~}
PWN
PWN很长一段时间里杂七杂八看的,基础比较差,很多概念不清楚,甚至连链表unlink都不太懂,为之后的堆开了个坏头
数据结构很重要,补码也是
感谢xiaoyu
学长
whitegive
白给题,一开始想着scanf栈溢出没跑了,后来checksec发现开了Canary,然后看了一眼printf函数没一个可控的,那估计不是堆溢出
关键点其实在这里,题目提示都给到脸上了,C的字符串比较不能用==
,因为这样是地址比较
1 | if (num == "paSsw0rd") { //Do you know strcmp? |
所以利用思路就是让num的地址==字符串的地址,程序没开PIE,那么这个地址是固定的,IDA一开
1 | .rodata:0000000000402012 aPassw0rd db 'paSsw0rd',0 |
那直接输入0x402012
的十进制形式4202514
就可以getshell,nc连接后直接cat /flag
得到flaghgame{W3lCOme_t0_Hg4m3_2222Z222zO2l}
letter
这道题是后做的,先做的once
,上来checksec,啥都没开,但是应用了seccomp
1 | __int64 init() |
seccomp策略的具体规则不太清楚,但第三个参数应该是系统调用号,第二个参数是政策,这里是白名单
开放了open,read以及write,但没开execve,应该不能常规弹shell了
gdb打开vmmap一下,栈和bss段都可执行,盲猜ret2shellcode
但是ret2shellcode需要知道栈地址(shellcode布置的地址),或者jmp rsp
的gadget
而现代linux都开了aslr,栈地址是随机的,所以需要找一个jmp rsp,问题是这么小的程序里肯定是没有这样的指令的,libc基址也泄露不出来,当时直接stuck
后来一堆百度后看到一个叫bss段的东西,就是vmmap输出里一个ELF最后的那一段,全局变量存在这个段里
这个段不开PIE的时候地址是固定的,恰好,length
这个全局变量就在这个段里,我们只要设法使length代表jmp rsp
,然后覆盖返回地址到length的地址,直接就能让控制流跳到栈上执行shellcode
这里用pwntools提供的asm工具来获取jmp rsp的机器码
1 | from pwnlib import * |
输出
1 | b'\xff\xe4' |
我这边的pwntools不能直接调用把这个转化成数值的p64
函数,所以只能自己算补码
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
算补码的原因是要通过负数绕过if ( (int)length > 15 )
的检查,然后下面把length当作无符号整数来作为读取输入的长度,经典考点了
补码符号位是高位,程序是little
,所以我们需要让length=0xffffe4ff
,输入就要是-6913
之后通过第二个read在栈上写入shellcode即可,shellcode我直接通过shellcraft
构造,偏移通过cyclic
找,不得不说没pwntools我做不了pwn
构造shellcode的脚本
1 | context.arch='amd64' |
这个flag_length
是一位一位加试出来的,把输出保存到同文件夹的shellcode_asm文件里
那接下来很简单,覆盖返回地址到length,之后写入shellcode,最终脚本如下
1 | from pwn import * |
一路n下来到read后,可以看见length已经可以被解析成jmp rsp了
脚本起nc连题目,拿到flaghgame{400a48b3d1b03dc8b9947174a3255bbc2783494c97c90a2ad76c7ed22158048f}
本地测试的时候,没有注意到read
读取到预期长度前会阻塞,然后脚本在远程没回显,跟xiaoyu
学长交 流后意识到这个问题,一位一位加flag长度,最终在0x47收到了完整的flag
once
典型的泄露地址+覆盖返回地址+再来一次覆盖返回地址+ROP一条龙,题目标题提示的很明确了
这是我第一次完成一道栈溢出的pwn题
写到这里已经是2月5号的17:01了,感觉要赶不上ddl,所以直接放脚本
1 | from pwn import * |
直接对着脚本解释,题目开了PIE,但PIE不能随机化地址的最后三位,所以可以通过部分覆盖返回地址来回到main函数
1 | p.send('%p'*20+'\x6a'*1) #69 for main d2 for vuln d6 |
不写入\x69
回到main开始的地址是因为push rbp
会让后面的printf
检查内存对齐的时候失败
通过第一次写入20个%p
到printf泄露libc_start_main
的地址,减去偏移拿到libc基址,这里用了一个比较丑的筛选地址的循环,python实在不算是会写,顶多能用
然后第二次的返回地址写入one_gadget
1 | one_gadget=libc+0x4f3e3 |
原本我用的one_gadget是在0x4f432
1 | 0x4f432 execve("/bin/sh", rsp+0x40, environ) |
但是限制条件*(rsp+0x40)==NULL
不满足,直接stuck
在xiaoyu
学长的提示下,了解到可以通过gadget地址之前的指令调整rbp指向的值,于是调整成0x4f3e3
,getshell
开nc连接题目,执行cat /flag
拿到flaghgame{b73c0d87f1c49e4e4e0962dcddd8f38c95fc835a2b4b66243444505229119328}
MISC
Base全家福
上来给密文R1k0RE1OWldHRTNFSU5SVkc1QkRLTlpXR1VaVENOUlRHTVlETVJCV0dVMlVNTlpVR01ZREtSUlVIQTJET01aVUdSQ0RHTVpWSVlaVEVNWlFHTVpER01KWElRPT09PT09
直接丢进CyberChef,魔法棒提示Base64->Base32->Hex,直接拿到flaghgame{We1c0me_t0_HG4M3_2021}
不起眼压缩包的养成的方法
嗯,虽然图感觉不是特别还原,但还是要吹我惠美如画中仙
看到标题直接把图片后缀改.zip,发现非法,又改成.rar,能打开了,后来学到能用binwalk直接一把梭识别出拼接的文件
但是有密码,压缩包提示
1 | Password is picture ID (Up to 8 digits) |
一开始直接爆破,无果,后来经提示通过pixiv识图搜到了这张图加藤惠,第一层压缩包的密码就是70415155
解开得到第二层压缩包plain.zip
,里面有个flag.zip和NO PASSWORD.txt
,又直接stuck
后来又经提示,这两个压缩包里都有NO PASSWORD.txt
这个文件,而且CRC32一样,可以用已知明文攻击,得到第二层密码C8uvP$DP
第三层压缩包flag.zip
是伪加密,因为010Editor打开直接能看到flag
得到flag的hex形式
1 | hgame{2IP_is_Usefu1_and_Me9umi_i5_W0r1d} |
直接替换去掉&#x
,hex decode得到flaghgame{2IP_is_Usefu1_and_Me9umi_i5_W0r1d}
Galaxy
baby流量审计,直接wireshark打开应用过滤器http
,得到http流量
直接dump出这个png
原本看这张图,最后一个IDAT块大小与之前的不同,dump出来之后发现有点像二维码但是只有中间一小块
画质不太好以为是位隐写,当场stuck
在问Akira
学长后得到hint,宽高CRC,经典考点了,因为高比宽小,盲猜高不够,直接补成正方形
在图片底部得到flaghgame{Wh4t_A_W0derful_Wallpaper}
Word RE:MASTER
word相关的MISC之前从来没碰到过,但知道word本质是一个压缩文件
以压缩包的方式打开first.doc
,在word
目录下看到一个password.html
的文件
1 |
|
搜索了很久后发现这是个BrainFuck,网上找解释器,直接一把梭
1 |
|
得到原文DOYOUKNOWHIDDEN?
解密第二个wordmaimai.doc
,除了一张图之外没啥内容
对着图先笑一会
先不考虑jpg隐写那堆考点,看下这个word本身的内容
标准流程Ctrl+A Ctrl+D
开启段落符号、换行符和隐藏文字,发现了一串Tab和空格组成的东西
开头认为是莫尔斯电码,后来搜了一大堆尝试了培根密码,猪圈密码都乱码,直接stuck
经提示,发现是snow
加密,太怪了,直接复制密文到十六进制编辑器,保存为snow2.txt
下解密程序,运行.\snow -C snow2.txt
得到flaghgame{Cha11en9e_Whit3_P4ND0R4_P4R4D0XXX}
总结
基础还要提升,同届师傅们好肝
感谢学长们的帮助和指导
不要拖到最后再写wp