catflag
nc 连接直接get shell
homeosrk
数组下标溢出,绕过canary保护直接修改ret地址为后门函数call_me_mabe
这里可以算出arr[14]为ret位置
exp:
1 | from pwn import * |
ROP
栈溢出,而且是gets的栈溢出,溢出空间无限,可以随便写,这道题有很多种写法,这里选择system call
execve的系统调用号为0xb,eax,放着系统调用号,ebx,ecx,edx分别放着execve的三个参数,先找一波gadget
1 | 0x0806c943 : int 0x80 |
于是就可以构造ROP链进入系统 调用了
exp:
1 | #-*-coding:utf-8-*- |
ROP2
syscall()
是系统调用函数,第一个参数是系统调用号,后面的函数分别为调用函数的参数,查表可知4为write函数的系统调用号,3为read函数的系统调用号,所以
1 | syscall(4, 1, v4, 42); == write(1,v4,42) |
read 这里就存在一个很明显的栈溢出了,我们可以控制程序回到syscall的位置,只要将他的4个参数分别设为(b,'/bin/sh',0,0)
就行了
exp:
1 | #-*-coding:utf-8-*- |
toooomuch
可以看到有一个gets,而且还有一个print_flag函数直接打印flag,溢出跳转就完事了
exp:
1 | from pwn import * |
toooomuch-2
程序 跟toooomuch一模一样,但是这次要求get shell ,那就不能直接跳到print_flag函数上去了
因为什么保护都没开,所以可以直接ret2shellcode,思路是这样的,先跳到gets函数往bss段写入shellcode,再跳到bss执行shellcode
exp:
1 | from pwn import * |
echo
这是一道格式化字符串,直接修改printf_got为system_plt的值就行了,都是已知值,手动修改(当然也可以用工具 : fmtstr_payload(7,{printf_got:system_plt}))
exp:
1 | #-*-coding:utf-8-*- |
echo2
64位的格式化字符串漏洞,漏点跟echo一样,不过有一些坑需要注意一下
- 首先是保护开启了PIE,位置无关的可执行程序,即可执行程序的代码指令集可以被加载到任意位置,进程通过相对地址获取指令操作和数据,如果不是位置无关的可执行程序,则该可执行程序的代码指令集必须放到特定的位置才可以运行进程。但是低两位字节是固定的,所以可以通过这个泄露出程序的基地址。
- 64位程序函数地址存在
'\x00'
截断,所以要将函数地址放在最后(不能用fmtstr_payload这个工具,它只适用于32位)
printf处下断查看栈可以看到main+74
和libc_start_main+340
这两个可以泄漏的地址,偏移分别为41和43,因为开启了PIE,而且后三位不变,所以可以泄漏出程序基地址就是0x555555554a03-0xa03
,之后对一切地址的操作都加上这个基地址就是正确的地址了,以及libc_start_main
的真实地址0x7ffff7a2d830-240
就可以算出偏移,从而得到其它函数的真空地址,比如system
,不过这道题我用的是one_gadget一把梭
得到了真实地址和偏移就可以进行写入操作了,修改exit_got表为one_gadget_addr
exp:
1 | #-*-coding:utf-8-*- |
这里解释一下4*’a'
:是为了最后的p64(exit_got)
对齐,gdb下断看一个栈的分布就清楚了
ehco3
还是格式化字符串,不过我们的输入不再是在栈中了,是保存在bss段,这就不好操作了,我们需要在栈中找到指向栈的指针来进行操作向栈写入内容(建议先做一下jarvis OJ的lab 9然后再回头来看这题,因为题型差不多,但是lab 9没有下面的蛇皮操作)
不过这题最坑的还是在hardfmt函数前的这个玩意v3 = alloca(16 * (((buf & 0x3039u) + 30) / 0x10));
看了大佬的 writeup 这是一个抬栈操作,我们回到汇编去可以看到,在最后esp会减去eax使得整个栈帧往栈顶移了eax,而且eax是个随机数,好在还是有范围的。
测试一下我们可以发现大概的范围:
1 | import random |
可能的数值有0x10,0x20,0x30,0x40,0x1030,........
等等等等,也就是说一个值对应一个栈帧,所以我们只需要确定eax的值就可以确定栈的分布了,在.text:08048774 sub esp, eax
下断gdb调试一下:
这一次eax 的值 为0x2050,我把它设为0x20,进去,在printf 下个断点,c一下,就可以看到正确的栈帧了
1 | Breakpoint *0x08048646 |
这里就以0x20的栈帧进行分析了,可以发现几个有用的地址
1 | 14:0050│ 0xffffcd50 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f10 (_DYNAMIC) ◂— 0x1 |
偏移分别 为20,21,30,31,43,(这里规定它们分别为fmt20,fmt21,ebp1,ebp2
) 而且偏移43处放着的是libc_start_main+247
的地址,它的偏移是不变的,所以就可以用来做爆破的标志,来找到我们要的栈帧(exa = 0x20的栈帧)
1 | while True: |
爆破完成之后就可以进行正常的操作了思路如下 :
- 通过
libc_start_main
算出偏移,进而得到system
的真实地址 - %n操作 30,10偏移处使ebp1指向fmt20,ebp2指向fmt21
- %n操作 ebp1使fmt20的内容修改为
exit_got
操作 ebp2 使fmt21的内容修改为exit_got+2
- %n操作 fmt20 修改
exit_got
为system
低4位,操作 fmt21 修改exit_got+2
为system
高4位 - 发送
'/bin/sh'
作为system
函数的参数
完整exp:
1 | #-*-coding:utf-8-*- |
smash-the-stack
这题是利用ssp报错的方法泄漏出flag
,在ctf-wiki
中有介绍:Stack smash
只要将argv[0]
覆盖为存放flag
的地址即可,在write处下断查看argvp[0]
的偏移
1 | pwndbg> stack 20 |
exp:
1 | from pwn import * |
还有另一种做法就是不用算偏移,直接塞一大把p32(flag_addr)
进去,因为只要覆盖到argv[0]
的位置就可以了,但是实践表明,如果这个数差偏移太多的话,也是不太行的。(比如上面的塞100个在本地还是可以的,但是远程的话63个以上就已经不正常了,我猜是覆盖到了___stack_chk_fail
函数部分导致函数无法正常执行,也就不存在通过___stack_chk_fail
函数打印出flag
了)
onepunch
这道题还是挺有趣的,起初看反编译代码不是很理解 v6 跟v4的关系,但是在汇编中就很直观了,v6是地址,v4是写入的内容,也就是任意地址写
还有就是这个程序的text
段居然是可写的,结合上面的任意地址写就意味着我们可以修改程序的逻辑实现各种操作,相当于打patch
再看一下main
函数:这里对输入的v4进行判断,如果不等于255就跳到400773处,所以我们只需要在这里打patch使其跳到main
函数就可以实现无限输入。
1 | .text:0000000000400756 mov rax, [rbp+v6] |
这里就需要修改16进制了,IDA->options
将Number of opcode bytes(non-graph)
的值设为16就可以看到汇编对应的16进制数。接下来算偏移,要从0x400769
跳跟0x4006f1
偏移应该为0x88 = 136,所以第一步就是将0x400768
处的0xA修改为0x88
1 | p.recvuntil('Where What?') |
接下来往text段写入shellcode,写完后再修改0x400768
处为shellcode地址即可
完整exp:
1 | #-*-coding:utf-8-*- |
tictactoe-1
每次可以写入一个字节,所以就很容易可以想到,把puts的got表修改成0x08048C46
(cat flag的位置),就可以拿到flag_simple了
exp:
1 | #-*-coding:utf-8-*- |
rsbo-2
栈溢出漏洞,程序中又有write函数,所以其实很清晰了,利用write函数泄漏出read的真实地址进而得到system的真实地址再跳转到去就可以get shell 了,但是这里有一个坑需要说一下,就是垃圾字符要用\x00
去填充而不是用’a’啊啥的这些
因为len(buf)在栈中的位置跟buf的重叠的,所以当我们有字母去填充时,会导致 v8的值出错,这样程序就会崩溃退出
用\x00
填的时候才会正常,接下来的操作不用多说了
exp:
1 | #-*-coding:utf-8-*- |
rsbo1
其实我是用的rsbo2
的脚本直接拿到两题的flag
再回过头来用open
的方法做rsbo1
,因为我一直在纳闷open返回的指针怎么获取给read用,但是后来问了师兄才知道了read的第一个参数的妙处:
关于read的第一个参数read(fd,buf,size) 为0时表示标准输入流(键盘),为1时表示标准输出流(屏幕)(1一般是用在write吧),为2时表示错误信息输出,为3之后表示文件流依次表示第一个open的文件第二个,第三个…….(如果同时打开多个文件的话)
所以open(“/home/ctf/flag”)后可以直接调用read(3,bss,0x60)再write就可以把flag打印出来的
还有个坑,在open这里虽然它只需要一个参数,但是它并不只有一个参数,我们要保证它的其实参数为0才能正常调用
exp:
1 | #-*-coding:utf-8 |
stack
这题还是挺有意思的,程序主要做的事就是模拟一个栈的push,pop操作,并将自己模拟的栈放在函数栈帧中,
上图为栈的分布,我们可以看到,自己构造的esp也同样放在栈中,那么我们就可以通过pop,push操作控制esp的位置实现任意地址读,写,思路如下 :
- 将esp指向esp所以在位置的上方,push写入改变esp指向
libc_start_main+247
的位置 - pop出
libc_start_main+247
的值,利用偏移算出system
及"/bin/sh"
的真实地址 - 继续控制esp指向
main的ret地址
位置,修改为system
的地址,以及参数"/bin/sh"
- x 退出即可
get shell
好了,接下来详细讲一下过程以及上图是怎么来的
这是IDA反编译出来的东西,我看着是看不出什么有用的信息的,建议看汇编,如果单纯汇编很难看懂的话,可以跟着gdb一步步调试来理解,那我们看汇编
1 | .text:000006F0 public stack_push |
可以看到,在进行push操作的时候mov [edx+eax*4+4], ecx
是与ecx有关,pop的时候mov eax, ds:(dword_1FC4 - 1FC0h)[eax+edx*4]
也是跟exc有关,到gdb里看一波
可以看到ecx存的是push的值(我输入的是123=0x7b)eax是与ebp的偏移(将初始esp看成ebp吧),edx是ebp,这里应该就能看出一开始给的图的上半部分了吧
单步一下可以看到我们push的值已经入栈,push的操作明白了我们来看一下pop的操作
因为我们已经先push一个0x7b
,所以这次pop指向的就是0xffffcc4c
处的0x7b
并且esp更新为0(-1),这就是pop的过程,理清这两个过程就可以来实现上面的4个思路了
首先要修改esp的值就是先将esp指向esp的上方,即0xffffcc44
处,初始esp是指向0xffffcc48
,所以只需要pop一下就可以了,然后就是修改esp的值 ,用push,修改为多少呢
修改esp指向libc_start_main
的位置,也就是0x59 = 89,之后再用pop将地址泄漏出来,进而算出偏移,得到system
跟'/bin/sh'
的地址,得到地址之后就要找到main
函数的返回地址,覆盖为system
我们再往下看多一点栈的内容,回到main,在0x8fb
处下个断点,单步往下
看到这个栈帧是不是很熟悉,继续单步到ret处查看栈
对比一下可以很清楚的发现main函数的返回地址是第二个的libc_start_main
而不是我们用来泄漏地址的位置,这就是一开始那张图的下半部分了,好了,那开始覆盖:将0xffffcdbc
覆盖为system_addr
,将0xffffcdc4
覆盖为binsh_addr
写入的时候用还是跟泄漏地址时一样的做法,先将esp指向其上方,然后用push压入相应值
不过这里要注意的是,scanf的格式化字符是%d
,它能接收的最大值是0x7fffffff
而我们要写入的真实地址都是0xf7
开头的,明显太大,所以我们要用负数去写,0xffffffff == -1
,0xfffffffe == -2
这样子就能写入我们要的真实地址了
exp:
1 | #-*-coding:utf-8-*- |
leave_msg
这一题可真是长姿势了呀,主要的知识有:
strlen
函数遇到'\x00'
就会停止计算长度atoi
函数会跳过字符串前面的空格或者换行符,直到遇到数字才进行转换- 也是最骚的,got表不一定是写入地址,也可以写入可执行代码(在特定的条件下:比如这一题got表是可执行的,就可以)
其实一开始分析main函数的时候,就发现了改写got表的漏洞,但是因为既加了长度限定,又加了负数检测,一时间就卡住不知如何下手,但其实这几处保护是有缺陷的,这就涉及到了我上面讲到的3点知识,只要我们在8个字符后加'\x00'
就可以路过strlen
继续往栈输入内容,对于负数检测因为nptr
是输入字符串的第一个字符,所以我们只要输入空格+负数,就可以跳这个检测了。接下来就是核心了,因为你会发现虽然可以修改got表了,但是,修改成哪个地址?这处程序既没有后门函数,也没有可泄漏地址的漏洞。
这里可以看到 0x804a000 到 0x804b000 居然是可执行的,这就说明我们可以修改got表为可执行代码了,但是同样有个问题就是,可写的代码长度只有8,所以是无法构造shellcode的,只能进行间接的跳转,而且程序的保护并没有开启NX,所以可以往栈里写入shellcode 然后修改got表为add esp ,*** jmp esp
执行shellcode,所以接下来就是要先确定好这个偏移
构造一个'a'*8 + '\x00' + 'b'*8
这样'a'*8
就会写入到puts的got表,整个buf也会写到栈中去,再在下一次的puts处下断点查看偏移:0x0804861d
si,进入puts函数内部,这里就可以看到输入的字符相对esp的偏移是0x30,而我们的shellcode是在’\x00’后面,也就是’b’*8,所以got表中的跳转代码就应该是add esp,0x30+len(jmp)+1 ; jmp esp
,就可以指向shellcode 了
exp:
1 | #-*-coding:utf-8-*- |
very_overflow
这题就有意思了,同样是可以实现任意地址读写,不过这次是通过控制结构体的指针来实现,不过这里定义的结构体有点简单,只有指向下一个结构体的指针next和数据data
先add 一个数据进去,在gdb中下断点看一下情况
在这里可以看到结构体在栈中的存储方式是 node->next node->data
(不明白aa上面为什么是next指针的话,可以再add一个然后查看栈就明白了)而且node->next = (node + strlen(node->data) + 5);
,所以当add多个node的时候,next跟data是紧挨着排下来的,data跟next中间只隔着一个'\x00',
我一开始在知道这个布局后并没有想到什么有用的利用条件(还是太菜了),但是正常这样紧挨着的布局,使得一种可能:修改node[0]的data从而覆盖node[1]在next达到控制next指针的目的,控制了指向就可以任意地址读写了。
因为show函数会将next指向打印出来,这样就知道了栈地址,计算出node[0]->next跟libc_start_main
的偏移,就可以修改指向通过show将它打印出来,接下来算出system的真实地址用同样的方法写入到返回地址处就行了
exp:
1 | #-*-coding:utf-8-*- |