Skip to content

[FirmWire] Add 5G NR NAS Fuzzer for AFL++ #7

Description

@MustBastani

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:

Image

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

Image

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

Image

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.

  1. 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
  1. 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>
  1. 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");
}
  1. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions