今年有幸参与WHUCTF
出题,贡献了一道MISC
和一道REVERSE
周六去打别的比赛了,没赶上开赛…
反正两天忙里偷闲做了几题
题解完成情况:
MISC
- REAL SIGN IN
- 好宅呀我都不看这些的
- eldroW
- Rubber
- secretplayer
Reverse
- signin_2048
- Sleeeeeeeeeeeeeeeeeeep
- MMMc
- Way
-
pe_format没放出来的题
Pwn
- ssp
- fmt
- armRop
- Brainfork
Blockchain
Web
Crypto (
我要是会数论昨天的蓝桥杯就不至于心态崩了)
=== Update On 2022-04-19 ===
被隔离了….,没有什么事要干,补了armRop
这题
=== Update On 2022-04-29 ===
明天有一场DawgCTF
,于是今天就补了Brainfork
这题(并没有关联)
MISC
REAL SIGN IN
BASE64
解码直接出flag
:whuctf{WelCome_t0_the_W0rld_of_CTF}
好宅呀我都不看这些的
题目zip
解压后发现Eva终.MP4
,file
查询可知该文件为rar
压缩
解压后发现触动灵魂的ccd.pcapng
和flag.pyc
,这玩意不是去年华为武研119的题目?
迄今为止还记得那个flag.pyc
存在隐写没发现….
现在正经的做这题:
首先拿flag.pyc
开刀,使用stegosaurus处理pyc
中的隐写
需要注意的是这个工具需要在Python 3.6
中运行,我在3.9
中会出现错误
我们得到了flag
的前半段flag{6754997a
然后处理ccd.pcapng
首先用文件->导出对象->HTTP导出所有HTTP
通信的文件
首先看1.php
:一个中规中矩的PHP Shell
,返回的数据使用7c8087
作为开始符,160394d3a42
作为结束符
然后逐个分析,1(1).php
返回了目录信息,1(2).php
中更换了Shell
,起始符变为8d89130c2
,结束符变为a889274
接下来比较重要的是1(3).php
,这是一个rar
文件,手动去除首位标记解压后得到了password.txt
密码表
1(4).php
中更换了Shell
,起始符变为f87e9
,结束符变为e9671e4cc6
1(5).php
是一个zip
文件,其中有密码保护,通过之前的密码表,使用Advanced Archive Password Recovery
或者类似的软件爆破可以得到密码:duome438caodan!&^demima
我们得到了一个gif
然而这个gif
无法直接打开
从Wikipeida
上我们可以得到GIF
文件的格式:文件头 + 逻辑屏幕描述符 + 颜色表 + …
注意到:The series of sub-blocks is terminated by an empty sub-block (a 0 byte).
也就是意味着文件内应该存在大量的0x00
而文件结尾是由0xFF
组成的,怀疑文件内容对0xFF
做了差,写一个脚本进行转换:
1 | f = open('pic1.gif', 'rb') |
得到了pic2.gif
,但是文件头不对
手动改成GIF87a
得到了可以打开的GIF
文件
使用stegsolve
等工具逐帧分析可以得到后半段flag
:44ofd5f4}
本题的flag
:flag{6754997a44ofd5f4}
eldroW
Wordle + 脚本编写
3Blue1Brown - Solving Wordle using information theory
(话说昨晚跑脚本把服务器跑崩了,成功发现了服务端的Bug
:smile:
Rubber
本人出的题,去年刷FB
的是否发现了有关LaTex Injection
的内容,于是想着在新生赛出一个相关题目。本来考虑做更多的变式,比如说从环境变量读取FLAG
之类的,但是感觉较难遂放弃了,最后是往flag
里面添加了一些会改变格式的字符来稍稍增加难度
但是,从做题的情况来看,实属不理想(我还给了多个Hint
…)
验题的时候我把题目给了两个同班同学,他们都能够看出生成的文档是由$\LaTeX$ 生成的,并且能猜测出$\LaTeX$命令执行的考点,唯一难点在于主观性的认为代码是由lstlisting
生成,所以我在题面内加入了相关指示
做题情况分析
最后只有一支队做出了…
这就是 C T F
最后30s
成功拿到flag
两天时间总共收到了302
发提交,其中有:
7
发提交猜测lstlisting
34
发提交成功猜测出了代码片段使用的是minted
宏包46
发提交尝试构造了执行命令的payload
- 剩下的包含但不限于
- 一位语言过于激烈的选手(
fxxk
) 1
次尝试node.js
的提交3
次尝试XSS
的提交5
次很认真的想写Python
代码的提交- 超过一百发提交:
print('hello world')
- 一位语言过于激烈的选手(
在平台上总共收到了28
次flag
提交,其中有:
1
发Catch the flag
3
发提交flag
格式4
发提交成功的获取到了Shell
执行权限但是把临时文件(夹)的名字当成flag
交了6
发提交过滤后的REPLACED
- 剩下的在猜测
flag
从简单的统计数据中来看,只有$\frac{46}{302}\approx 15%$的提交是真正看懂了题意并尝试解答
总结一下:大一新生可能没有接触过$\LaTeX$,验题时样本偏差较大…. (唯一做出来的队还是大三的)
flag
1 | WHUCTF{La$eX^1n_Jec$i0n-JmgCaHnLoKQfsC135zwK} |
payload
regex ( PHP Side )
在题目页面上,只要稍微尝试写一些python
代码即可发现存在过滤(最常见的输入input()
会被替换),所以答题者在构造下面的payload
时需要注意fuzz
一下黑名单词汇
1 | $pattern = '/echo|flag|immediate|write18|input/i'; |
payload
这里有一点难度的是需要猜测代码高亮的宏包,多数人可能听过的时lstlisting
,而题目用的是minted
(minted
需要开启shell escape
选项,刚刚好提供了shell
执行的能力)
考虑到猜测minted
可能存在一定的难度,我在题目描述中故意使用了这个词:
I believe you will be happy with this newly minted document.
1 | \end{minted} |
顺便附上解题者的payload
:他使用了base64
处理flag
,很聪明的做法成功绕过了特殊字符解析的问题
1 | \end{minted} |
Conversion
- You may notice that there is no
{
afterWHUCTF
, because it’s escaped by $\LaTeX$, you have to append it back - Italic characters like
eX1nJec
means that they are surrounded with$
- Superscript and subscript means
^
and_
如果采用base64
编码后偷flag
,可以跳过这一步
Reference
secretplayer [TODO]
给了两个文件password.jpg
和flag.zip
,其中flag.zip
有密码,并且压缩方式为ZipCrypto Deflate
,基本排除明文攻击
现在分析password.jpg
发现文件末尾有多余内容:
提取出来后交给CyberChef
发现这部分是UTF-8
编码:
于是得到了flag.zip
的密码,解压得到79352859_p0.png
检查发现图像末尾没有多余数据,图像放大后能看到规律的白色点阵
=== TODO ===
Reverse
signin_2048
APK
文件拖入JEB
直接找到flag
Sleeeeeeeeeeeeeeeeeeep
去花指令并删除反调试后我们可以理清程序的逻辑:
1 | for ( i = 0; i < 8192 && program[i] != -1; ++i ) |
我们可以照此得出虚拟机的程序:
1 | RESET |
我们把出现了32
次的这个结构拿出来看一下:
1 | Regs[3] += Regs[4]; |
然后对比一下TEA
算法的加密过程:
1 | for (i=0; i<32; i++) { |
不难发现虚拟机的Regs
与TEA
的关系:
1 | sum -> Regs[3] |
然后从读入和初始化可以看出,程序使用常量i_want_to_sleep.
和but_i_study_QWQ.
作为加密密钥,将输入的flag
切成两半进行加密,与预置的常量进行比对,确认加密后结果是否一致。
从Wiki
上抄一段TEA
的解密程序即可,不过需要注意修改程序的常量
1 | uint32_t delta = 0x61C88646; /* a key schedule constant */ |
运行输出:
1 | 61772049 7420746e |
根据题目要求,flag
即为flag{I_want_to_sleep}
题外话,出这题的师傅一开始放了一个让大家验题的版本,结果程序里用输入的flag
作为密钥对上面那个常量进行加密…..我周末的时候发现有师傅过了这题,于是重新研究了一下
MMMc [TODO]
一个解魔方的程序
Way
这题是我出的
题目灵感
2020 ByteCTF “Where are you GOing” (Docs (feishu.cn))
原题背景是Dijkstra求最短路,对最短路径与给定数组异或得到flag
,有意思的地方在于给定的程序求最短路时使用了“睡眠排序”,反向以时间换空间
本题一样是求最短路,给定了一张很“大”的图(1000000
点,5000000
边),给出的程序使用Floyd
求出了最短路。程序给定了一组目标点,由源点到目标点的最短路径即为flag
中对应字符的ASCII
考虑到图中点的数目有1000000
,Floyd
复杂度$O(n^3)$,故运算量为$10^{18}$。在出题者的电脑上运行点数目为4000
的Floyd
算法用时42741ms
,故可估算该题使用Floyd
算法运行需要时间为$\frac{10^{18}}{4000^3}\times4.2\text s\approx6\times10^{6}\text s\approx\frac{6\times10^{6}}{60\times60\times24\times365}\text{year}\approx2\text{year}$
显然,我们没有这么长的时间去运算,所以本题核心在于读懂程序中的算法(Floyd
最简单的三重循环,即便不借助F5
应该也能读懂),并且使用其它算法完成最短路的运算(如Dijkstra
)
其它一些杂项:
fmemopen
函数:把内存映射到FILE*
上,需查阅资料- 花指令:共有两种花指令,出现在三处地方,需要手动去除以便
IDA
分析
题解
直接IDA
加载并跳转到main
处
发现花指令,空格切换视图并去除花指令
简单分析不难发现loc_1B73
的工作是将0x1B6B
处call
指令的返回地址+1
,所以在0x1B70
处按U
取消分析,在0x1B71
处按C
转换为Code
发现该处指令为跳转到0x1B8C
,所以,批量将0x1B6B
到0x1B84
处代码NOP
继续向下分析,在0x1E60
和0x2070
处还有花指令,如法炮制去除花指令
接下来直接F5
对着反编译代码进行分析:
首先看52
行,该函数将unk_46F0
处开始,长度为len
的数据转换为FILE*
以便读入
接下来53
行开始为快速读入,即读入一个整数,其中v4
为符号位,v5
为读取的数字(69
行)。下方还多次出现此结构,不在赘述
我这里将第一个读入的数记为num1
,第二个读入的记为num2
继续向下看,程序使用new
动态开了一个二维数组:long long [num1+5][num1+5]
接下来一段是初始化数组:
这里这一长串是编译器做的优化,循环展开 + 利用xmm
寄存器一次写128
位。完成之后将刚刚申请的二维数组初始化为了一个很大的数
接下来一个循环(num2
次),每次读入三个数u
,v
,w
,并记录到二维数组中(241行)
完成所有的读入后就是一个经典的三重循环,不难看出,这里是一个Floyd
算法:
写出相应的伪代码:
1 | for(long long i = 1; i <= num1; ++i) |
程序通过了floyd
算法求出了多源最短路,最后根据最短路信息输出0x3D
位的flag
,每一位的ASCII
值为从1
号点开始,到qword_4500
对应点的距离除以3
程序分析大致如上,现在考虑解这个flag
:
我们把数据提取出来,qword_4500
为长61
位的数组,直接复制出来即可:
1 | u64 qword_4500[] = {261665,37124,443545,630934,573385,532784,29709,67370,994718,723285,511549,515957,369940,116891,122238,610250,421050,255808,966487,538057,178586,354758,761522,807557,977157,842572,788820,653219,357297,156760,831100,134624,917040,994718,808186,733839,840945,136697,78019,31777,162882,113443,125129,530113,503572,588804,903124,774034,718630,967011,265877,622273,689175,334507,115551,521400,152985,288044,528661,837731,533976} |
而一开始的输入数据unk_46F0
我建议直接用hex editor
:
首先考虑求单源最短路,直接使用Dijkstra
,一个参考的实现如下(补题:P4779)
1 | void dijkstra(unsigned n, unsigned s) |
最后,通过qword_4500
计算flag
:
1 | for(int i = 0; i < 61; i++) |
我们得到了flag
:WHUCTF{A1g0R1tHm_1S_FvN_9587FF6F-1D83-437C-BAD8-A46E2F85B24A}
pe_format
RE
出多了一道题,本来准备放的,但考虑到@secsome
大佬把逆向题AK
了,就不再放出来送分了…
(话说@secsome
大佬好像还去打了ACM
新生赛
Pwn
ssp
CTF-Wiki
有很详细的解释,不过多赘述(花式栈溢出技巧 - CTF Wiki (ctf-wiki.org))
题目很友好,把flag
所在的地址给了出来,所以我们只需要一路覆盖到argv[0]
即可
我们可以采用偷懒的做法,从1
循环到128
,看看溢出多少可以覆盖到argv[0]
1 | #!/usr/bin/env python3 |
(题外话,本题由于我本地环境的GLIBC
太新了,默认不打印,所以直接在服务器上枚举尝试 :)
fmt
这个漏洞的总体利用思想是利用%n
向指定地址写入数据
通过IDA
逆向可知我们需要操控变量v4
使得其值大于1024
通过%2048d
我们可以构造出一个输出2048
长度的串,然后考虑使用%{offset}$n
来向v4
地址写入2048
我们使用GDB
进行调试,由于GDB
在载入程序的时候采用固定地址,这会方便我们的查找
首先我们在malloc
之后下断点,查看v4
存储的地址:
可以知道v4
存储的地址为0x5555555592a0
然后,我们在printf
处下断点,打印进入printf
后的栈结构,查看上面那个地址在栈里面的偏移
我们通过一个简单的例子来看一下printf
在x84-64
下的参数传递过程:
可以发现,除格式化字符串外,前5
个参数使用寄存器传递,从第6
个参数开始往栈上存放,所以在上述栈帧结构中,除返回地址外,我们的目标0x5555555592a0
位于第二个位置,加上前5
个寄存器传递的参数,可以得出其偏移为7
所以构造payload
:%2048d%7$n
向目标地址写入数据2048
,这样就能打印出flag
了
armRop
这是一道基于AARCH64
的栈溢出ROP
题目
首先查一下ELF
属性:
可以发现NX
(栈不可执行,往栈上写shell
时得先mprotect
开权限),NO PIE
(调Gadgets
方便多了)
然后考虑ARM
的ROP
利用方法:
注意到vulnerable
中因为调用了其它的函数,在进入vulnerable
时保存了R29
和R30
寄存器,返回时从栈上弹出,故我们可以通过gets
栈溢出劫持main
的返回地址,构造ROP
链
我们现在需要找两种类型的Gadgets
:一种是将栈中的值复制到寄存器中(一般只有复制到R19
开始的寄存器),一种是将寄存器的值移动到R0-R7
作为调用参数;然后考虑构造如下的链:mprotect
开data/bss
执行权限 -> syscall read
入读shellcode
到data/bss
-> 跳转到shellcode
执行
然而,找了一上午gadgets
,没有找到什么有用的,所有的移动寄存器到R0-R7
的gadgets
最后都通过寄存器寻址跳走了,难以控制目标地址,于是考虑转换思路,研究出题人是否提供了seed
(seed
这个名词对吗?)
于是发现了一个很不自然的函数welcome
:
这个函数使用syscall
做了一件事:输出Welcome! Please say something:
而且这个syscall
有一个完整的结构:
1 | 0x00000000004006dc <+48>: ldrsw x0, [sp, #28] |
即从栈上取出4
个参数,然后调用syscall
,不需要额外的准备或移动 !!
现在考虑我们如何利用syscall
来getshell
:
首先找一下文件中是否存在/bin/sh\0
,这样可以省去写data/bss
的操作:ROPgadget --binary arm --string "/bin/sh"
,然而并不存在。所以我们第一步需要使用syscall(read, fd, buf, count)
将/bin/sh\0
写入到我们可以控制的地址上去
然后使用syscall(execve, filename, argv, envp)
调用shell
AARCH64 Linux System Call
的列表,调用号,调用参数可以在这里找到:Linux System Call Table - aarch64 (thog.github.io)
接下来就是常规的操作,使用cyclic
找到溢出所需的偏移量:144
(覆盖R30
)
然后构造payload
:(需要注意的是:系统调用的参数是32
位的)
1 | from pwn import * |
Brainfork
这是一道brainfuck
的解释器,漏洞在于数据指针没有对边界进行限制,于是可以修改任意地址
首先checksec
发现没有canary
但是有PIE
,所以我们总体利用的思路是:
- 泄漏
__libc_start_main
的地址(刚好有puts
) - 想办法重入
main
- 跳转到
one_gadgets
找到的地址getshell
首先,经过一些简单的测试,不难发现我们将数据指针右移0x104
次可以到达返回地址(需要注意的是,这个数据指针是16
位的)
然后,考虑我们劫持后的栈结构:
1 | +-------------+ |
需要注意的是,我们需要给地址增加偏移量(PIE
),我们不妨复制原来的返回地址,然后计算偏移量并加上
在BrainFuck
中,我们可以通过类似于下述的结构来复制数据
1 | [->+>+<<]>>[-<<+>>]<< |
在第二次重入后,我们需要将返回地址重定向到one_gadget
找到的地址
由于libc
中的地址一般都是0x7ff
开头的高地址,直接加上去会超过65535
的指令条数限制,所以我们需要一种算法来生成高效的BrainFuck
代码:Brainfuck constants - Esolang (esolangs.org)
由于出题人没有提供远程环境,所以我们在本地实验一下即可:
1 | #!/usr/bin/env python3 |