The purpose of this issue is to develop a fuzzing harness for 5G NR NAS task named nasot and find pattern for all of its required symbols (data labels and API functions). A sample harness is available here, but be aware that it uses many symbols with hard-coded addresses. Below, each symbol is discussed in detail. Symbol names might be slightly different that the provided sample harness. I’m also open to suggestions if you’d like to change any of these names.
fake_test
This is top-level function that can enqueue an SItem instance and trigger an event for the target task. Here is the address of fake_test for few baseband discovered through reverse engineering:
oriole-ap2a.240905.003.f1: 0x420d42ce
oriole-bp2a.250605.031.a5: 0x420d51b4
G991BXXSCGXF5: 0x4147b9be
To find this function manually, you can search for the string message that it prints at the beginning of function: "SMPF task is not created yet. Message(%s) is inserted to pending array". The following screenshot shows how this function looks like in Ghidra:

SMPF_TASK_CREATED
SMPF task is initialized through mainTask after all tasks are initialized (including) fuzzing harness. So, we need to check if SMPF is not created to schedule out fuzzing task and wait for SMPF. As you may have noticed from above screenshot, one way to find this symbol is through fake_test function.
fake_test_harness
This symbol is required for NASOT task fuzzing to disable all encryption and integrity protection checks on OTA message this task receives. It should be used like below in fuzz_single before sending MM_RRC_DATA_IND. Below is the address of fake_test_herness for few basebands.
uint8_t *harness = fake_test_harness();
*harness = 1;
oriole-ap2a.240905.003.f1: 0x423044fe
oriole-bp2a.250605.031.a5: 0x42307c04
SetMmState
fake_test_harness alone is not enough to bypass NR NAS checks before decoding a fuzz input. We need to find SetMmState function to change MM state like below. To find this function manually, you can search for "[N :MM,%d] SetMmState = %lx %lx". Different values are acceptable for the state variable (3rd argument) based on checks. You can find the state checks within MM_RRC_DATA_IND_Handler function by searching for "[N :MM,%d] MM_RRC_DATA_IND_Handler". The screenshot below shows two wrapper functions calling GetMmState. Pay attention to their 3rd argument!
SetMmState(MmGeneralContext, 0, 0x4035634, 0);

MmGeneralContext
As you may have noticed, SetMmState takes a pointer to a class instance:
N2cn2mm27MmGeneralContext_MacroClassE cn::mm::MmGeneralContext_MacroClass
This instance is created during SMPF initialization. If you run the baseband once, you can get its address by hooking either its constructor (which can be found using the above mangled name → its vtable) or any method like SetMmState that takes this instance (like GetMmState; be aware that the wrapper in above screenshot, FUN_422ed2a0 takes NrmmFacade).
I don’t know of a good way to determine the address of these instances without running the emulator once. Feel free to discuss below if you have suggestions on how to use this kinds of APIs from baseband.
🗣️ These classes are well-reverse in oriole-ap2a.240905.003.f1, which is available in our server.
NrmmStartProcedure_Wrapper
This is another API like the previous one that takes the pointer to NrmmFacade instance. To find this function manually, first find NrmmStartProcedure by searching for "[N :MM,%d] Start Procedure : %d" and they it has two caller function of which the one with smaller body is the wrapper. This function should be called like below before SetMmState.
NrmmStartProcedure_Wrapper(NrmmFacade, 1);
NrmmFacade
Similar to MmGeneralContext, this is a class instance, and only one instance is created.
N2cn2mm10NrmmFacadeE cn::mm::NrmmFacade
MM_MSG_CLASS
This is symbol with constant value that is used to construct an SItem. There is a function creating many handlers where one of them has this ID. The screenshot below is taken from oriole-ap2a.240905.003.f1 function at 41b89b40. One way to find this function is to first find the handler function (line 623) and search for movt and movw instructions loading its address to a register (likely r2 as this function as two arguments).

MM_MSG_DOMAIN
Let’s keep this constant for now: 0x7fe00400
File Structure
For each fuzzing harness, we will add 3 new files and update the modkit/shannon/Makefile.
fuzzers/nasot.h will define the required symbols, such as:
#ifndef _NASOT_H
#define _NASOT_H
#include <smpf.h>
MODKIT_FUNCTION_SYMBOL(void, SetMmState, uint32_t, uint32_t, uint32_t, uint32_t)
MODKIT_FUNCTION_SYMBOL(uint8_t *, fake_test_harness, void)
MODKIT_FUNCTION_SYMBOL(void, NrmmStartProcedure_Wrapper, uint32_t, uint32_t)
MODKIT_DATA_SYMBOL(uint32_t, MmGeneralContext)
MODKIT_DATA_SYMBOL(uint32_t, MM_MSG_CLASS)
MODKIT_DATA_SYMBOL(uint32_t, MM_MSG_DOMAIN)
MODKIT_DATA_SYMBOL(uint32_t, NrmmFacade)
#endif // _NASOT_H
fuzzers/nasot_symbolc.c is used to manage required symbols per fuzzing harness and include the common symbols from smpf.h. It should look like the following:
#ifndef MODKIT_INSTANTIATE
#define MODKIT_INSTANTIATE
#endif
#include <fuzzers/nasot.h>
fuzzers/nasot.c is the main fuzzing harness, which always should start with the following include directives and name definition:
#include <afl.h>
#include <shannon.h>
#include <fuzzers/nasot.h>
const char TASK_NAME[] = "AFL_NASOT\0";
Similar to other existing fuzzers, it should implement fuzz_single_setup and fuzz_single. fuzz_single_setup is used to perform one-time jobs such as sending an INIT message, and fuzz_single is where the fuzzing input is exercised.
fuzz_single_setup
Similar to existing fuzz harnesses, please use available API to find the source and destination queue IDs by name and replace the hard-coded values (0xbb and 0x132) below. There might be additional state setup required to fuzz NR NAS decoder that I’ve missed here.
typedef struct PACKED {
uint8_t payload[10];
} Payload_MMC_NRMM_INIT_REQ;
typedef struct PACKED {
struct qitem_header header;
Payload_MMC_NRMM_INIT_REQ pl;
} QItem_MMC_NRMM_INIT_REQ;
void send_MMC_NRMM_INIT_REQ()
{
QItem_MMC_NRMM_INIT_REQ *item = (QItem_MMC_NRMM_INIT_REQ*) pal_MemAlloc(4, sizeof(QItem_MMC_NRMM_INIT_REQ), __FILE__, __LINE__);
if (!item) {
MODEM_LOG("ALLOC FAILED");
return;
}
item->header.op1 = 0xbb;
item->header.op2 = 0x132;
item->header.size = sizeof(QItem_MMC_NRMM_INIT_REQ) - sizeof(struct qitem_header);
item->header.msgGroup = 0x2000;
memset(&item->pl, 0, sizeof(item->pl));
item->pl.payload[0] = 0;
pal_MsgSendTo(0x132, item, 2);
}
int fuzz_single_setup()
{
do {
pal_Sleep(2);
} while (*SMPF_TASK_CREATED == 0);
send_MM_GMC_INIT_REQ();
return 1;
}
fuzz_single
void send_MM_RRC_DATA_IND(uint8_t *buf, uint32_t input_size) {
uint8_t *pData = (uint8_t *)pal_MemAlloc(4, input_size + 8, __FILE__, __LINE__);
if (!pData) {
MODEM_LOG("ALLOC FAILED");
return;
}
memcpy(pData + 8, buf, input_size);
SItem *msg = (SItem *)pal_MemAlloc(4, sizeof(SItem), __FILE__, __LINE__);
if (!msg) {
MODEM_LOG("ALLOC FAILED");
return;
}
msg->field_0x0.msg_id = MM_MSG_CLASS;
msg->field_0x0.field_0x4 = (MM_MSG_CLASS >> 0xc) | MM_MSG_DOMAIN;
msg->field_0x0.target_obj_id = (MM_MSG_CLASS >> 0x16) | MM_MSG_DOMAIN;
msg->field_0x0.domain_s = 0x40;
msg->field_0x0.domain_d = 0;
msg->field_0x0.routing = 0x80;
memset(msg->field_0x0.field_0x14, 0, 0xc);
msg->field_0x0.msg_type = 4;
msg->field_0x0.size = 0x48;
msg->field_0x0.msg_name = "MM_RRC_DATA_IND";
msg->pl.pData = pData + 8;
msg->pl.dataLength = input_size;
(*fake_test)(msg);
}
void fuzz_single()
{
uint32_t input_size;
uint16_t size;
MODEM_LOG("Getting Work");
uint8_t *buf = (uint8_t *)getWork(&input_size);
size = (uint16_t) input_size;
MODEM_LOG("[+] Received 0x%x bytes (buf=0x%08x): ", input_size, (uint32_t)buf);
uart_dump_hex((uint8_t *)buf, size); // Print some for testing
MODEM_LOG("FIRE");
startWork(0, 0xffffffff); // memory range to collect coverage
uint8_t *harness = fake_test_harness();
*harness = 1;
NrmmStartProcedure_Wrapper(NrmmFacade, 1);
SetMmState(MmGeneralContext, 0, 0x4035634, 0);
MODEM_LOG("Sending MM_RRC_DATA_IND...");
send_MM_RRC_DATA_IND(buf + 8, input_size - 8);
doneWork(0);
MODEM_LOG("WorkDone");
}
smpf.h includes all common definitions between 5G tasks.
#ifndef _SMPF_H
#define _SMPF_H
#include <common.h>
#include <modkit.h>
typedef enum PACKED {
SMPF_URGENT_MSG_Q = 0,
SMPF_HIGH_MSG_Q,
SMPF_SELF_MSG_Q,
SMPF_INTERNAL_MSG_Q,
SMPF_EXTERNAL_MSG_Q,
SMPF_TIMER_MSG_Q,
SMPF_BYPASS_MSG_Q,
SMPF_BYPASS_URG_MSG_Q,
SMPF_USER_CB_MSG_Q,
} SmpfMsgType;
typedef struct PACKED {
uint32_t msg_id;
uint32_t field_0x4;
uint32_t target_obj_id;
uint8_t domain_s;
uint8_t domain_d;
uint8_t field_0xe[2];
uint32_t routing;
uint8_t field_0x14[0xc];
SmpfMsgType msg_type;
uint16_t field_0x21;
uint8_t field_0x23;
uint32_t size;
char * msg_name;
} astruct_28;
typedef struct PACKED {
uint8_t *pData;
uint16_t dataLength;
} SItemPayload;
typedef struct PACKED {
astruct_28 field_0x0;
SItemPayload pl;
} SItem;
MODKIT_DATA_SYMBOL(uint8_t *, SMPF_TASK_CREATED)
MODKIT_FUNCTION_SYMBOL(void, fake_test, SItem *)
#endif // _SMPF_H
The purpose of this issue is to develop a fuzzing harness for 5G NR NAS task named
nasotand find pattern for all of its required symbols (data labels and API functions). A sample harness is available here, but be aware that it uses many symbols with hard-coded addresses. Below, each symbol is discussed in detail. Symbol names might be slightly different that the provided sample harness. I’m also open to suggestions if you’d like to change any of these names.fake_testThis is top-level function that can enqueue an
SIteminstance and trigger an event for the target task. Here is the address offake_testfor few baseband discovered through reverse engineering:oriole-ap2a.240905.003.f1:0x420d42ceoriole-bp2a.250605.031.a5:0x420d51b4G991BXXSCGXF5:0x4147b9beTo find this function manually, you can search for the string message that it prints at the beginning of function:
"SMPF task is not created yet. Message(%s) is inserted to pending array". The following screenshot shows how this function looks like in Ghidra:SMPF_TASK_CREATEDSMPF task is initialized through
mainTaskafter all tasks are initialized (including) fuzzing harness. So, we need to check if SMPF is not created to schedule out fuzzing task and wait for SMPF. As you may have noticed from above screenshot, one way to find this symbol is throughfake_testfunction.fake_test_harnessThis symbol is required for
NASOTtask fuzzing to disable all encryption and integrity protection checks on OTA message this task receives. It should be used like below infuzz_singlebefore sendingMM_RRC_DATA_IND. Below is the address offake_test_hernessfor few basebands.oriole-ap2a.240905.003.f1:0x423044feoriole-bp2a.250605.031.a5:0x42307c04SetMmStatefake_test_harnessalone is not enough to bypass NR NAS checks before decoding a fuzz input. We need to findSetMmStatefunction to change MM state like below. To find this function manually, you can search for"[N :MM,%d] SetMmState = %lx %lx". Different values are acceptable for the state variable (3rd argument) based on checks. You can find the state checks withinMM_RRC_DATA_IND_Handlerfunction by searching for"[N :MM,%d] MM_RRC_DATA_IND_Handler". The screenshot below shows two wrapper functions callingGetMmState. Pay attention to their 3rd argument!MmGeneralContextAs you may have noticed,
SetMmStatetakes a pointer to a class instance:This instance is created during SMPF initialization. If you run the baseband once, you can get its address by hooking either its constructor (which can be found using the above mangled name → its vtable) or any method like
SetMmStatethat takes this instance (likeGetMmState; be aware that the wrapper in above screenshot,FUN_422ed2a0takesNrmmFacade).I don’t know of a good way to determine the address of these instances without running the emulator once. Feel free to discuss below if you have suggestions on how to use this kinds of APIs from baseband.
🗣️ These classes are well-reverse in
oriole-ap2a.240905.003.f1, which is available in our server.NrmmStartProcedure_WrapperThis is another API like the previous one that takes the pointer to
NrmmFacadeinstance. To find this function manually, first findNrmmStartProcedureby searching for"[N :MM,%d] Start Procedure : %d"and they it has two caller function of which the one with smaller body is the wrapper. This function should be called like below beforeSetMmState.NrmmFacadeSimilar to
MmGeneralContext, this is a class instance, and only one instance is created.MM_MSG_CLASSThis is symbol with constant value that is used to construct an
SItem. There is a function creating many handlers where one of them has this ID. The screenshot below is taken fromoriole-ap2a.240905.003.f1function at41b89b40. One way to find this function is to first find the handler function (line 623) and search formovtandmovwinstructions loading its address to a register (likelyr2as this function as two arguments).MM_MSG_DOMAINLet’s keep this constant for now:
0x7fe00400File Structure
For each fuzzing harness, we will add 3 new files and update the
modkit/shannon/Makefile.fuzzers/nasot.hwill define the required symbols, such as:fuzzers/nasot_symbolc.cis used to manage required symbols per fuzzing harness and include the common symbols fromsmpf.h. It should look like the following:fuzzers/nasot.cis the main fuzzing harness, which always should start with the following include directives and name definition:Similar to other existing fuzzers, it should implement
fuzz_single_setupandfuzz_single.fuzz_single_setupis used to perform one-time jobs such as sending an INIT message, andfuzz_singleis where the fuzzing input is exercised.fuzz_single_setupSimilar to existing fuzz harnesses, please use available API to find the source and destination queue IDs by name and replace the hard-coded values (0xbb and 0x132) below. There might be additional state setup required to fuzz NR NAS decoder that I’ve missed here.
fuzz_singlesmpf.hincludes all common definitions between 5G tasks.