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
- typo: 只有一个elf文件,静态链接
- embedded_heap: elf动态链接,以及相关的动态链接库
- 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.so
,libc.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>
总结
- 可以用pwndbg插件的vmmap来观察程序内存,这里的内存是qemu模拟出来的,宿主机上并不存在
embedded_heap
这个进程,因为gdbserver本身就是qemu自己提供的支持,所以他可以给gdb一个模拟出来的假的内存布局。 - 经过测试,peda、gef、pwndbg三款插件,pwndbg对于qemu-user的gdbserver支持是最好的,另两个很多东西都看不到。
- 可以看到这里识别embedded_heap的内存中没有libc.so.0,但是在qemu模拟的内存中,libc的确存在,需要自行在目标程序中打断,然后分析libc地址,如果采用qemu-system方法启动一整个文件系统,将不会出现这种情况。
- 使用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连接远程即可开始调试。
- 各种指令集的linux内核以及文件系统可以在这里获得: https://people.debian.org/~aurel32/qemu/
- 各种指令集的静态编译的gdbserver已经上传到百度网盘: https://pan.baidu.com/s/1_Grqzwyf3NOesbWLp6gBKg 密码:hfab
对与这种一个整包的固件可以使用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过程虽然有些繁琐:
- 对于固件,要解包,拷贝调试器,可能还要修改或者patch,最后打包
- 对于本机,要配置网卡,架设Web服务器
- 对于qemu,要下载对应架构的执行环境,启动后要配置网卡,下载固件,挂载文件系统,最后chroot
- 对于目标程序,要想办法正常启动起来,然后用gdb挂上
在执行环境的层次上,可以用如下图表示:
虽然繁琐,但是有如下两处优点:第一,支持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调试接口,并且对于最简单的调试来说,我们只需要准备内核镜像以及根文件系统即可:
- 如果是从源码编译linux内核以及手动构建文件系统可以参考: linux kernel 爬坑记录
- 也可以使用这里给出的基本环境直接理解调试的原理: https://pan.baidu.com/s/1fGyB5JcCdCXYTkW3Ez_dwA 密码:uhj6
基本环境中有如下文件:
- 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仿真器,并按照如下方式连接:
然后安装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) |