ret2shellcode刷题
首先写在前面:所有题都是没有开NX保护的,我都checksec过了,就不再每一题都贴了
ctfshow pwn60
入门难度shellcode
考点:strncpy函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
关于strncpy函数的介绍: https://www.runoob.com/cprogramming/c-function-strncpy.html

1 | from pwn import * |
这里用的很妙的是ljust函数,因为shellcode也是有长度的,如果直接用+拼接,那么会超过偏移量,这里的意思是:不管 shellcode 实际有多长,ljust都能让总长度变成 112 字节。如果 shellcode 不够 112 字节,缺多少就在后面(右边)补多少个 a
这样做到了一举两得,既填充了padding,又写入了shellcode
ctfshow pwn61(shellcode长度超标和recvuntil)
输出了什么?
考点:recvuntil的分步使用,内存分布的理解
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
这个题的PIE保护是开的,因此我们每次输出的v5的地址在栈上都是随机的,我们需要接收这个地址,并且把shellcode写进去,但问题是%p周围有一堆提示,我们应该怎么只挑出%p打印的地址呢,这就需要用到recvuntil了
recvuntil 的基本用法
tube.recvuntil(delimiters, drop=False, timeout=default)
其中:
tube: 是一个连接对象,通常是 pwnlib.tubes.remote (远程连接) 或 pwnlib.tubes.process (本地进程)
delimiters: 必需。您期望接收数据直到遇到的那个终止字符串(或字节串),必须是b’string’
drop: 可选。一个布尔值,默认为 False
如果设置为 True,则接收到的数据将不包含这个终止字符串 (delimiters)
如果设置为 False (默认),则接收到的数据将包含这个终止字符串 (delimiters)
timeout: 可选。设置等待数据的最大时间(秒)。如果超时,会抛出 pwnlib.timeout 异常
我们可以通过用两次recvuntil”过滤”掉%p周围的字符,第一次recvuntil是接收[前的字符,第二次是接收]前的字符,我们用的时候就是第二次recvuntil的内容了
1 | io.recvuntil('[') |
再说一下为什么要转成int类型,因为输出的地址,如0x7ffeebf02fb0,在python里是以字符串(bytes 类型)的形式存在的,我们需要转成16进制的地址
shellcode长度超标
一开始的思路是想往v5里写shellcode的,但是v5的长度只有2*8=16个字节,shellcode的长度肯定是大于v5的长度的,所以我们需要想想别的办法
我们必须要和上个题区分开的是,上个题给我们的变量buf2在.bss段,这一次给的v5在栈上,0x7ffd46799d80,一般来说,0x7ff这种的地址都在栈上
全局变量存放在 .bss 或 .data 段
局部变量存放在栈上
这是ELF程序的主要内存布局
[高地址]
stack(栈)
heap(堆)
bss, data(全局变量)
text(代码)
[低地址]
所以既然v5写不了,那我们可以直接写到v5后面的栈上
1 | payload = b'a' * 0x18 + p64(v5_addr + 0x20) + shellcode |
这里的payload我想了得一个小时,其实很简单
gets函数从v5开始写,我们还是常规的先填充0x10+8=0x18个字节到rip,关键是v5_addr + 0x20怎么去理解
p64(v5_addr + 0x20)得到的就是v5_addr往后第0x20个长度的地址,而0x20-0x18=8个字节,也就是我们用b’a’ * 0x18的填充后的下一个槽,也所以rip指向的就是v5_addr + 0x20这个地址,我们把shellcode写到了往后的栈上
v5_addr + 0x00 : padding
v5_addr + 0x10 : padding
v5_addr + 0x18 : saved_RBP 被覆盖
v5_addr + 0x20 : saved_RIP 被覆盖 ← 写入的跳转地址
v5_addr + 0x28 : 这里开始是 shellcode
v5_addr + 0x30 : (继续 shellcode)
…
完整payload
1 | from pwn import * |

ctfshow pwn62(编写短的shellcode)
短了一点
考点:长度更短的shellcode编写
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
本题和上一题的最大区别是gets函数换成了read函数,并且限制了我们读入的长度为0x38uLL
这个时候pwntools自动生成的shellcode显然太长了,于是我们需要自己写一些短的shellcode,并且换成机器码
我用AI写了好几个shellcode都不行,于是抄了别的大佬博客上的,终于可以了
32 位 短字节 shellcode (21 字节) \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80
64 位 较短的 shellcode (23 字节)\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
exp如下,和上题区别只有shellcode不同
1 | from pwn import * |

ctfshow pwn62(同pwn62)
又短了一点
考点:长度更短的shellcode编写
和上一个题的exp一模一样,因为我找的shellcode已经是相当相当短的了

ctfshow pwn60
有时候开启某种保护并不代表这条路不通
考点:mmap函数
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
这个题有点意思,确实开了NX保护,但是给了一个特殊的函数mmap,它可以将文件或其他对象映射至进程的内存地址空间,使得进程能像访问内存一样直接操作文件内容
那就直接写shellcode就好了,连栈溢出都不需要了
1 | from pwn import* |
这里我顺便试了一下刚才的32位的机器码,也是可以打得通的

ctfshow pwn65-68
感觉难度确实上来了,有个新知识nop sled感觉挺难的,但是问了下AI感觉也很重要,打算先鸽一下,下个周回来补吧






