固件下的how2hook & how2patch

在模拟固件的时候通常需要我们自己对二进制文件进行patch和hook,但无论是patch还是hook,都是通过修改(劫持)二进制文件的执行流程来达到固件顺利启动的目的。

BIN-100

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
qmemcpy(encoded_flag, &byte_400A7E, sizeof(encoded_flag));
stack_canary = __readfsqword(0x28u);
data_pointer = obfuscated_key;
for ( index_outer_loop = 9LL; index_outer_loop; --index_outer_loop )
*data_pointer++ = 0;
remaining_iterations = 0x31337;
initial_time = time(0LL);
do
{
for ( i = 0LL; i != 36; ++i )
{
current_length = 0LL;
current_time = time(0LL);
srand(0xDEFACED - initial_time + current_time);
previous_byte = obfuscated_key[i];
obfuscated_key[i] = rand() ^ previous_byte;
funny_element = (&funny)[i];
while ( current_length < strlen(funny_element) )
{
character_value = funny_element[current_length];
if ( (_BYTE)character_value == 'i' )
{
formatted_string[(int)current_length] = 'i';
}
else
{
if ( (_DWORD)current_length && funny_element[current_length - 1] != ' ' )
ctype_function = __ctype_toupper_loc();
else
ctype_function = __ctype_tolower_loc();
formatted_string[(int)current_length] = (*ctype_function)[character_value];
}
++current_length;
}
formatted_string[(int)current_length] = 0;
__printf_chk(1LL, " ♫ %80s ♫\n", formatted_string);
sleep(1u);
}
--remaining_iterations;
}
while ( remaining_iterations );
key_pointer = obfuscated_key;
__printf_chk(1LL, "KEY: ");
do
{
byte_value = (unsigned __int8)*key_pointer++;
__printf_chk(1LL, "%02x ", byte_value);
}
while ( key_pointer != encoded_flag );
flag_index = 0LL;
putchar('\n');
__printf_chk(1LL, "OK YOU WIN. HERE'S YOUR FLAG: ");
do
{
decoded_char = encoded_flag[flag_index] ^ obfuscated_key[flag_index];
++flag_index;
putchar(decoded_char); //输出flag
}
while ( flag_index != 36 );
putchar(10);
return 0;
}

其实我们发现这个demo的循环次数也算是比较少的,主要在于如何加快这个过程,发现有个sleep(1u),那么我们可以利用LD_PRELOAD对sleep进行hook,然后nop掉一些无用的函数,如printf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> cat hook2.c
// gcc -shared -fPIC hook2.c -o hook2.so
#include<stdio.h>
#include<stdlib.h>

static int t=0x31337;

unsigned int sleep(unsigned int seconds)
{
t++;
}

int time(void)
{
return t;
}

之后

1
2
LD_PRELOAD=$PWD/hook2.so ./elf >tmp
tail tmp

得到flag:p4ul_1z_d34d_1z_wh4t_th3_r3c0rd_s4ys

实战!

PSV-2020-0211

netgear R8300 1.0.2.130 的upnp中存在栈溢出的漏洞,主要学习利用手法,NVram hook手法,Poc编写 以及 调试技巧

  • **UPnP (Universal Plug and Play)**:现在由开放连接基金会管理的是一套网络协议,它允许网络设备无缝地发现彼此在网络上的存在,并为数据共享、通信和娱乐建立功能网络服务。

UPnP会将某个端口连接到多播组,即允许接收其他客户端传输数据(一对多)【或许就是通过这个来使得网络设备可以无缝发现彼此扒】

image-20250228222327229

漏洞看着是很简单的,但是由于sub_25E04是通过strcpy来栈溢出的,因此\x00截断便是不可绕过的障碍,而原作者的利用手法值得学习

启动服务:这次使用用户模式运行

因为涉及到nvram,使用LD_PRELOAD Hook技术,网上有人已经写好了针对netgear R6250/6400的nvram hook,对于R8300也是适用的 https://github.com/therealsaumil/custom_nvram/blob/master/custom_nvram_r6250.c

使用buildroot进行交叉编译

首先构造buildroot

下载地址: https://buildroot.org/download.html,下载一个最新的版本

然后

1
2
cd ~/buildroot-2024.11
make menuconfig

Target Option -> Target Archite… -> Arm little endian

之后file随便一个nvram的文件,发现是uClibc

因此toolchain->C library->uClibc

但是在编译之前需要先换源,这样可以节省一大堆的时间

Build Option -> Mirrors and Download locations ->

1
2
3
4
5
https://sources.buildroot.net                              
https://mirrors.aliyun.com/linux-kernel
https://mirrors.aliyun.com/gnu
https://luarocks.cn
https://mirrors.aliyun.com/CPAN

之后多次esc退出,然后执行make toolchain -j4 V=0

  1. images/
    • 存放编译后生成的所有镜像文件,包括内核镜像、加载引导镜像和根文件系统镜像。
    • 这些镜像用于在目标设备上启动和运行操作系统。
  2. build/
    • 存放所有构建组件的目录,除了交叉编译工具链的组件。
    • 每个功能对应一个子目录,包含该功能所需的所有组件。
    • 例如,内核、库、应用程序等在这里进行编译和配置。
  3. staging/
    • 包含一个类似于根文件系统的层次结构。
    • 这个目录包含已安装的交叉编译工具链和所有为目标板选择的用户空间包。
    • 用于准备最终的根文件系统。
  4. target/
    • 包含根文件系统的完整结构,但不能直接用于开发板。
    • 这个目录是构建过程中生成的文件系统的中间状态。
  5. host/
    • 包含构建过程中需要的交叉编译工具集。
    • 用于在主机系统上编译目标系统的代码。
    • 提供了编译器、链接器和其他必要的开发工具。
1
2
cd ~/buildroot-2024.11/output/host/bin 
export PATH=$PATH:$(pwd)
1
2
arm-linux-gcc -c -O2 -fPIC -Wall ./nvram1.c -o ./nvram1.o
arm-linux-gcc -shared -nostdlib ./nvram1.o -o ./nvram1.so

之后创建/tmp/nvram.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
upnpd_debug_level=9
lan_ipaddr=192.168.122.167
hwver=R8500
friendly_name=R8300
upnp_enable=1
upnp_turn_on=1
upnp_advert_period=30
upnp_advert_ttl=4
upnp_portmap_entry=1
upnp_duration=3600
upnp_DHCPServerConfigurable=1
wps_is_upnp=0
upnp_sa_uuid=00000000000000000000
lan_hwaddr=AA:BB:CC:DD:EE:FF

之后的lan_ipaddr还要连接,因此要把ip起起来

1
2
sudo ip addr add 192.168.122.167/24 dev virbr0
ip addr show virbr0

显示对应的ip就代表成功啦

之后

1
sudo chroot . ./qemu-arm-static -E LD_PRELOAD="./nvram1.so ./lib/libdl.so.0" ./usr/sbin/upnpd
1
2
3
4
sudo lsof -i | grep qemu                                                                     
qemu-arm- 6267 root 3u IPv4 93141 0t0 UDP *:1900
qemu-arm- 6267 root 4u IPv4 93142 0t0 UDP *:45791
qemu-arm- 6267 root 5u IPv4 93143 0t0 TCP *:5000 (LISTEN)

如此程序就起起来了,就可以开始Poc的编写了

调试:

1
sudo chroot . ./qemu-arm-static -g 1234 -E LD_PRELOAD="./nvram1.so ./lib/libdl.so.0" ./usr/sbin/upnpd

一开始傻呗了,忘记了用户模式可以直接调试,直接attach pid调试调的是qemu的进程()

main函数中有

1
2
3
4
5
6
if ( daemon(1, 1) == -1 )
{
debug(3, "Fail to run as daemon");
v13 = _errno_location();
exit(*v13);
}

daemon使得当前进程会变成守护进程并保持工作目录不变,但是这个会让该进程脱离gdb的控制,考虑nop掉,成功~

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
 R0   0x61616161 ('aaaa')
R1 0xfffed504 ◂— 0x61616161 ('aaaa')
R2 0xfffed504 ◂— 0x61616161 ('aaaa')
R3 0x61616161 ('aaaa')
R4 0x61
R5 0xfffed500 ◂— 0x61616161 ('aaaa')
R6 0xfffed504 ◂— 0x61616161 ('aaaa')
R7 0xfffed500 ◂— 0x61616161 ('aaaa')
R8 0xfffed504 ◂— 0x61616161 ('aaaa')
R9 0x10ab
R10 0x1
R11 0xc4584 ◂— 7
R12 0x553dc —▸ 0xff60393c ◂— push {r4, lr} /* 0xe92d4010 */
SP 0xfffeceb8 ◂— 0x61616161 ('aaaa')
PC 0xff603954 ◂— ldrb ip, [r3] /* 0xe5d3c000 */
──────────────────────────────────────────────────────────[ DISASM / arm / set emulate on ]──────────────────────────────────────────────────────────
0xff603948 ldrb r4, [r2]
0xff60394c cmp r4, #0
0xff603950 popeq {r4, pc}
0xff603954 ldrb ip, [r3]
0xff603958 cmp r4, ip
0xff60395c addeq r2, r2, #1
0xff603960 addeq r3, r3, #1
0xff603964 beq #0xff603948 <0xff603948>

0xff603948 ldrb r4, [r2]
0xff60394c cmp r4, #0
0xff603950 popeq {r4, pc}
──────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────
00:0000│ sp 0xfffeceb8 ◂— 0x61616161 ('aaaa')
01:00040xfffecebc —▸ 0xb62c ◂— subs r7, r0, #0 /* 0xe2507000 */
02:00080xfffecec0 —▸ 0xfffedefd ◂— 0x61616161 ('aaaa')
03:000c│ 0xfffecec4 —▸ 0xfffed574 ◂— 0x61616161 ('aaaa')
04:00100xfffecec8 —▸ 0x8df9 ◂— 0xad
05:00140xfffececc —▸ 0xfffef5f4 ◂— mrchs p9, #1, r3, c2, c1, #1 /* 0x2e323931; '192.168.122.1' */
06:00180xfffeced0 —▸ 0xfffed500 ◂— 0x61616161 ('aaaa')
07:001c│ 0xfffeced4 —▸ 0x25e80 ◂— subs sl, r0, #0 /* 0xe250a000 */
────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────
► f 0 0xff603954
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

发送大量数据就成功发现栈溢出漏洞

漏洞利用:

1
2
3
4
5
6
7
8
frimware/_R8300-V1.0.2.130_1.0.99.chk.extracted/squashfs-root 
> checksec ./usr/sbin/upnpd
[*] '/home/s1nec-1o/frimware/_R8300-V1.0.2.130_1.0.99.chk.extracted/squashfs-root/usr/sbin/upnpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

没pie,就启用了nx

SSD 公开的漏洞细节中给出了一个方案:通过 stack reuse 的方式来绕过该限制。具体思路为,先通过 socket 发送第一次数据,往栈上填充相应的 rop payload,同时保证不会造成程序崩溃;再通过 socket 发送第二次数据用于覆盖栈上的返回地址,填充的返回地址用来实现 stack pivot,即劫持栈指针使其指向第一次发送的 payload 处,然后再复用之前的 payload 以完成漏洞利用。SSD 公开的漏洞细节中的示意图如下。

image-20250301114232391

这里借用的ROP有

1
2
3
.text:000230F0                 ADD             SP, SP, #0x20C
.text:000230F4 ADD SP, SP, #0x1000
.text:000230F8 POP {R4-R11,PC}

升栈并且pop出所有关键寄存器,如此只需要提前构造好对应的payload就可以进行ROP啦!

这个利用的手法使得一些简单的栈溢出都能变得很危险(学到惹)

参考

cyberangle

原作者