ggbond
1 | pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=ToABmmiACyxYP6ANjPii/mBNQG9mzJa6bIWHLsokK/NmmbD1vsv7bojlz5M5b8/uc_h6deBc8Nkqg4RmiJv, stripped |
- 64位,dynamically,NX,FORTIFY
程序分析
程序是一个 gRPC 服务器,先使用 /pbtk/extractors/from_binary.py
来获取其 proto 文件:
1 | syntax = "proto3"; |
- 服务端内置模块为 Handler,其中有3个子功能
使用如下命令编译 proto 文件:
1 | python3 -m grpc_tools.protoc -I ./ --python_out=./ --grpc_python_out=. ./ggbond.proto |
在开始分析程序前建议用 AlphaGolang 恢复符号
在 RegisterService 函数前断点,开始调试分析:
1 | .text:00000000007EE21A 48 8B 0D 8F D1 18 00 mov rcx, cs:off_97B3B0 |
打印其第二个参数(RBX)的数据,该参数其实是 ServiceDesc 对象,源码如下:
1 | type ServiceDesc struct { |
- MethodDesc 中存储有我们注册的函数
1 | pwndbg> telescope 0xc59860 |
- 打印 MethodDesc 的数据如下:
1 | pwndbg> telescope 0xc525c0 |
- 地址 0x7ed300 所在的函数即是目标函数的 handler:
1 | void *__fastcall main_ggbond__GGBondServer_Handler_Handler( |
- 我们直接在调用目标函数的地方打断点(
b* 0x7ED545
),查看目标函数的位置:
1 | ► 0x7ed545 call rdx <0x7ed860> |
- 这里的 0x7ed860 即使目标函数的地址
漏洞分析
找到服务端注册函数的位置后,我们可以开始漏洞分析:
- 程序内置了4个 role(编号为:0,1,2,3)
- 通过 whoami 命令可以查看当前的 role
- 通过 role_change 命令则可以切换 role
- 通过 repeater 可以发送一条长数据(没有限制长度)
漏洞点如下:
1 | if ( qword_C94B80 == 3 ) /* 如果role编号为'3' */ |
- 程序对 “role编号为3” 这种情况进行了特殊处理,并且往栈中填写了传入数据(栈溢出)
入侵思路
有了栈溢出就可以构造 ORW 链(没法在服务端上直接获取 shell)
- 注意:传入的数据会进行 base64 加密,因此实际偏移应该是 0xc8
由于 go 的内置函数是使用栈来传参的,因此需要一个 gadget 来为 ORW 链恢复栈帧
1 | 0x469e60 mov edi, 0ffffff9ch |
可以使用 ROPgadget 来进行查找:
1 | ROPgadget --binary ./pwn --only "add|ret" | grep "rsp" |
1 | 0x000000000040295a : add rsp, 0x20 ; ret |
我们不能直接在服务端上 write flag,这里有两种思路:
- 重新构建 socket 将 flag 传送到客户端
- 将 flag 拷贝到响应数据中,借用程序的代码发送数据
经过尝试发现这两种方式都不好实现,查看网上 wp 时发现了另一种方式,通过现成的 socket 传输 flag
- 这样会导致结构错误从而使 python 没法处理数据,但是我们可以直接抓包获取 flag
- 除此以外还有另一种方法:
1 | p = remote("127.0.0.1", 23334) |
- 这两个虽然是不同的连接,但 p.recv 仍然可以接受 conn 的数据(可能底层就是抓取数据包)
1 | try: |
于是便可以直接 ORW
完整 exp 如下:
1 | # -*- coding:utf-8 -*- |
BuggyAllocator
1 | GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.6) stable release version 2.35. |
1 | pwn: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=5724ebe3943a39c4ff00f553bc288b5fbb9a2e61, stripped |
- 64位,dynamically,Full RELRO,Canary,NX
漏洞分析
本题目实现了一个简易的堆分配器
分配逻辑如下:
1 | if ( !len ) |
- 长度大于 0x80 使用 ptmalloc,否则使用程序实现的逻辑
1 | pwndbg> telescope 0x11b5ea0 |
- 先申请一个大缓冲区,然后分割为相同大小的小缓冲区
- 对于没有使用的空间则会记录 free chunk 链表
程序会维护一个链表数组,并从链表数组中提取 free chunk:
1 | pwndbg> telescope 0x404420 |
程序会维护一个结构体数组,用于记录已经分配的 chunk:
1 | pwndbg> telescope 0x4044A0 |
释放逻辑如下:
1 | if ( a2 <= 0x80 ) |
- 首先确定目标 chunk 在结构体数组中的位置,然后进行释放
- 在 free chunk 中记录
free_list[order]
,并将 free chunk 记录为新的链表头
该漏洞是一个逻辑漏洞,本质原因是因为大缓冲区有部分空间没有第一时间初始化:
1 | pwndbg> telescope 0xe7aea0 |
如果我们提前在未初始化的空间中写入数据,那么程序就会误以为该空间已经初始化过了,从而将我们写入的空间分配出去
入侵思路
程序没有泄露,但 stdout 处于 bss 段可以被劫持,因此首先我们需要劫持 stdout 来泄露数据:
1 | pwndbg> telescope 0x404420 |
- 申请两次 chunk 即可修改
_IO_2_1_stdout_
泄露完成以后就是高 libc 利用的过程了:
- 劫持 libc GOT
- 劫持 stack
- 劫持 IO
- 劫持 exit_hook
- 劫持 tls_dtor_list_addr
由于我们已经劫持了程序的 free_list,可以进行任意读写,因此这里选择劫持栈
完整 exp 如下:
1 | # -*- coding:utf-8 -*- |