HWS夏令营 之 GDB调一切

HWS夏令营的课程分为三个部分,IoT固件安全,linux内核安全、IoT硬件安全。GDB作为一个出色的调试工具,也在三个部分的课程中频频登场,说哪都有他一点也不过分。三个部分中,我们用GDB依次调试了:arm,mips等与本机x86(x64)不同架构的linux用户态应用程序、x86(x64)的linux内核、STM32裸机程序。前两者目标的运行方法是qemu,后者是用的STM32单板以及JLINK仿真器。

平日我们在linux中调试用户态程序时,直接使用gdb命令即可,但是如果是我们无法在目标系统上执行gdb,或者目标系统并不直接支持程序状态的监视,那么我们怎么去调试呢?gdb支持远程调试,即gdb这个client和远程的gdbserver通信,所以只要在目标系统之下的层面跑起来gdbserver即可。以下三种目标都是这种情况,你无法直接使用gdb和二进制文件就将程序跑起来,而是需要一个能监视目标程序的底层系统,并且这个系统需要支持gdbserver。

调试arm,mips架构linux的用户态程序

当然你可以使用arm,mips的真机,然后在上面安装gdb或者拷贝一个gdbserver进去,比如树莓派。不过qemu可以帮我们省去购买真机开销,而且对于一些特殊情况的程序,可能真机反倒会带来麻烦,先看以下三个目标:

附件:firmware.zip

  file typo 
typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped
  file embedded_heap 
embedded_heap: ELF 32-bit MSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
  file tpra_sr20v1.bin 
tpra_sr20v1.bin: data
  1. typo: 只有一个elf文件,静态链接
  2. embedded_heap: elf动态链接,以及相关的动态链接库
  3. tpra_sr20v1.bin: 貌似是整个固件,需要提取文件系统

所以如果仅仅是提供了elf文件,则可以使用qemu-user直接启动。如果给出了文件系统,则可以使用qemu-system。不过二者对于用户态程序来说并没有什么本质的区别,也可以将给的单个的elf文件里扔到文件系统然后用qemu-system,也可以将文件系统中的模目标要pwn的程序拿出来用qemu-user。具体使用什么策略要看目标的情况,方便就好。

qemu-user

typo

对于typo这种静态链接的程序直接使用qemu-arm即可,qemu-user这种模式下,-g选项是开始gdb调试:

  qemu-arm -h | grep gdb
-g port       QEMU_GDB          wait gdb connection to 'port'
  qemu-arm -g 1234 ./typo

这样即可开启一个1234端口的gdbserver,然后即可在另一个terminal中使用gdb的target remote命令进行连接,不过在连接之前先设置目标的指令集和大小端,然后连接即可开始调试:

  gdb-multiarch -q ./typo
pwndbg> set architecture arm
The target architecture is assumed to be arm
pwndbg> set endian little
The target is assumed to be little endian
pwndbg> target remote :1234
Remote debugging using :1234
0x00008b98 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 R0   0x0
 R1   0xf6fff1cc ◂— './typo'
 R2   0x0
 R3   0x0
 R4   0x0
 R5   0x0
 R6   0x0
 R7   0x0
 R8   0x0
 R9   0x0
 R10  0x8af6c —▸ 0xa1e94 —▸ 0x6ff44 —▸ 0x7b918 ◂— andeq  r0, r0, r3, asr #32 /* 'C' */
 R11  0x0
 R12  0x0
 SP   0xf6fff000 ◂— 0x1
 PC   0x8b98 ◂— mov    fp, #0
───────────────────────────────────[ DISASM ]───────────────────────────────────
  0x8b98    mov    fp, #0
   0x8b9c    mov    lr, #0
   0x8ba0    pop    {r1}
   0x8ba4    mov    r2, sp
   0x8ba8    str    r2, [sp, #-4]!
   0x8bac    str    r0, [sp, #-4]!
   0x8bb0    ldr    ip, [pc, #0x10]
   0x8bb4    str    ip, [sp, #-4]!
   0x8bb8    ldr    r0, [pc, #0xc]
   0x8bbc    ldr    r3, [pc, #0xc]
   0x8bc0    bl     #0x9ebc
───────────────────────────────────[ STACK ]───────

embedded_heap

如果带动态链接库的二进制文件,可以使用qemu-user的-L选项使用当前目录,然后动态库存放于当目录的lib子目录中:

  file embedded_heap
embedded_heap: ELF 32-bit MSB shared object, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
  ls *
embedded_heap

lib:
ld-uClibc-0.9.33.2.so  ld-uClibc.so.0  libc.so.0  libuClibc-0.9.33.2.so
  qemu-mips -L ./ ./embedded_heap 
/lib/ld-uClibc.so.0: Invalid ELF image for this architecture

这里报错:Invalid ELF image,是因为给的文件是从文件系统中提取出来的,提取的过程把链接文件弄错了。正常来说ld-uClibc.so.0应该是一个链接文件,指向ld-uClibc-0.9.33.2.solibc.so.0也一样,我们可以看一下这俩文件:

  cat ./lib/libc.so.0 
./lib/libc.so.0
  cat ./lib/ld-uClibc.so.0 
./lib/ld-uClibc.so.0

可以看到这俩文件压根就是文本文件,所以把这俩删掉,然后把另外两个动态库的实体改成链接文件的名即可:

  rm -rf libc.so.0 
  rm -rf ld-uClibc.so.0 
  mv libuClibc-0.9.33.2.so libc.so.0
  mv ld-uClibc-0.9.33.2.so ld-uClibc.so.0
  cd ..
  qemu-mips -g 1234 -L ./ ./embedded_heap 

然后仍然是设置目标的指令集和大小端,最后连接上即可:

  gdb-multiarch -q ./embedded_heap
pwndbg: loaded 180 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./embedded_heap...(no debugging symbols found)...done.
pwndbg> set architecture mips
The target architecture is assumed to be mips
pwndbg> set endian big
The target is assumed to be big endian
pwndbg> target remote :1234
Remote debugging using :1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
Reading symbols from /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0...(no debugging symbols found)...done.
0x767d4f80 in _start () from /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────
 V0   0x0
 V1   0x0
 A0   0x0
 A1   0x0
 A2   0x0
 A3   0x0
 T0   0x0
 T1   0x0
 T2   0x0
 T3   0x0
 T4   0x0
 T5   0x0
 T6   0x0
 T7   0x0
 T8   0x0
 T9   0x0
 S0   0x0
 S1   0x0
 S2   0x0
 S3   0x0
 S4   0x0
 S5   0x0
 S6   0x0
 S7   0x0
 S8   0x0
 FP   0x76febfd0 ◂— 0x1
 SP   0x76febfd0 ◂— 0x1
 PC   0x767d4f80 (_start) ◂— move   $t9, $ra
─────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────
  0x767d4f80 <_start>       move   $t9, $ra
   0x767d4f84 <_start+4>     bal    _start+12 <0x767d4f8c>
   0x767d4f88 <_start+8>     nop    
   0x767d4f8c <_start+12>    lui    $gp, 2
   0x767d4f90 <_start+16>    addiu  $gp, $gp, -0x1f7c
   0x767d4f94 <_start+20>    addu   $gp, $gp, $ra
   0x767d4f98 <_start+24>    move   $ra, $t9
   0x767d4f9c <_start+28>    lw     $a0, -0x7fe8($gp)
   0x767d4fa0 <_start+32>    sw     $a0, -0x7ff0($gp)
   0x767d4fa4 <_start+36>    move   $a0, $sp
   0x767d4fa8 <_start+40>    addiu  $sp, $sp, -0x10
─────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────
00:0000 fp sp  0x76febfd0 ◂— 0x1
01:0004        0x76febfd4 —▸ 0x76fec18c ◂— './embedded_heap'
02:0008        0x76febfd8 ◂— 0x0
03:000c        0x76febfdc —▸ 0x76fec19c ◂— '_=/usr/bin/qemu-mips'
04:0010        0x76febfe0 —▸ 0x76fec1b1 ◂— 0x4c535f43 ('LS_C')
05:0014        0x76febfe4 —▸ 0x76fec739 ◂— 'LSCOLORS=Gxfxcxdxbxegedabagacad'
06:0018        0x76febfe8 —▸ 0x76fec759 ◂— 'LC_CTYPE=en_US.UTF-8'
07:001c        0x76febfec —▸ 0x76fec76e ◂— 'LESS=-R'
───────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────
  f 0 767d4f80 _start
────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x767d4000 0x767db000 r-xp     7000 0      [linker]
0x767d4000 0x767db000 r-xp     7000 0      /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0
0x767db000 0x767ea000 ---p     f000 6000   [linker]
0x767db000 0x767ea000 ---p     f000 6000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0
0x767ea000 0x767eb000 r--p     1000 6000   [linker]
0x767ea000 0x767eb000 r--p     1000 6000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0
0x767eb000 0x767ec000 rw-p     1000 7000   [linker]
0x767eb000 0x767ec000 rw-p     1000 7000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/lib/ld-uClibc.so.0
0x76fea000 0x76fed000 rw-p     3000 0      [stack]
0x76fed000 0x76fef000 r-xp     2000 0      /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/embedded_heap
0x76fef000 0x76ffe000 ---p     f000 1000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/embedded_heap
0x76ffe000 0x76fff000 r--p     1000 1000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/embedded_heap
0x76fff000 0x77000000 rw-p     1000 2000   /mnt/hgfs/桌面/hws/gdb调一切/embedded_heap/embedded_heap
pwndbg> 

总结

  1. 可以用pwndbg插件的vmmap来观察程序内存,这里的内存是qemu模拟出来的,宿主机上并不存在embedded_heap这个进程,因为gdbserver本身就是qemu自己提供的支持,所以他可以给gdb一个模拟出来的假的内存布局。
  2. 经过测试,peda、gef、pwndbg三款插件,pwndbg对于qemu-user的gdbserver支持是最好的,另两个很多东西都看不到。
  3. 可以看到这里识别embedded_heap的内存中没有libc.so.0,但是在qemu模拟的内存中,libc的确存在,需要自行在目标程序中打断,然后分析libc地址,如果采用qemu-system方法启动一整个文件系统,将不会出现这种情况。
  4. 使用qemu-user模式下的自带的gdbserver,gdb调试时无法使用control+c发送SIGINT,也就无法使程序随意断下,必须通过手动下断点的方式调试程序。

qemu-system

面对第三个固件tpra_sr20v1.bin我们首先需要使用binwalk来进行分析以及解包:

➜  binwalk -Me tpra_sr20v1.bin 

这里发现我kail以及在mac上安装的binwalk无法完整的解开这个固件,会缺东西,尝试在ubuntu中安装后,即可正常解包:

  git clone https://gitee.com/h4lo1/binwalk.git
  sudo python setup.py install
  sudo apt install python-lzma

这样就可以直接将固件中包含的squashfs文件系统直接解包出来,对于这种带文件系统固件且没有在固件中提取出linux内核的bzImage文件的情况下,我们一般采用qemu-system启动一个和固件指令集相同的linux系统,然后将固件的文件系统打包扔进去并chroot,然后即可使用该指令集下静态编译好的gdbserver对目标程序进行附加,最后在宿主机使用gdb连接远程即可开始调试。

对与这种一个整包的固件可以使用binwalk来识别其指令集:

  binwalk -A tpra_sr20v1.bin            

DECIMAL       HEXADECIMAL     DESCRIPTION
------------------------------------------------------------------
15556         0x3CC4          ARM instructions, function prologue
15580         0x3CDC          ARM instructions, function prologue

也可以解包后使用file命令查看文件系统中的二进制文件:

  file ./usr/bin/tddp 
./usr/bin/tddp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

参考重现 TP-Link SR20 本地网络远程代码执行漏洞,我们虽然可以通过qemu-user启动目标程序,但是无法触发漏洞,所以要使用qemu-system搭建完整系统。即https://people.debian.org/~aurel32/qemu/armhf/,我们需要三个文件:

  • vmlinuz-3.2.0-4-vexpress:zImage格式压缩的linux内核
  • initrd.img-3.2.0-4-vexpress:ramdisk镜像,不可持久化
  • debian_wheezy_armhf_standard.qcow2:文件系统镜像,修改内容后可以持久化保存

因为要和qemu虚拟机使用网络通信才能方便的把固件的文件系统送进qemu,所以直接在ubuntu中使用qemu会更方便,首先要配置虚拟网卡:

$ sudo tunctl -t tap0 -u `whoami`  # 为了与 QEMU 虚拟机通信,添加一个虚拟网卡
$ sudo ifconfig tap0 1.1.1.1/24 # 为添加的虚拟网卡配置 IP 地址

然后可以按照如下命令启动qemu:

qemu-system-arm \
-M vexpress-a9 \
-kernel vmlinuz-3.2.0-4-vexpress \
-initrd initrd.img-3.2.0-4-vexpress \
-drive if=sd,file=debian_wheezy_armhf_standard.qcow2 \
-append "root=/dev/mmcblk0p2 console=ttyAMA0" \
-net nic \
-net tap,ifname=tap0,script=no,downscript=no \
-nographic

然后在qemu以用户密码root:root登录后,里配一下网卡即可:

root@debian-armhf:~$ ifconfig eth0 1.1.1.2
root@debian-armhf:~$ ping 1.1.1.1
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.
64 bytes from 1.1.1.1: icmp_req=1 ttl=64 time=4.59 ms

然后把gdbserver拷贝到刚才binwalk解出来的文件系统中,并一起打包,使用python启动一个简单的web服务器

  cp ../gdbserver/gdbserver-7.7.1-armhf-eabi5-v1-sysv ./squashfs-root
  tar -cvf squashfs-root.tar ./squashfs-root
  python -m SimpleHTTPServer

回到qemu中,使用wget下载文件系统并解包,然后挂载proc和dev文件系统,最后chroot将固件的文件系统设置为根目录:

root@debian-armhf:~$ wget http://1.1.1.1:8000/squashfs-root.tar
root@debian-armhf:~$ tar -xvf ./squashfs-root.tar
root@debian-armhf:~$ cd squashfs-root/
root@debian-armhf:~$ chmod +x ./gdbserver-7.7.1-armhf-eabi5-v1-sysv 
root@debian-armhf:~/squashfs-root$ mount -o bind /dev ./dev/
root@debian-armhf:~/squashfs-root$ mount -t proc /proc/ ./proc/
root@debian-armhf:~/squashfs-root$ chroot . sh

chroot后即进入以固件的文件系统为根目录,以固件的shell为当前shell,以固件的lib目录为当前依赖的lib的执行环境,即可使用两种方式将目标程序启动并挂上gdbserver:

第一种:直接使用gdbserver启动程序,设置端口参数,程序将断在入口点:

/ # ./gdbserver-7.7.1-armhf-eabi5-v1-sysv :1234 /usr/bin/tddp 

第二种:使用gdbserver的--attach选项,根据pid附加到已经启动的进程,会断在程序当前运行的状态处

/ # /usr/bin/tddp &
/ # [tddp_taskEntry():151] tddp task start
/ # ps | grep tddp
 2457 root      1352 S    /usr/bin/tddp
 2459 root      1324 S    grep tddp
/ # ./gdbserver-7.7.1-armhf-eabi5-v1-sysv :1234 --attach 2457
Attached; pid = 2457
Listening on port 1234
Remote debugging from host 1.1.1.1

两者均可在ubuntu使用gdb客户端进行连接:

  gdb-multiarch -q ./usr/bin/tppd
pwndbg: loaded 180 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
./usr/bin/tppd: No such file or directory.
pwndbg> set architecture arm
The target architecture is assumed to be arm
pwndbg> set endian little
The target is assumed to be little endian
pwndbg> target remote 1.1.1.2:1234
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────[ REGISTERS ]──────────────────────────────────
 R0   0x4
 R1   0x7efa1ccc ◂— 0x8
 R2   0x0
 R3   0x0
 R4   0x7efa1cc4 ◂— 0x239
 R5   0x7efa1e14 —▸ 0x7efa1f01 ◂— '/usr/bin/tddp'
 R6   0x1
 R7   0x8e
 R8   0x8da8 ◂— mov    ip, sp
 R9   0x971c ◂— push   {fp, lr}
 R10  0x7efa1d88 ◂— 0x0
 R11  0x7efa1d6c —▸ 0x9750 ◂— bl     #0x16d40 /* 'z5' */
 R12  0x76edd4bc ◂— push   {r3, r4, r7, lr}
 SP   0x7efa1ca0 ◂— 0x0
 PC   0x76edd4c8 ◂— svc    #0
───────────────────────────────────[ DISASM ]────────────────────────────────────
  0x76edd4c8    svc    #0 <SYS__newselect>
        r0: 0x4
        r1: 0x7efa1ccc ◂— 0x8
        r2: 0x0
        r3: 0x0
   0x76edd4cc    cmn    r0, #0x1000
   0x76edd4d0    mov    r4, r0
   0x76edd4d4    bls    #0x76edd4e8
 
   0x76edd4d8    rsb    r4, r4, #0
   0x76edd4dc    bl     #0x76eda4d8
 
   0x76edd4e0    str    r4, [r0]
   0x76edd4e4    mvn    r4, #0
   0x76edd4e8    mov    r0, r4
   0x76edd4ec    pop    {r3, r4, r7, pc}
   0x76edd4f0    push   {r3, r4, r7, lr}
────────────────────────────────────[ STACK ]────────────────────────────────────
00:0000 sp  0x7efa1ca0 ◂— 0x0
01:0004     0x7efa1ca4 —▸ 0x17a6160 ◂— 0x0
02:0008     0x7efa1ca8 —▸ 0x7efa1f01 ◂— '/usr/bin/tddp'
03:000c     0x7efa1cac —▸ 0x9608 ◂— str    r0, [fp, #-0x1c]
04:0010     0x7efa1cb0 —▸ 0x7efa1cc4 ◂— 0x239
05:0014     0x7efa1cb4 ◂— 0x0
... 
07:001c     0x7efa1cbc ◂— 0x1
──────────────────────────────────[ BACKTRACE ]──────────────────────────────────
  f 0 76edd4c8
─────────────────────────────────────────────────────────────────────────────────

使用qemu-system过程虽然有些繁琐:

  1. 对于固件,要解包,拷贝调试器,可能还要修改或者patch,最后打包
  2. 对于本机,要配置网卡,架设Web服务器
  3. 对于qemu,要下载对应架构的执行环境,启动后要配置网卡,下载固件,挂载文件系统,最后chroot
  4. 对于目标程序,要想办法正常启动起来,然后用gdb挂上

在执行环境的层次上,可以用如下图表示:

image

虽然繁琐,但是有如下两处优点:第一,支持control+c发送SIGINT信号将程序断下:

pwndbg> c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x76edd4c8 in ?? ()

第二,在映射了qemu本机的proc伪文件系统后对于vmmap的支持更好:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x8000    0x1a000 r-xp    12000 0      /usr/bin/tddp
   0x21000    0x22000 rw-p     1000 11000  /usr/bin/tddp
 0x17a6000  0x17bd000 rw-p    17000 0      [heap]
0x76ebb000 0x76ebd000 r-xp     2000 0      /lib/libdl.so.0
0x76ebd000 0x76ec4000 ---p     7000 0      
0x76ec4000 0x76ec5000 r--p     1000 1000   /lib/libdl.so.0
0x76ec5000 0x76ec6000 rw-p     1000 0      
0x76ec6000 0x76f2b000 r-xp    65000 0      /lib/libc.so.0
0x76f2b000 0x76f33000 ---p     8000 0      
0x76f33000 0x76f34000 r--p     1000 65000  /lib/libc.so.0
0x76f34000 0x76f35000 rw-p     1000 66000  /lib/libc.so.0
0x76f35000 0x76f3a000 rw-p     5000 0      
0x76f3a000 0x76f45000 r-xp     b000 0      /lib/libpthread.so.0
0x76f45000 0x76f4c000 ---p     7000 0      
0x76f4c000 0x76f4d000 r--p     1000 a000   /lib/libpthread.so.0
0x76f4d000 0x76f52000 rw-p     5000 b000   /lib/libpthread.so.0
0x76f52000 0x76f54000 rw-p     2000 0      
0x76f54000 0x76f63000 r-xp     f000 0      /lib/libm.so.0
0x76f63000 0x76f6b000 ---p     8000 0      
0x76f6b000 0x76f6c000 r--p     1000 f000   /lib/libm.so.0
0x76f6c000 0x76f6d000 rw-p     1000 10000  /lib/libm.so.0
0x76f6d000 0x76fa1000 r-xp    34000 0      /usr/lib/liblua.so.5.1.4
0x76fa1000 0x76fa8000 ---p     7000 0      
0x76fa8000 0x76fa9000 rw-p     1000 33000  /usr/lib/liblua.so.5.1.4
0x76fa9000 0x76fb0000 r-xp     7000 0      /lib/libuci.so
0x76fb0000 0x76fb7000 ---p     7000 0      
0x76fb7000 0x76fb8000 rw-p     1000 6000   /lib/libuci.so
0x76fb8000 0x76fbd000 r-xp     5000 0      /lib/ld-uClibc.so.0
0x76fc3000 0x76fc4000 rw-p     1000 0      
0x76fc4000 0x76fc5000 r--p     1000 4000   /lib/ld-uClibc.so.0
0x76fc5000 0x76fc6000 rw-p     1000 5000   /lib/ld-uClibc.so.0
0x7ef81000 0x7efa2000 rw-p    21000 0      [stack]
0xffff0000 0xffff1000 r-xp     1000 0      [vectors]

这种调试方法中qemu只是帮我们搭起了一个可以运行目标架构程序的执行环境,gdbserver是我们自己启动的,与qemu无关。

调试linux内核

在没有qemu之前,对于linux内核的调试是需要两台机器的,一台调试另一台,二者用串口线相连。有了qemu之后,一切都方便了许多,qemu提供了对应运行在其上面的程序的gdb调试接口,并且对于最简单的调试来说,我们只需要准备内核镜像以及根文件系统即可:

基本环境中有如下文件:

  • bzImage: 压缩的linux内核
  • vmlinux: 未压缩的ELF格式的linux内核,可以使用extract-vmlinux解压bzImage得到
  • rootfs.img: 根目录的文件系统
  • dev_helper.ko: 编译好的驱动模块,也是要分析的目标
  • startQemu.sh: qemu的启动脚本

启动脚本内容如下:

qemu-system-x86_64 \
-m 2G \
-kernel ./bzImage \
-drive file=./rootfs.img \
-append "console=ttyS0 root=/dev/sda earlyprintk=serial nokaslr" \
-nographic \
-s

其中-s参数为启用gdb调试,而且默认将gdbserver开启在1234端口:

  squashfs-root qemu-system-x86_64 -h | grep gdb
-gdb dev        wait for gdb connection on 'dev'
-s              shorthand for -gdb tcp::1234

启动qemu后可以用户密码root:空登录,然后即可在宿主机上使用gdb进行调试了

  gdb -q ./vmlinux
pwndbg: loaded 180 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./vmlinux...done.
pwndbg> target remote :1234
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────
 RAX  0xffffffff832cee10 (default_idle) ◂— push   r14 /* 0x5355544155415641 */
 RBX  0x0
 RCX  0xffffffff8124abe1 (rcu_dynticks_eqs_enter+33) ◂— 0xc10f3e00000002b8
 RDX  0x1ffffffff07c3970
 RDI  0xffff88806d22a978 ◂— 0xc60 /* '`\x0c' */
 RSI  0x4
 R8   0xc5e
 R9   0xffffed100da45530 ◂— 0
 R10  0xffffed100da4552f ◂— 0
 R11  0xffff88806d22a97b ◂— 0
 R12  0xffffffff83e1cb80 (init_task) ◂— 0x80000000
 R13  0x0
 R14  0x0
 R15  0xdffffc0000000000
 RBP  0xfffffbfff07c3970 ◂— 0
 RSP  0xffffffff83e07dc0 (init_thread_union+32192) ◂— 0x0
 RIP  0xffffffff832cee27 (default_idle+23) ◂— mov    r13d, dword ptr gs:[rip + 0x7cd49301] /* 0x7cd493012d8b4465 */
──────────────────────────────────────────────[ DISASM ]───────────────────────────────────────────────
  0xffffffff832cee27 <default_idle+23>    mov    r13d, dword ptr gs:[rip + 0x7cd49301]
   0xffffffff832cee2f <default_idle+31>    nop    
   0xffffffff832cee34 <default_idle+36>    pop    rbx
   0xffffffff832cee35 <default_idle+37>    pop    rbp
   0xffffffff832cee36 <default_idle+38>    pop    r12
   0xffffffff832cee38 <default_idle+40>    pop    r13
   0xffffffff832cee3a <default_idle+42>    pop    r14
   0xffffffff832cee3c <default_idle+44>    ret    
 
   0xffffffff832cee3d <default_idle+45>    mov    eax, dword ptr gs:[rip + 0x7cd492ec]
   0xffffffff832cee44 <default_idle+52>    mov    eax, eax
   0xffffffff832cee46 <default_idle+54>    bt     qword ptr [rip + 0x106c312], rax <0xffffffff8433b160>
───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000 rsp  0xffffffff83e07dc0 (init_thread_union+32192) ◂— 0x0
01:0008      0xffffffff83e07dc8 (init_thread_union+32200) —▸ 0xfffffbfff07c3970 ◂— 0
02:0010      0xffffffff83e07dd0 (init_thread_union+32208) —▸ 0xffffffff83e1cb80 (init_task) ◂— 0x80000000
03:0018      0xffffffff83e07dd8 (init_thread_union+32216) ◂— 0x0
... 
05:0028      0xffffffff83e07de8 (init_thread_union+32232) —▸ 0xffffffff811ab0ae (do_idle+702) ◂— jmp    0xffffffff811aafbc /* 0x4ed8e8ffffff09e9 */
06:0030      0xffffffff83e07df0 (init_thread_union+32240) —▸ 0xffffffff83e1cb80 (init_task) ◂— 0x80000000
07:0038      0xffffffff83e07df8 (init_thread_union+32248) ◂— 0x1ffffffff07c0fc1
─────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────
  f 0 ffffffff832cee27 default_idle+23
   f 1 ffffffff811ab0ae do_idle+702
   f 2 ffffffff811ab0ae do_idle+702
   f 3 ffffffff811ab4c4 cpu_startup_entry+20
   f 4 ffffffff832bee12 rest_init+194
   f 5 ffffffff849bd360
   f 6 ffffffff849bda2c start_kernel+1720
   f 7 ffffffff810000d4
   f 8                0
───────────────────────────────────────────────────────────────────────────────────────────────────────

在ubuntu中,这种调试方法是支持control+c发送SIGINT断下调试的,但是在mac上,此法失败,即需要在第一次gdb连接上时精确设置断点保证接下来每一次能断到,即可正常调试。

调试STM32裸机程序

对于STM32之前我是有过介绍的:SCTF 2020 Password Lock Plus 入门STM32逆向,keil可以对STM32真机进行调试,但我们编写的例程一般是裸机程序,与前两者qemu提供的调试不同,这里看起来没有更底层的软件能来帮我们监视STM32的程序状态了,那么keil能对STM32真机调试的原理是什么呢?没错,就是硬件。原理参考:看见我们看不见的。这里我们使用STM32开发板以及JINK仿真器,并按照如下方式连接:

image

然后安装openocd(Open On-Chip Debugger)工具,启动并指明仿真器以及目标板子的型号:

  brew install openocd
  cd /usr/local/Cellar/open-ocd/0.10.0/share/openocd/
  openocd -f ./interface/jlink.cfg -f target/stm32f1x.cfg

这个工具的原理和安装参考:

启动之后会开启以下三个端口:

  ~ lsof -nP  -i | grep openocd
openocd   7820    3u  IPv4 0x4db37f3eeec1ba61      0t0  TCP *:6666 (LISTEN)
openocd   7820    4u  IPv4 0x4db37f3eeb900bc1      0t0  TCP *:4444 (LISTEN)
openocd   7820    7u  IPv4 0x4db37f3eeb808441      0t0  TCP *:3333 (LISTEN)

其中4444是openocd的命令行端口,然后另起一个terminal:

  ~ telnet localhost 4444
Trying ::1...
Connection failed: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Open On-Chip Debugger
> help
adapter_khz [khz]
      With an argument, change to the specified maximum jtag speed.  For
      JTAG, 0 KHz signifies adaptive  clocking. With or without argument,
      display current setting. (command valid any time)
adapter_name
      Returns the name of the currently selected adapter (driver) (command
      valid any time)
adapter_nsrst_assert_width [milliseconds]
      delay after asserting SRST in ms (command valid any time)
adapter_nsrst_delay [milliseconds]
      delay after deasserting SRST in ms (command valid any time)
add_help_text command_name helptext_string
      Add new command help text; Command can be multiple tokens. (command
      valid any time)
...

而启动的3333端口就可以使用gdb进行连接并调试了,仍然是设置指令集以及连接远程。比如这里我用keil烧写了一个跑马灯的例程,然后将断点打在开关灯处0x08000506(分析编译好的hex文件可知),即可断下:

  ~ gdb -q 
gdb-peda$ set architecture armv7e-m
The target architecture is assumed to be armv7e-m
gdb-peda$ target remote :3333
Remote debugging using :3333
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0x00000000 in ?? ()
gdb-peda$ b * 0x8000506
Breakpoint 1 at 0x8000506
gdb-peda$ c
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
WARNING! The target is already running. All changes GDB did to registers will be discarded! Waiting for target to halt.

Breakpoint 1, 0x08000506 in ?? ()

gdb-peda$ x /20i $pc
=> 0x8000506:	movs	r0, #0
   0x8000508:	ldr	r1, [pc, #40]	; (0x8000534)
   0x800050a:	str	r0, [r1, #0]
   0x800050c:	movs	r0, #1
   0x800050e:	ldr	r1, [pc, #40]	; (0x8000538)
   0x8000510:	str.w	r0, [r1, #392]	; 0x188
   0x8000514:	movw	r0, #3000	; 0xbb8
   0x8000518:	bl	0x80004b8
   0x800051c:	movs	r0, #1
   0x800051e:	ldr	r1, [pc, #20]	; (0x8000534)
   0x8000520:	str	r0, [r1, #0]
   0x8000522:	movs	r0, #0
   0x8000524:	ldr	r1, [pc, #16]	; (0x8000538)
   0x8000526:	str.w	r0, [r1, #392]	; 0x188
   0x800052a:	movw	r0, #3000	; 0xbb8
   0x800052e:	bl	0x80004b8
   0x8000532:	b.n	0x8000506
   0x8000534:	lsls	r0, r4, #6
   0x8000536:	tst	r1, r4
   0x8000538:	strh	r0, [r0, #0]

gdb-peda$ info reg
r0             0x10001             0x10001
r1             0xbb8               0xbb8
r2             0x0                 0x0
r3             0xe000e000          0xe000e000
r4             0x0                 0x0
r5             0x200000d4          0x200000d4
r6             0x0                 0x0
r7             0x0                 0x0
r8             0x0                 0x0
r9             0xff5ffffd          0xff5ffffd
r10            0x800055c           0x800055c
r11            0x0                 0x0
r12            0x20000114          0x20000114
sp             0x20000738          0x20000738
lr             0x8000533           0x8000533
pc             0x8000506           0x8000506
xPSR           0x61000000          0x61000000
msp            0x20000738          0x20000738
psp            0xc3b3fb58          0xc3b3fb58
primask        0x0                 0x0
basepri        0x0                 0x0
faultmask      0x0                 0x0
control        0x0                 0x0

这里其实openocd这个软件将jtag调试的过程转化为gdb调试的过程,所以这里的gdbserver是由openocd支持的,而真正的调试功能是硬件支持的,jtag是作为调试的电路接口以及一个标准协议出现调试中。

总结

目标 层次 执行环境 gdbserver支持
STM32程序 内核 STM32开发板 jtag+openocd
linux内核 内核 qemu-system qemu-system(-s)
固件(文件系统) 应用 qemu-system gdbserver
固件(单个程序) 应用 qemu-user qemu-user(-g)