还在因为远程环境或者定制环境没有调试工具而苦恼吗?星盟安全团队给您带来福音啦。
简介
星盟安全团队推出了一款定制化调试工具:https://github.com/Ex-Origin/debug-server。
该工具不需要用户在远程环境或者定制环境中安装一大堆调试工具。利用gdbserver
程序,自动对目标进行Attach
。
除此以外该工具还支持一键启动strace
程序观察目前的系统调用情况,以及或者目前程序的内存映射地址。
主程序使用C语言编写,可以方便的在Linux
系列的系统上进行编译使用,帮您解决跨架构的烦恼。
其使用方法如下所示:
Usage: debug-server [-hmsvn] [-e CMD] [-p PID] [-o CMD]
General:
-e CMD service argv
-p PID attach to PID
-o CMD get pid by popen
-h print help message
-m enable multi-service
-s halt at entry point
-v show debug information
-n disable address space randomization
-u do not limit memory
其中 debug-server
提供了几个简单的接口:
attach(script='')
:attach目标进程,script为传入的gdb预执行脚本。strace()
:strace目标进程,strace信息将由debug-server的日志中输出。address(search:str)
:从/proc/pid/maps
中获得目标进程对应lib库的地址,比如 address(‘libc.so.6’) 就是获得 libc 库的地址。run_service()
:运行服务,该API常用于apache和nginx等网络服务。
依赖
远端环境需要安装 gdbserver
, strace
来保证服务正常。
本地环境需要安装 gdb-multiarch
, pwntools
来保证服务正常。
使用举例
远端环境和本地环境可以是同一个环境,但是为了凸显 debug-server 对于嵌入式的便利性,这里的远端环境使用的是 aarch64 架构的系统。
远程环境使用无桌面环境的 Debian GNU/Linux 12 (bookworm)
aarch64 架构。
本地环境使用带桌面环境的 Ubuntu 24.04 LTS
x86_64 架构。
远程测试的例子如下:
// aarch64-linux-gnu-gcc -g echo.c -o echo
#include <unistd.h>
int main()
{
while(1)
{
char buf[0x100] = {0};
int result = 0;
write(STDOUT_FILENO, "Input: ", 7);
result = read(STDIN_FILENO, buf, sizeof(buf)-1);
write(STDOUT_FILENO, "Output: ", 8);
write(STDOUT_FILENO, buf, result);
}
return 0;
}
其对应的依赖如下:
~ # ldd ./echo
linux-vdso.so.1 (0x0000ffffbd14a000)
libc.so.6 => /lib/libc.so.6 (0x0000ffffbcf30000)
/lib/ld-linux-aarch64.so.1 (0x0000ffffbd10d000)
远程环境输入如下命令对目标程序进行调试服务:
~ # ./debug-server -e ./echo
2024-05-19 18:16:25 | INFO | Start debugging service, pid=160, version=1.3.3
随后本地使用 gdbpwn.py 连接到对应的远程 IP:
$ gdbpwn.py 192.168.1.8
2024-05-19 18:17:28,277 : INFO : Connecting to 192.168.1.8:9545
2024-05-19 18:17:28,282 : INFO : It has connected successfully
2024-05-19 18:17:28,282 : INFO : Start gdb client
随后即可在 exp.py 中插入所需要的调试代码即可:
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
attach_host = '192.168.1.8'
attach_port = 9545
def attach(script=''):
tmp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
gdb_script = re.sub(r'#.*', '',
f'''
define pr
x/16gx $rebase(0x0)
end
b *$rebase(0x0)
''' + '\n' + script)
gdbinit = '/tmp/gdb_script_' + attach_host
script_f = open(gdbinit, 'w')
script_f.write(gdb_script)
script_f.close()
_attach_host = attach_host
if attach_host.find(':') == -1: _attach_host = '::ffff:' + attach_host
tmp_sock.sendto(struct.pack('BB', 0x02, len(gdbinit.encode())) + gdbinit.encode(), (_attach_host, attach_port))
tmp_sock.recvfrom(4096)
tmp_sock.close()
print('attach successfully')
def strace():
tmp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # UDP
_attach_host = attach_host
if attach_host.find(':') == -1: _attach_host = '::ffff:' + attach_host
tmp_sock.sendto(struct.pack('B', 0x03), (_attach_host, attach_port))
tmp_sock.recvfrom(4096)
tmp_sock.close()
print('strace successfully')
def address(search:str)->int:
tmp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
_attach_host = attach_host
if attach_host.find(':') == -1: _attach_host = '::ffff:' + attach_host
tmp_sock.sendto(struct.pack('BB', 0x04, len(search.encode())) + search.encode(), (_attach_host, attach_port))
tmp_recv = tmp_sock.recvfrom(4096)[0]
tmp_sock.close()
return struct.unpack('Q', tmp_recv[2:10])[0]
def run_service():
tmp_sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM, socket.IPPROTO_UDP) # UDP
_attach_host = attach_host
if attach_host.find(':') == -1: _attach_host = '::ffff:' + attach_host
tmp_sock.sendto(struct.pack('B', 0x06), (_attach_host, attach_port))
tmp_sock.recvfrom(4096)
tmp_sock.close()
print('run_service successfully')
'''
Your Code
'''
其中 Your Code
指的是我们要写的调试代码。
简单的调试例子
下面我举个调试代码的例子,只需要把下面的代码替换成 Your Code
对应的部分即可,该例子演示了如何简单的调试一个程序。
sh = remote(attach_host, 9541)
sh.recvuntil(b'Input: ')
attach()
sh.sendline(b'Hello world')
sh.interactive()
其本地 exp.py 输出信息如下所示:
$ python3 exp.py
[+] Opening connection to 192.168.1.8 on port 9541: Done
[DEBUG] Received 0x7 bytes:
b'Input: '
attach successfully
[DEBUG] Sent 0xc bytes:
b'Hello world\n'
[*] Switching to interactive mode
$
本地 gdbpwn.py 输出信息如下所示:
$ gdbpwn.py 192.168.1.8
2024-05-19 18:17:28,277 : INFO : Connecting to 192.168.1.8:9545
2024-05-19 18:17:28,282 : INFO : It has connected successfully
2024-05-19 18:17:28,282 : INFO : Start gdb client
2024-05-19 18:21:49,450 : INFO : Receive COMMAND_GDBSERVER_ATTACH
pwndbg: loaded 157 pwndbg commands and 46 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $base, $ida GDB functions (can be used with print/break)
Remote debugging using ::ffff:192.168.1.8:9549
...
Breakpoint 1 at 0xaaaab8580000
...
─────────────────────[ DISASM / aarch64 / set emulate on ]──────────────────────
► 0xffff9a219e64 <read+36> svc #0 <SYS_read>
fd: 0 (socket:[3088])
buf: 0xffffd4b72538 ◂— 0
nbytes: 0xff
0xffff9a219e68 <read+40> mov x19, x0
0xffff9a219e6c <read+44> cmn x0, #1, lsl #12
0xffff9a219e70 <read+48> b.hi #read+148 <read+148>
0xffff9a219e74 <read+52> mov x0, x19
0xffff9a219e78 <read+56> ldp x19, x20, [sp, #0x10]
0xffff9a219e7c <read+60> ldp x29, x30, [sp], #0x30
0xffff9a219e80 <read+64> ret
0xffff9a219e84 <read+68> mov x20, x2
0xffff9a219e88 <read+72> str x21, [sp, #0x20]
0xffff9a219e8c <read+76> mov x21, x1
───────────────────────────────────[ STACK ]────────────────────────────────────
...
pwndbg>
自动下断点
下面演示一个自动下断点的例子。首先查看一下 echo 程序的汇编代码:
$ aarch64-linux-gnu-objdump -d ./echo
...
0000000000000814 <main>:
814: d10483ff sub sp, sp, #0x120
818: a9117bfd stp x29, x30, [sp, #272]
81c: 910443fd add x29, sp, #0x110
820: f00000e0 adrp x0, 1f000 <__FRAME_END__+0x1e62c>
824: f947f400 ldr x0, [x0, #4072]
828: f9400001 ldr x1, [x0]
82c: f90087e1 str x1, [sp, #264]
830: d2800001 mov x1, #0x0 // #0
834: 910023e0 add x0, sp, #0x8
838: 4f000400 movi v0.4s, #0x0
83c: ad000000 stp q0, q0, [x0]
840: ad010000 stp q0, q0, [x0, #32]
844: ad020000 stp q0, q0, [x0, #64]
848: ad030000 stp q0, q0, [x0, #96]
84c: ad040000 stp q0, q0, [x0, #128]
850: ad050000 stp q0, q0, [x0, #160]
854: ad060000 stp q0, q0, [x0, #192]
858: ad070000 stp q0, q0, [x0, #224]
85c: b90007ff str wzr, [sp, #4]
860: d28000e2 mov x2, #0x7 // #7
864: 90000000 adrp x0, 0 <__abi_tag-0x278>
868: 91238001 add x1, x0, #0x8e0
86c: 52800020 mov w0, #0x1 // #1
870: 97ffff98 bl 6d0 <write@plt>
874: 910023e0 add x0, sp, #0x8
878: d2801fe2 mov x2, #0xff // #255
87c: aa0003e1 mov x1, x0
880: 52800000 mov w0, #0x0 // #0
884: 97ffff9b bl 6f0 <read@plt>
888: b90007e0 str w0, [sp, #4]
88c: d2800102 mov x2, #0x8 // #8
890: 90000000 adrp x0, 0 <__abi_tag-0x278>
894: 9123a001 add x1, x0, #0x8e8
898: 52800020 mov w0, #0x1 // #1
89c: 97ffff8d bl 6d0 <write@plt>
8a0: b98007e1 ldrsw x1, [sp, #4]
8a4: 910023e0 add x0, sp, #0x8
8a8: aa0103e2 mov x2, x1
8ac: aa0003e1 mov x1, x0
8b0: 52800020 mov w0, #0x1 // #1
8b4: 97ffff87 bl 6d0 <write@plt>
8b8: d503201f nop
8bc: 17ffffde b 834 <main+0x20>
这次我们的目标是在 89c
处下断点,那么其对应的调试代码如下:
sh = remote(attach_host, 9541)
sh.recvuntil(b'Input: ')
attach(
f'''
b *{address("echo")+0x89c}
c
''')
sh.sendline(b'Hello world')
sh.interactive()
其在 89c
处下断点后,立刻执行了 c
(continue)指令,最终结果是其可以一步执行到 89c
处,而不用每次手动调节,这极大加快了调试进度。
本地 gdbpwn.py 输出信息如下所示:
Breakpoint 1 at 0xaaaae31d0000
Breakpoint 2 at 0xaaaae31d089c: file echo.c, line 12.
Breakpoint 2, 0x0000aaaae31d089c in main () at echo.c:12
12 write(STDOUT_FILENO, "Output: ", 8);
...
─────────────────────[ DISASM / aarch64 / set emulate on ]──────────────────────
► 0xaaaae31d089c <main+136> bl #write@plt <write@plt>
fd: 1 (socket:[3150])
buf: 0xaaaae31d08e8 ◂— adr x15, #0xaaaae32b9793 /* 'Output: ' */
n: 8
0xaaaae31d08a0 <main+140> ldrsw x1, [sp, #4]
0xaaaae31d08a4 <main+144> add x0, sp, #8
0xaaaae31d08a8 <main+148> mov x2, x1
0xaaaae31d08ac <main+152> mov x1, x0
0xaaaae31d08b0 <main+156> mov w0, #1
0xaaaae31d08b4 <main+160> bl #write@plt <write@plt>
0xaaaae31d08b8 <main+164> nop
0xaaaae31d08bc <main+168> b #main+32 <main+32>
0xaaaae31d08c0 <_fini> nop
0xaaaae31d08c4 <_fini+4> stp x29, x30, [sp, #-0x10]!
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
lib库下断点
通常 gdb 对于 php 和 nginx 等需要导入 lib 库的方式难以下断点进行调试,但是本调试模式可以弥补该缺点。
举个例子,现在需要在 write 函数入口处第 8 字节的偏移处下个断点,其 libc.so.6 的汇编如下所示:
$ $ aarch64-linux-gnu-objdump -d libc.so.6
...
00000000000d9f10 <__write@@GLIBC_2.17>:
d9f10: a9bd7bfd stp x29, x30, [sp, #-48]!
d9f14: d0000663 adrp x3, 1a7000 <getdate_err@@GLIBC_2.17+0x338>
d9f18: 910003fd mov x29, sp
d9f1c: 3967a063 ldrb w3, [x3, #2536]
那么该调试框架只需要进行如下编写即可:
sh = remote(attach_host, 9541)
sh.recvuntil(b'Input: ')
attach(
f'''
b *{address("libc.so.6")+0xd9f18}
c
''')
sh.sendline(b'Hello world')
sh.interactive()
其首先获得libc.so.6
的地址,随后向对应的偏移下断点,整个过程自动实现,方便了对于 lib 库的调试,尤其是对于没有符号函数的 lib 库,其便利效果更突出。
本地 gdbpwn.py 输出信息如下所示:
Breakpoint 1 at 0xaaaab94f0000
Breakpoint 2 at 0xffffb6b09f18
Breakpoint 2, 0x0000ffffb6b09f18 in write () from target:/lib/libc.so.6
...
─────────────────────[ DISASM / aarch64 / set emulate on ]──────────────────────
► 0xffffb6b09f18 <write+8> mov x29, sp FP => 0xffffc4e3bd90
0xffffb6b09f1c <write+12> ldrb w3, [x3, #0x9e8]
0xffffb6b09f20 <write+16> stp x19, x20, [sp, #0x10]
0xffffb6b09f24 <write+20> sxtw x19, w0
0xffffb6b09f28 <write+24> cbz w3, #write+68 <write+68>
0xffffb6b09f2c <write+28> mov x0, x19 X0 => 1
0xffffb6b09f30 <write+32> mov x8, #0x40 X8 => 0x40
0xffffb6b09f34 <write+36> svc #0
0xffffb6b09f38 <write+40> mov x19, x0
0xffffb6b09f3c <write+44> cmn x0, #1, lsl #12
0xffffb6b09f40 <write+48> b.hi #write+148 <write+148>
───────────────────────────────────[ STACK ]────────────────────────────────────
调试网络应用程序
针对 nginx 和 sshd 等网络应用程序,由于其不是通过 标准输入输出流 进行交互,而是通过 socket 交互,这类网络应用程序的调试过程往往十分繁琐,并且出现问题时还需要重启服务。
这里我们演示使用 debug-server 来调试这类网络程序。
远程环境输入如下命令对目标程序进行调试:
./debug-server -e /usr/sbin/sshd -o 'pidof sshd'
对应的调试的代码如下:
run_service()
time.sleep(1)
attach()
sh = remote(attach_host, 22)
sh.send(b'aaaa')
sh.interactive()
在启动该调试前,需要确保当前系统没有已经启动的 sshd 服务。
其中 run_service()
函数会执行 /usr/sbin/sshd
,随后 time.sleep(1)
让调试脚本暂停 1 秒以确保 sshd 服务启动成功,随后 attach()
函数对目标进程进行 attach,其进程pid的定位方式是通过 popen 函数执行 pidof sshd
而得到。对应了 -o 'pidof sshd'
参数。
通过该种调试方式,可以极大简化网络应用调试流程。
禁用随机化
远程调试服务启动时,加上 -n
参数即可关闭目标程序的随机化。
./debug-server -n -e ./echo
该操作仅对目标程序有效,并不会影响系统的随机化规则,因此其具有更高的安全性。
strace举例
对于 strace 支持,只需要将 attach()
函数替换成 strace()
函数即可。
其对应的调试代码如下:
sh = remote(attach_host, 9541)
sh.recvuntil(b'Input: ')
strace()
sh.sendline(b'Hello world')
sh.interactive()
其对应的远程输出日志如下:
2024-05-19 21:02:46 | INFO | Strace start, pid=389
strace: Process 388 attached
read(0, "Hello world\n", 255) = 12
write(1, "Output: ", 8) = 8
write(1, "Hello world\n", 12) = 12
write(1, "Input: ", 7) = 7
read(0,
通过该种方式,可以观察指定代码的系统调用情况,方便研究人员理解程序。
入口处暂停
针对某些无 IO 的程序,或者是需要在入口函数之前进行修改的程序,使用 pwntools 进行调试时会十分麻烦。
本 debug-server
可以使用 -s
参数使得程序在入口处暂停,这样可以便利的对程序进行特异性初始化,尤其对于逆向某些无IO的程序很有帮助。
其远程调试服务启动的命令如下:
./debug-server -s -e ./echo
对应的调试脚本如下:
sh = remote(attach_host, 9541)
attach(
f'''
b main
c
c
''')
sh.recvuntil(b'Input: ')
sh.sendline(b'Hello world')
sh.interactive()
由于 debug-server
采用发送 SIGSTOP
信号的方式暂停程序,因此第一个 c
(continue)命令是处理 SIGSTOP
信号的,第二个 c
命令才会让程序继续执行下去。
本地 gdbpwn.py 输出信息如下所示:
Breakpoint 1 at 0xaaaaac560000
Breakpoint 2 at 0xaaaaac560820: file echo.c, line 5.
Program received signal SIGCONT, Continued.
0x0000ffff8a2d2980 in ?? () from target:/lib/ld-linux-aarch64.so.1
...
Breakpoint 2, main () at echo.c:5
...
─────────────────────[ DISASM / aarch64 / set emulate on ]──────────────────────
► 0xaaaaac560820 <main+12> adrp x0, #0xaaaaac57f000 X0 => 0xaaaaac57f000 ◂— 0
0xaaaaac560824 <main+16> ldr x0, [x0, #0xfe8] X0 => 0xffff8a2f7b88 (__stack_chk_guard) ◂— 0x657551da6d76f000
0xaaaaac560828 <main+20> ldr x1, [x0] X1 => 0x657551da6d76f000
0xaaaaac56082c <main+24> str x1, [sp, #0x108]
0xaaaaac560830 <main+28> mov x1, #0 X1 => 0
0xaaaaac560834 <main+32> add x0, sp, #8 X0 => 0xffffdf9036d8 —▸ 0xffff8a2f1f60 ◂— 0
0xaaaaac560838 <main+36> movi v0.4s, #0
0xaaaaac56083c <main+40> stp q0, q0, [x0]
0xaaaaac560840 <main+44> stp q0, q0, [x0, #0x20]
0xaaaaac560844 <main+48> stp q0, q0, [x0, #0x40]
0xaaaaac560848 <main+52> stp q0, q0, [x0, #0x60]
───────────────────────────────[ SOURCE (CODE) ]────────────────────────────────
如果不开启入口处暂停功能的话,程序则无法在入口处停下,其原因在与 IO 速度过快,如果不暂停程序,IO的速度始终大于调试的速度,使得无法调试 IO 过程之前的代码。
开源支持
本 debug-server
使用 MIT 开源证书,欢迎各位感兴趣的极客们加入维护。
原文链接:https://blog.eonew.cn/2024/05/19/Introduction-to-the-debug-server-Automated-Debugging-Tool/