skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
July 14, 2020

Structured fuzzing Android's NFC

Man Yue Mo

In this post I’ll go through some details of how I built a fuzzer for the Android Near Field Communication (NFC) component using libprotobuf-mutator. The NFC fuzzer itself is made public and contains documentation and instructions on how to use it, so this post will focus on some design considerations when building the fuzzer.

Android NFC

The NFC component in Android (there after referred to as Android NFC for brevity) allows an Android phone to share data with an NFC tag (smartcard, smart sticker etc.) or other Android-powered devices. This component supports three different types of operations:

  1. Reader/Writer mode: This allows the mobile device to act as an NFC tag/card reader or writer and is used for interacting with NFC tags
  2. Peer-to-peer (P2P) mode: This allows the device to interact with another device via NFC
  3. Card emulation mode: This allows the NFC device to act like an NFC tag and interact with an external card reader or writer

This post and the fuzzer will only look at the Reader/Writer mode. The implementation in Android supports various types of NFC tags defined by the NFC Forum, which are different protocols used for NFC communications. When interacting with NFC tags, an Android device can either act as a reader or a writer. While a writer can perform more types of operations (and hence has a bigger attack surface), it typically requires a third-party application such as this one to be installed, as well as user interaction to initialize the write. Reading and detecting NFC tags, on the other hand, is done automatically as soon as the device comes into range of an NFC tag, provided NFC is turned on (which is the default) and the screen is active. A vulnerability in the reader mode, therefore, is much high risk than one in the writer mode.

The NFC component in Android has been a previous source of vulnerabilitie. In 2018, it was the forth biggest source of high severity vulnerabilities in Android. The trend continued into 2019 with many of the vulnerabilities in Android NFC found by Qi Zhao @JHyrathon and Guang Gong @oldfresher of Qihoo 360 Alpha Lab, and Xuan Xing of Google.

Qi Zhao wrote a great, informative report on NFC vulnerabilities here, which explained the basic concepts of Android NFC as well as the attack surfaces, and included some good examples, so I’ll not duplicate his effort here but focus instead on the fuzzing aspects in this article. He also has a repository that showedcased some PoCs of the vulnerabilities that he discovered, which was extremely useful in the early days of this research.

Structured fuzzing Android NFC

The considerations of creating a fuzzer for Android NFC is actually very similar to that of socket fuzzing, with some complication of running it on Android. Since Android has reasonably good support of libFuzzer, we’ll be using libFuzzer together with libprotobuf-mutator to implement structured fuzzing.

Fuzzing environment setup

The Android NFC reader/writer (rw) component uses a message loop to process events, which may contain internal events from the device itself, or external events coming from an NFC tag. This message loop is implemented in the nfc_task function:

uint32_t nfc_task(__attribute__((unused)) uint32_t arg) {
  uint16_t event;
  NFC_HDR* p_msg;
  bool free_buf;

  /* Initialize the nfc control block */
  memset(&nfc_cb, 0, sizeof(tNFC_CB));

  DLOG_IF(INFO, nfc_debug_enabled) << StringPrintf("NFC_TASK started.");

  /* main loop */
  while (true) {
    event = GKI_wait(0xFFFF, 0); //<-- take an event from a task queue
    ...
    if (event & NFC_MBOX_EVT_MASK) {
      /* Process all incoming NCI messages */
      while ((p_msg = (NFC_HDR*)GKI_read_mbox(NFC_MBOX_ID)) != nullptr) { //<-- take a message from a queue
        free_buf = true;

        /* Determine the input message type. */
        switch (p_msg->event & NFC_EVT_MASK) {
          case BT_EVT_TO_NFC_NCI:
            free_buf = nfc_ncif_process_event(p_msg);  //<--- processing the message
            break;

          case BT_EVT_TO_START_TIMER:
             ...

In the above, the GKI_read_mbox is responsible for reading data into a message (p_msg) and then process it in the nfc_ncif_process_event function. Communications with an NFC tag is handled here.

To save us from setting up various task queues and to focus on events that can be controlled by an NFC tag, I decided to take the nfc_ncif_process_event function as an entry point and implement a simpler message loop that will keep calling this function with inputs from the fuzzer. Getting the entry point, however, is only the first step. In order to produce useful results, the fuzzer needs to be a reasonable approximation of the actual application. This requires at least the following:

  1. Which part of the input data we can control from a tag, e.g. Would an arbitrary length message be received by nfc_ncif_process_event in a realistic situation? Is there metadata that is filled in by the hardware based on the incoming data?
  2. What state variables need to be set before reading/writing tags and do they get reset after each read/write?

After much testing, I identified various constraints to the p_msg that is processed in nfc_ncif_process_event in the above, including constraints to the message length, as well as some metadata. This knowledge was then used to implement various methods in harness_common.cc to create p_msg used for fuzzing, alongside other methods to set up the fuzzing envoirnment. At this point, the fuzzing can very much start as:

  ...
  setup(PROTOCOL);
  nfa_rw_start_ndef_detection(); 
  nfa_rw_cb.cur_op = NFA_RW_OP_READ_NDEF;
  uint8_t[258] buffer;
  size_t data_len;
  while (data_len = readSomeData(buffer)) {
    NFC_HDR* p_msg = create_data_msg_meta(data_len, buffer);
    nfc_ncif_process_event(p_msg);
    ...
  }

This would not be a very effective fuzzer as NFC protocols expect the message data to be of a certain structure, otherwise some early checks will fail.

Taking protocol specific details into account

I’ll now restrict to the NFC forum type 2 tag, which is mostly implemented in rw_t2t.cc and rw_t2t_ndef.cc.

A message processed by nfc_ncif_process_event first arrives at rw_t2t_proc_data, where various checks are carried out. During the detection stage, which is when the device first comes into contact with an NFC tag, if all the checks in message length and metadata pass, it will proceed to rw_t2t_handle_rsp, where more interesting processing is performed on the actual data.

The checks in rw_t2t_proc_data places fairly strong restrictions on the message. It basically means that each message needs to have a fixed length of either 1 (an “ack”) or 16 (actual data), except when the substate is RW_T2T_SUBSTATE_WAIT_SELECT_SECTOR_SUPPORT:

  if (p_t2t->substate == RW_T2T_SUBSTATE_WAIT_SELECT_SECTOR_SUPPORT) {
    /* The select process happens in two steps */
    if ((*p & 0x0f) == T2T_RSP_ACK) {
      if (rw_t2t_sector_change(p_t2t->select_sector) == NFC_STATUS_OK)
    ...

To satify these constraints, I used libprotobuf-mutator on top of libFuzzer to provide structure for the inputs. This is a very useful library for implementing structured fuzzing with libFuzzer and has seen various successes. It allows one to specify an input structure using the protobuf syntax. In my case, I’d like each message to be of either length 16 or 1, so I use 2 int64 for the length 16 message and a third field, ack as a probability to switch either use length 16 or length 1:

//Generic response of fixed length
message DefaultResponse {
  required int64 hdr_0 = 1;
  required int64 hdr_1 = 2;
  required uint32 ack = 3;
}

The field ack is converted to a probability when parsing to decide whether to use the full 16 bytes or just 1 byte. (Basically checking ack against a threshold).

Taking state into account

As explained before, the allowed message length also depends on the substate of Android NFC. This is rather inconvenient as the randomly generated message may just end not being the one expected and get rejected right away. It can also lead to the fuzzer “giving up” on the less likely states (i.e. if some states occur more often than others, then the fuzzer may stop generating samples suitable for the lesser state, and those would end up not being tested at all). To counter this, instead of generating a single linear queue from the fuzzer, each sample in the corpus contains multiple queues, one for each state, and the fuzzer will decide which queue to take sample from depending on the current state:

message DetectSession {
  repeated WaitSelectSector wait_select = 1;
  repeated DefaultResponse dr = 2;
  repeated WaitCc wait_cc = 3;
}

A fuzzing session now contains three queues (repeated is the equivalent of array in protobuf). Each corresponds to a specialized message for a different state and the fuzzer decides which one to use depending on the actual state:

void handleT2T(const nfc::DetectSession& session) {
  bool free_buf;
  tRW_T2T_CB* p_t2t = &rw_cb.tcb.t2t;
  int wait_cc_counter = 0;
  int default_message_counter = 0;
  int wait_select_counter = 0;
  for (int i = 0; i < max_op; i++) {
    NFC_HDR* p_msgs[1] = {0};
    //Choose the appropriate message depending on the state
    switch (p_t2t->substate) {
        case RW_T2T_SUBSTATE_WAIT_READ_CC:
          if (wait_cc_counter >= session.wait_cc_size()) return;
          create_t2t_wait_cc(session.wait_cc(wait_cc_counter++), p_msgs);
          break;
        case RW_T2T_SUBSTATE_WAIT_SELECT_SECTOR:
          if (wait_select_counter >= session.wait_select_size()) return;
          create_t2t_wait_select_sector(session.wait_select(wait_select_counter++), p_msgs);
          break;
        default:
          if (default_message_counter >= session.dr_size()) return;
          create_t2t_default_response(session.dr(default_message_counter++), p_msgs);
          break;
    }
    if (*p_msgs) {
      set_message_len(*p_msgs);
      NFC_HDR* msg = copy_msg(*p_msgs);
      ...
  }
}

While this tweaking does not seem to make a gain in the current case, I believe it would be useful when there are more differences between states.

Switching between enums and data

Within the code responsible for parsing the NFC data, I noticed that the data itself is sometimes interpreted as an enum:

    switch (p_t2t->substate) {
      case RW_T2T_SUBSTATE_WAIT_TLV_DETECT:
        /* Search for the tlv */
        p_t2t->found_tlv = p_data[offset++]; //<--- p_data is the user controlled data
        switch (p_t2t->found_tlv) {  //<--- interpreted as enum
          case TAG_NULL_TLV: /* May be used for padding. SHALL ignore this */
            break;

          case TAG_NDEF_TLV:

While other times it may be interpreted as a number:

      case RW_T2T_SUBSTATE_WAIT_FIND_LEN_FIELD_LEN:
        len = p_data[offset];  //<--- p_data is user controlled, 
         ...
            } else {
              /* one byte length field */
              p_t2t->ndef_msg_len = len;   //<--- len interpreted as an actual number
              p_t2t->bytes_count = p_t2t->ndef_msg_len;
              p_t2t->substate = RW_T2T_SUBSTATE_WAIT_READ_TLV_VALUE;

This difference in the interpretation does not depend on the offset. Data at the same offset can sometimes be interpreted as enum, while other times be interpreted as a number, depending on the data that goes before it. This is a problem because enum only has six meaningful values, while a number could take the full range. To overcome this, I introduced a function to convert number into enum whenever the data is to be interpreted as an enum:

//converts uint8_t to one of the tlv states (see tags_defs.h)
static uint8_t num2tlv(uint8_t input) {
  switch (input % 6) {
    case 0:
      return TAG_NULL_TLV;
    ...
  }
}
...
     switch (p_t2t->substate) {
       case RW_T2T_SUBSTATE_WAIT_TLV_DETECT:
         /* Search for the tlv */
        p_t2t->found_tlv = p_data[offset++];
        p_t2t->found_tlv = num2tlv(p_data[offset++]); //<--- Converts to enum
         switch (p_t2t->found_tlv) {
           case TAG_NULL_TLV: /* May be used for padding. SHALL ignore this */
             break;

This way the fuzzer can still generate data within the fuzz range of uint8_t while not getting blocked when an enum is required.

Results

Admittedly, since many vulnerabilities have already been found and the NFC type 2 protocol is fairly restrictive (more or less fixed length), I was a bit surprised when it discovered four OOB writes within a few minutes of running, without an input corpus. These vulnerabilities are disclosed as CVE-2020-0070/GHSL-2020-010, CVE-2020-0071/GHSL-2020-008, CVE-2020-0072/GHSL-2020-007, and CVE-2020-0073/GHSL-2020-006.

These are all inner struct OOB writes, which ASAN (address sanitizer) wouldn’t detect. In fact, when I first got a crash, it was because one of the bugs ended up overwriting a function pointer and hit a control flow integrity (CFI) check and crashed. To make things worse, libFuzzer seemed to have thought that it was a nested crash and did not produce a crash case. By introducing some manual assertions, I was eventually able to reconstruct the test cases. After some discussions with Antonio Morales, he pointed out that there are cases where ASAN does not detect OOB writes and that inner struct overflow is one of those cases. But it should be possible to detect these cases with undefined behavior sanitizer (UBSAN). So I took the advice and included UBSAN bounds check in the build config:

    sanitize : {
        misc_undefined: [
            "bounds",
        ],
    },

This then detects all the cases without my manual assertions and was used in the final version of the fuzzer. (Note that full UBSAN was not used because of the vtable checks in UBSAN that made the fuzzer unusable, as not all dependencies are compiled with the sanitizer enabled.)

Conclusions

In this post I’ve described how I implemented a fuzzer for Android NFC and showed how to incorporate structured fuzzing into the fuzzer to resolve various problems. It also showed how fuzzing complements manual auditing efforts, and I was able to discover new bugs even after significant auditing performed by other researchers.