中关村线下赛pwn题解

源程序和 patch 好的程序下载链接:http://file.eonew.cn/ctf/pwn/zhonguancun.zip

比赛的时候只有一道pwn题,但是此次的pwn题是新题型,主办方并没有给源程序,我们只有执行的权限,没有读的权限。但是我们可以利用 动态库劫持 或者 ptrace 来直接读取内存。

读取代码如下:

// gcc -fPIC -shared hook.c -o hook.so
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/prctl.h>

#define PATH "/home/ctf/pwn"

void my_init(void) __attribute__((constructor));

void my_init()
{
    char *image_base;
    int result, fd;
    unsigned  long long offset;

    setbuf(stdout, NULL);

    image_base = *(char **)dlopen(NULL, RTLD_LAZY);
    fd = open(PATH, O_RDWR|O_CREAT|O_NOCTTY, 0644);
    if(fd < 0)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    offset = 0;
    while(offset < 0x1000000 )
    {
        result = write(fd, image_base + offset, 0x1000);
        if(result > 0)
        {
            printf("%p\n", image_base + offset);
        }
        offset += 0x1000;
    }
    exit(EXIT_SUCCESS);
}

读出来的内存并不能直接运行,但是能用IDA进行分析。

下面我说一下他的一些漏洞,以及patch方法。

预置后门

void __cdecl flag()
{
  int v0; // eax
  char buf[256]; // [rsp+0h] [rbp-110h]
  unsigned __int64 v2; // [rsp+108h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  memset(buf, 0, sizeof(buf));
  v0 = open("/flag", 0, buf);
  read(v0, buf, 0x100uLL);
  printf("%s", "If you want to get flag, please login ...\n", "Don't patch this function!!!");
  if ( (unsigned int)check_passwd() )
    puts(buf);
  memset(buf, 0, 0x100uLL);
}

__int64 check_passwd()
{
  int fd; // [rsp+Ch] [rbp-54h]
  char v2[32]; // [rsp+10h] [rbp-50h]
  char s[8]; // [rsp+30h] [rbp-30h]
  __int64 v4; // [rsp+38h] [rbp-28h]
  __int64 buf; // [rsp+40h] [rbp-20h]
  __int64 v6; // [rsp+48h] [rbp-18h]
  unsigned __int64 v7; // [rsp+58h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  fd = open("/tmp/passwd", 0);
  *(_QWORD *)v2 = 'nimda';
  *(_QWORD *)&v2[8] = 0LL;
  *(_QWORD *)&v2[16] = 0LL;
  *(_QWORD *)&v2[24] = 0LL;
  *(_QWORD *)s = 0LL;
  v4 = 0LL;
  buf = 0LL;
  v6 = 0LL;
  if ( !fd )
    puts("Open File Fail");
  read(fd, &buf, 6uLL);
  printf("Please input your name: ", &buf);
  read_n(&v2[16], 16);
  if ( !strcmp(&v2[16], v2) )
  {
    printf("%s, Please input your password: ", &v2[16]);
    gets(s);
  }
  printf("We just hava a account -> \x1B[%dm%s\x1B[0m\n", '"', v2);
  return 0LL;
}

对于上面的代码,如果有实力 patch 的话,可以直接 nop 掉open和puts函数。

否则的话就只能设置密码,而且如果程序不能读/tmp/passwd文件的话,程序默认为空,那么就是空密码,则可以直接泄露flag,但是即使设置了密码,但是程序会返回匹配成功的字节数,这里必须要查看汇编才可以得知这一点。

LOAD:0000000000402DF2 loc_402DF2:                             ; CODE XREF: check_passwd+184↓j
LOAD:0000000000402DF2                 mov     eax, [rbp+success_amount]
LOAD:0000000000402DF5                 cdqe
LOAD:0000000000402DF7                 movzx   edx, byte ptr [rbp+rax+buf]
LOAD:0000000000402DFC                 mov     eax, [rbp+success_amount]
LOAD:0000000000402DFF                 cdqe
LOAD:0000000000402E01                 movzx   eax, [rbp+rax+s]
LOAD:0000000000402E06                 cmp     dl, al
LOAD:0000000000402E08                 jz      short loc_402E25
LOAD:0000000000402E0A                 mov     eax, [rbp+success_amount]
LOAD:0000000000402E0D                 mov     esi, eax
LOAD:0000000000402E0F                 mov     edi, offset aLoginConfirmFa ; "Login confirm fail %d\n"
LOAD:0000000000402E14                 mov     eax, 0
LOAD:0000000000402E19                 call    _printf
LOAD:0000000000402E1E                 mov     eax, 0
LOAD:0000000000402E23                 jmp     short loc_402E3E

所以只要对6个字节进行逐个爆破,就能爆破出密码,如果没有实力 patch 的话,防御方法就是经常换密码。

格式化字符串

void __cdecl fmt(__int64 a1, unsigned int a2)
{
  char *v2; // rdi
  int fd; // [rsp+Ch] [rbp-34h]
  char format[32]; // [rsp+10h] [rbp-30h]
  unsigned __int64 v5; // [rsp+38h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  sub_4023BF(a1, a2);
  fd = open("/tmp/user_lock", 0);
  if ( !fd )
  {
    puts("Open Fail..");
    sub_400E40(0LL);
  }
  if ( fd == -1 )
  {
    v2 = "Function not yet implemented.\nThis is the beta version.";
    puts("Function not yet implemented.\nThis is the beta version.");
    *(_QWORD *)format = 0LL;
    *(_QWORD *)&format[8] = 0LL;
    *(_QWORD *)&format[16] = 0LL;
    *(_QWORD *)&format[24] = 0LL;
    while ( 1 )
    {
      sub_4023BF((__int64)v2, 0);
      --dword_606194;
      read(0, format, 28uLL);
      if ( format[0] == 'q' || format[0] == 'Q' || !strncmp(format, "exit", 4uLL) )
        break;
      printf(format, "exit");
      v2 = format;
      memset(format, 0, 32uLL);
    }
  }
  else
  {
    puts("user_lock");
    --dword_606194;
  }
}

这里,如果 open("/tmp/user_lock", 0);失败的话,就可以进行格式化字符串漏洞,由于payload 仅有28个字节,我们可以逐个字节修改 atoi.got 为 system 地址,下次在进行 get_int 时 输入 sh 即可起shell。

防御方法还是分为两种,有能力patch 就直接让open返回 1,并把printf 修改掉,否则直接创建/tmp/user_lock文件也行。

off by one

程序还有了 heap 题,如下面代码所示,由于使用的size是chunk_size,那么如果chunk有prev_inuse标志位的我们则可以溢出一个字节,但是还有8个字节的填充区,理论上这里 是利用不了的。这里的修复方式:对size进行对齐操作即可。

void __fastcall edit(__int64 a1, unsigned int a2)
{
  int v2; // [rsp+4h] [rbp-Ch]
  int v3; // [rsp+8h] [rbp-8h]

  sub_4023BF(a1, a2);
  printf("1. modify name\n2. modify note\nChoose >");
  v2 = get_int();
  if ( (unsigned int)(v2 - 1) <= 1 )
  {
    print_pipe_info(0);
    v3 = get_pipe_id((__int64)"Please choose the modify pipe id: ");
    if ( v3 < 0 )
      return;
    printf("Plese input info >");
    if ( v2 == 1 )
      read_n(gloabl_pipes[v3].malloc_ptr, *((_DWORD *)gloabl_pipes[v3].malloc_ptr - 2) - 16);
    else
      read_n(gloabl_pipes[v3].malloc_note, 256);
    puts("OK, the pipe info is changed, spend money 1");
    --dword_606194;
  }
  puts("Invalid choose");
}

我们amount开始为51,每次做不同的操作都会让amount减少,当amount为负数时,则check_amount会直接退出。

并且print_pipe_info中还有一个格式化字符串漏洞,用这个泄露基地址,劫持got表。这个patch 的方式,只要加上第一个参数%s就可以了。

void __cdecl print_pipe_info(int a1)
{
    ...
      if ( gloabl_pipes[i].malloc_note )
        printf(gloabl_pipes[i].malloc_note);
      puts(&byte_4042B7);
      ...
}

double free

这里没有清理指针,由于环境是glibc-2.28,此时并没有tcache的double free的检查,所以这个漏洞还是很好利用的。修复 时完成指针清理操作就可以了。

void __fastcall delete(__int64 a1, unsigned int a2)
{
  int v2; // [rsp+Ch] [rbp-4h]

  check_amount();
  v2 = get_pipe_id((__int64)"Please input want to destroy id: ");
  if ( v2 >= 0 )
  {
    free(gloabl_pipes[v2].malloc_ptr);
    free(gloabl_pipes[v2].malloc_note);
    puts("OK, the pipe is successfully destroy, Spend money 5");
    amount -= 5;
  }
}

栈溢出

gets函数直接导致溢出。配合上面的格式化串泄露 stack_guard 就能利用该漏洞。这里用 read 函数完成 gets 的功能,并进行替换即可。

__int64 check_passwd()
{
  int fd; // [rsp+Ch] [rbp-54h]
  char v2[32]; // [rsp+10h] [rbp-50h]
  char s[8]; // [rsp+30h] [rbp-30h]
  __int64 v4; // [rsp+38h] [rbp-28h]
  __int64 buf; // [rsp+40h] [rbp-20h]
  __int64 v6; // [rsp+48h] [rbp-18h]
  unsigned __int64 v7; // [rsp+58h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  fd = open("/tmp/passwd", 0);
  *(_QWORD *)v2 = 'nimda';
  *(_QWORD *)&v2[8] = 0LL;
  *(_QWORD *)&v2[16] = 0LL;
  *(_QWORD *)&v2[24] = 0LL;
  *(_QWORD *)s = 0LL;
  v4 = 0LL;
  buf = 0LL;
  v6 = 0LL;
  if ( !fd )
    puts("Open File Fail");
  read(fd, &buf, 6uLL);
  printf("Please input your name: ", &buf);
  read_n(&v2[16], 16);
  if ( !strcmp(&v2[16], v2) )
  {
    printf("%s, Please input your password: ", &v2[16]);
    gets(s);
  }
  printf("We just hava a account -> \x1B[%dm%s\x1B[0m\n", '"', v2);
  return 0LL;
}

patch 的汇编如下:

mov rsi, rdi
push rdi
xor edi, edi
mov edx, 0x10
call read
pop rdi
dec eax
add rdi, rax
cmp byte ptr [rdi], 0xa
jne over
mov byte ptr [rdi], 0

over:
ret

如何恢复源程序

由于从内存dump下来的程序中没有section表,所以我们需要手动还原该表,主办方也给了我们相关资料,但是无论如何还原程序都是一项复杂的工作,这里我用一个小技巧来运行程序。

直接把dump下来的内存通过一个程序转载到内存里,然后再跳到main函数执行就行。

其中我们只要对got表和std指针进行复原就行,之后程序便能正常运行。装载程序代码如下所示。

// gcc -w load.c -pthread -ldl
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <gnu/lib-names.h>
#include <termios.h>

#define PWN_PATH "./pwn"
#define PWN_IMAGE_BASE 0x400000
#define PWN_GOT_OFFSET (0x606018-0x400000)
#define PWN_STD_OFFSET (0x6061A0-0x400000)
#define PWN_MAIN_OFFSET (0x403C6E -0x400000)

char *gets(char *);

int main(int argc, char **args, char **envp)
{
    char *image_base, **dst, **src;
    int fd;
    int (*main_entry)(int, char **, char**);

    fd = open(PWN_PATH, O_RDONLY);

    image_base = mmap(PWN_IMAGE_BASE, 0x6000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    read(fd, image_base, 0x6000);
    mmap(image_base + 0x205000, 0x3000, PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    read(fd, image_base + 0x205000, 0x3000);

    dst = image_base + PWN_GOT_OFFSET;
    dst[0] = free;
    dst[1] = pthread_create;
    dst[2] = strncmp;
    dst[3] = puts;
    dst[4] = strlen;
    dst[5] = exit;
    dst[6] = system;
    dst[7] = printf;
    dst[8] = memset;
    dst[9] = alarm;
    dst[10] = read;
    // dst[11] = __libc_start_main;
    dst[12] = srand;
    dst[13] = strcmp;
    dst[14] = getchar;
    dst[15] = memcpy;
    dst[16] = time;
    dst[17] = gets;
    dst[18] = pthread_exit;
    dst[19] = malloc;
    dst[20] = setvbuf;
    dst[21] = tcgetattr;
    dst[22] = tcsetattr;
    dst[23] = open;
    dst[24] = pthread_join;
    dst[25] = atoi;
    dst[26] = scanf;
    dst[27] = sprintf;
    dst[28] = exit;
    dst[29] = sleep;
    dst[30] = rand;
    dst[31] = usleep;

    dst = image_base + PWN_STD_OFFSET;
    dst[0] = stdout;
    dst[2] = stdin;
    dst[4] = stderr;

    main_entry = image_base + PWN_MAIN_OFFSET;

    mprotect(image_base, 0x6000, PROT_READ|PROT_EXEC);
    mprotect(image_base + 0x205000, 0x3000, PROT_READ|PROT_WRITE);

    return main_entry(argc, args, envp);
}