本篇writeup已收录进pwn解题本. Link
前言
最为第一届BJDCTF的参赛选手和本届比赛的二进制出题人+运维,真心祝愿BJDCTF越办越好!(说多了怕被打)
还是要啰嗦一句,本次BJDCTF为七校联盟萌新赛,同时在BUUCTF对外开发,感谢赵师傅的大力支持,以及各位外校师傅的捧场。
第一次出题,pwn貌似出难了,其实主要是想考察下linux基础,在这里给各位萌新们道歉啦。
不过还是希望大家能打下坚实的linux基础~
p.s. ret2text3,4&YDS是芝士师傅出的,算账不要来找我鸭
一把梭(one_gadget) - TaQini
考察点:one_gadget
题目给出了printf
的地址,由此可算得libc基址,然后找one_gadget
、计算libc中one_gadget
地址
1 | printf("Give me your one gadget:"); |
v4
是个函数指针,scanf
的时候把one_gadget
转成十进制输入即可getshell。
1 | #!/usr/bin/python |
Imagin的小秘密(secret) - TaQini
考察点:缓冲区溢出、GOT表覆写
1 | .data:000000000046D080 ; char buf[] |
程序开头read(0, buf, 0x16)
,实际上buf
大小只有0x10,后6字节会覆盖times
变量
1 | void __noreturn sub_401301() |
猜错退出程序时,有个printf
打印buf
内容,查看got表,发现printf
和system
只差0x10
1 | [0x46d038] system@GLIBC_2.2.5 -> 0x401076 (system@plt+6) ◂— push 4 |
所以把times
覆盖成got[printf]
1 | buf='/bin/sh;' ; got[printf] -> system |
times
每猜对一次自减1,控制猜对的次数,即可构造出system("/bin/sh")
exp:
1 | sl('/bin/sh;AAAAAAAA'+p32(0x46d040)) |
只用找出前0x10个secret即可,不过,硬要找全10000也不难,正则匹配一下就好 。
Test your ssh(test) - TaQini
由于Ubuntu 14之后,通过egid执行/bin/sh的权限被ban了
所以这次比赛ssh靶机用的全是Ubuntu 14.04
考察点:linux基础
这题设置的目的是测试ssh连接显示编码什么的是否正常,但是直接白给不太好,就加了个字符过滤。
看源码,可知过滤了以下字符:
1 | n e p b u s h i f l a g | / $ ` - < > . |
于是就找可用的命令呗,先看下环境变量PATH
,然后grep
搜一下
1 | env $PATH |
发现od
幸存
1 | ctf@f930cab87217:~$ ./test | grep 045102 -C 2 |
使用od
输出flag,然后解八进制即可
非预期解
x86_64
命令没有过滤掉,可以直接拿shell
原理如下
1 | ls -al /usr/bin/x86_64 |
x86_64
是指向setarch
命令(soft link),查看一下setarch
的文档,如下:
1 | setarch - change reported architecture in new program environment and/or set personality flags |
挑食的小蛇(snake) - TaQini
考察点:字符串截断 or 耐心
背景知识:c语言中字符串以’\x00’为结尾
把程序下载下来,调试,发现Name
和flag
相邻,相差0x100字节
1 | p &Name |
查看源码:
1 | void getName(){ |
输入昵称时会copy 0x100个字节到Name
,所以只要输入长度为0x100的昵称,Name
的结尾就不会有’\x00’,游戏显示玩家昵称时就会把Name
和flag
一起打印出来。
1 | 正常情况 |
p.s.这题玩到3000分也可解,比赛时好多师傅硬怼出来的……嗯….耐心也是一名pwn手的基本素养
贪吃的小蛇(snake2) - TaQini
考察点:scanf
这题的设计参考pwnable.kr的passcode
题解详见:https://blog.csdn.net/smalosnail/article/details/53247502
文章题目: scanf忘记加’&’危害有多大?详解GOT表覆写攻击技术
拿到代码后找不同,看看那里和snake1
不一样
获胜分数提高了,硬玩儿是玩儿不出来的
c1
printf(" 控制Imagin吃豆豆,达到300000分\n");
getName
读昵称的长度变短了,不能利用snake1
的解法c1
2
3
4
5
6void getName(){
char buf[0x100];
printf("请输入玩家昵称(仅限英文)[按回车开始游戏]:");
scanf("%s",buf);
strncpy(Name, buf, 0x10);
}多了一个调查问卷功能
c1
2
3
4
5
6
7
8
9
10
11void questionnaire(void){
int Goal;
char Answer[0x20];
puts("你收到了一份来自TaQini的调查问卷");
printf("1.Snake系列游戏中,贪吃蛇的名字是:");
scanf("%20s",Answer);
printf("2.Pwn/Game真好玩儿[Y/n]:");
scanf("%20s",Answer);
printf("3.你目标的分数是:");
scanf("%d",Goal);
}
通过对比可知,snake1
的漏洞点在getName
,snake2
的漏洞点在questionnaire
1 | void GameRun(void) { |
查看questionnaire
的上一层函数,可见getName
和questionnaire
用是同一片栈空间
按照参考文章中的做法,利用scanf
覆写got表为后门system("/bin/sh")
的地址,即可getshell
比如,后续的Init
函数中调用了malloc
,因此可以覆写malloc
的got表:
1 | PSnake head=(PSnake)malloc(sizeof(Node)); |
这题malloc
的got表地址0x405078
都是可见字符,解题时甚至不用写脚本
1 | name = 'a'*220+'xP@' # xP@ <- (malloc.got) |
鹅螺狮的方块(els) - TaQini
考察点:格式化字符串
打开游戏,发现底部有个留言板十分瞩目,找到对应源码,发现存在格式化字符串漏洞:
1 | /* 实时显示留言 */ |
那么本题的主要漏洞就是他了。
知己知彼,百战不殆。要想pwn掉els,需要先对程序了解个大概。于是浏览源码:
- 程序开头读取本地record文件,加载变量最高记录,随后判断最高分数,大于阈值就给shell c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/* 读取文件的最高记录 */
fp = fopen("./record","r+");
if (NULL == fp)
{
/*
* 文件不存在则创建并打开
* "w"方式打开会自动创建不存在的文
*/
fp = fopen("./record","w");
}
fscanf(fp,"%u",&maxScore);
if(maxScore > 666666)
{
puts("干的漂亮!奖励鹅罗狮高手shell一个!");
system("/bin/sh");
exit(0);
} - 实时显示留言功能:读取msg文件,打印留言,其中
fprintf(stdout,message)
存在漏洞c1
2
3
4
5
6
7/* 实时显示留言 */
fmsg = fopen("./msg","r+");
if (NULL == fmsg) exit(0);
char message[0x100] = {0};
fread(message,0x80,1,fmsg);
fprintf(stdout,"\033[22;1H留言:");
fprintf(stdout,message); - 消除方块功能:更新最高分数,将最高分写入record文件 c
1
2
3
4
5
6
7
8
9
10
11
12void checkDeleteLine(void)
{
// ...
/* 记录最高分 */
if (score > maxScore)
{
maxScore = score;
/* 保存最高分 */
rewind(fp);
fprintf(fp,"%u\n",maxScore);
}
//...
鲁迅曾经说过:
一切皆文件
所以上述代码的浏览主要以msg
和record
这两个文件为线索。
现在思路就很明朗了,通过格式化字符串漏洞修改maxScore
,消除一行方块,触发历史记录更新,改写record
文件,重新开始游戏,getshell。
关于文件权限,可以通过
ls -al
查看:msg
可读可写,record
可读,只有运行els程序时可写
由于开了地址随机化,maxScore
的地址不固定,但是这在格式化字符串漏洞面前都不是事儿,先泄漏,再改写即可。exp如下:
leak.py
1 | #!/usr/bin/python |
exp.py
1 | #!/usr/bin/python |
上述两个文件放到/tmp
目录下,先执行leak拿到程序基址,再通过exp计算maxScore
地址并改写。消除一行方块后触发记录更新,游戏结束后maxScore
写入文件,再次打开游戏即可getshell。
营救Imagin(rci) - TaQini
考察点:linux基础,ls命令
本题的设计源于HGame2020 - findyourself
题解链接:http://taqini.space/2020/02/12/2020-Hgame-pwn-writeup/#findyourself
背景就不多介绍了,imagin被关进了随机创建的48个房间之一,这时有一次执行系统命令的机会,经过层层过滤,只有ls命令可用,使用ls获取一些线索后,就要输入imagin所在的正确房间号了,答对后获得第二次执行系统命令的机会,可以getshell。
在hgame-fys中第一次命令执行是通过 ls -l /proc/self/cwd
获取的当前目录,而本题没有给/proc
,所以要另辟蹊径,也就是本题的考察点inode
了。
inode
是linux用于文件储存的索引节点,操作系统大家应该都学过:
系统读取硬盘的时候,不会一个个扇区的读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个“块”(block)。这种由多个扇区组成的“块”,是文件存取的最小单位。“块”的大小,最常见的是4KB,即连续八个sector组成一个block。
文件数据都储存在“块”中,那么很显然,我们还必须找到一个地方储存文件的“元信息”,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode
摘自:https://blog.csdn.net/xuz0917/article/details/79473562
也就是说inode
和文件是一一对应的,鲁迅曾经说过:
一切皆文件
目录也是文件,也有他对应的inode,于是,本题的重点来了——ls命令常用参数(敲黑板)
1 | ls -l # 以列表格式显示 |
众所周知,当前目录文件用 .
表示,所以输入 ls -ali
命令即可显示当前目录的inode
号
也就是说,imagin所在房间的inode
已知了,但是 .
是相对路径,题目中要求验证绝对路径
于是想办法查看绝对路径,我们已知房间是在/tmp目录下的,所以不难想到,再开一个shell,输入 ls -ali /tmp
显示/tmp目录下所有文件inode
,根据唯一的inode
找到对应房间号,即可通过check1。
本题重点结束。
check2 也过滤了一些字符,可以通过输入 $0
绕过。
我们不一样(diff) - TaQini
考察点:栈溢出
题目是汇编写的,所以就没给源码,做题时需要把文件下载到本地分析。
下载方法挺多的,这里说两种比较直接的方法:
base64
编码后复制粘贴到本地scp
命令 使用ssh协议传输文件
用过diff
命令的师傅不难看出,这题是一个缩减版的diff
命令,功能是比较两个文件,输出两文件内容不相同的那一行的行号。分析程序,打开文件部分没得说,直接看比较函数:
1 | int __cdecl compare(int a1, int fd) |
addr
长度120,read
读了128字节,很明显的栈溢出。此外buf1
具有可执行权限:
1 | p &buf1 |
打开的第一个文件数据读入buf1中,打开的第二个文件数据读入addr
因此,在第一个文件中存shellcode
,在第二个文件中存payload
,将返回地址覆盖为buf1
地址,即可getshell。
二进制一家亲(diff2) - TaQini
考察点:字符上溢出、跑脚本爆破
题目来源:巨佬keer的diff非预期解+我两年前的汇编实验
代码链接:https://github.com/TaQini/AssemblyLanguage/tree/master/lab/7
二进制,一家亲。
diff
的预期解是缓冲区溢出,硬是让keer师傅找到了一处字符溢出…直接把flag给爆破出来了…tql….于是,我把
diff
的缓冲区溢出的洞补上,将之魔改成为re题目一道。
既然是re,就要想怎样解出flag。diff
程序可以读flag
和另一个文件,就叫做ktql
好啦,并且会对这两个文件进行比较,所以思路就是变化ktql
、爆破flag
。
比较字符函数如下:
1 | int compare() |
乍一看没毛病,其实不然。for的循环条件:
1 | buf2[i] + buf1[i] && i < 0x400; |
一般师傅:char
+ char
= char
没毛病
keer师傅:char
+ char
= 溢出!怼他!
我们知道char
型变量占1个字节,相当于unsigned byte
,表示范围是0x0-0xff,那么两char相加的范围就是0x0 - 0x1fe ,可是char
型只能存储1个字节的数据,因此两char
相加产生的进位就会被忽略。举个栗子,0x7d+0x83=0x100->0x0。get到了这一点,再看for循环条件,就能看出些端倪了。
buf2[i] + buf1[i] = 0x100
时会终止for循环,并且返回0。按程序正常的流程走,除非buf1
和buf2
完全相同,否则不可能返回0,而现在只要buf1
和buf2
任意位置对应的字节相加等于0x100,compare
也会返回0。
返回 0 时程序打印 “一样”
返回值非0时 程序打印 行号
根据不同的返回值,就可以对flag进行逐个字节的爆破了,脚本如下:
1 | #!/usr/bin/python |
End
上述所有题目复现地址 https://buuoj.cn/ or http://ctf.taqini.space/