还在因为远程环境或者定制环境没有调试工具而苦恼吗?星盟安全团队给您带来福音啦。

简介

星盟安全团队推出了一款定制化调试工具:https://github.com/Ex-Origin/debug-server

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/