How to decrypt NS3 passwords from ZyXEL config file (NDMS V2)

ZyXEL routers, for example, the Keenetic Lite II model, have a System → Configuration tab where you can download the running-config (running configuration) file. This file contains the current settings, including the password for connecting to the Internet service provider and the password for the Wi-Fi network.

Passwords are stored encoded, for example:

authentication wpa-psk ns3 HfNV/Uq8ubal+UXhXERgHRdO

authentication password ns3 9IpS3eYkkEN3Vld3CXRZtnBz

The wpa-psk line is the encoded password from the Wi-Fi access point, and the password line is the password for the Internet connection to the ISP.

This encoding method is called differently: NS3 / NDMS V2 / ZyXEL config.

User Felis-Sapiens reverse-engineered the coding method and even wrote a Python 3 script to encode and decode.

And the user VasiliyP made another implementation of decoding in JavaScript, thanks to which users can decode NS3 (NDMS V2) right in the web browser window.

Decoding NS3 (NDMS V2) in Web Browser

Create zyxel_scramble.htm file:

gedit zyxel_scramble.htm

And copy into it

<!DOCTYPE html>

<html>
  
  <head>
    <meta charset="utf-8">

<meta name="robots" content="noindex">
    <title>ns3 decoder</title>
    <script>

var Base64 = {
    _keyStr: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
    decode: function (f_input) {
        var result = '';
        var v1, v2, v3;
        var v4, v5, v6, v7;
        var v8 = 0;
        f_input = f_input.replace(/[^A-Za-z0-9+/=]/g, '');
        while (v8 < f_input.length) {
            v4 = this._keyStr.indexOf(f_input.charAt(v8++));
            v5 = this._keyStr.indexOf(f_input.charAt(v8++));
            v6 = this._keyStr.indexOf(f_input.charAt(v8++));
            v7 = this._keyStr.indexOf(f_input.charAt(v8++));
            v1 = v4 << 2 | v5 >> 4;
            v2 = (v5 & 15) << 4 | v6 >> 2;
            v3 = (v6 & 3) << 6 | v7;
            result = result + String.fromCharCode(v1);
            if (v6 != 64) {
                result = result + String.fromCharCode(v2)
            };
            if (v7 != 64) {
                result = result + String.fromCharCode(v3)
            }
        };
        return result
    }
}
     
      function xor1(a0, buffer){
        var a1 = 0;
        var a3 = 0;

        var v1 = 1;
        for (;;) {
          v1++;
          if (v1 == a0) break;
          a3 ^= buffer[a1 + v1 - 2];
        }

        var a2 = 1;
        var t0 = 8;
        for (;;) {
          var v0 = 0xFF ^ a3;
          v0 = v0 << a2;
          var v1 = a3 >> a2;
          v1 ^= v0;
          v1 &= 0xFF;
          v0 = 0xFF ^ v1;
          v0 ^= a3;
          v1 = v1 << a2;
          v1 ^= v0;
          a2++;
          a3 = v1 & 0xFF;
          if (a2 == t0) break;
        }

        var v1 = 1;
        for (;;) {
          v1++;
          a1++;
          if (v1 == a0) return;
          buffer[a1 - 1] ^= a3;
        }
      }

      function xor2(a0, buffer, a2, a3){
        var a1 = 0;
        a2 &= 0xFF;
        a3 &= 0xFF;
        var v1 = 1;
        for (;;) {
          v1 += 1;
          a1++;
          if (v1 == a0) return;

          var v0 = buffer[a1 - 1];
          v0 ^= a3;
          a3 += a2;
          buffer[a1 - 1] = v0 & 0xFF;
          a2 = a3 - a2;
        }
      }

      function ns3decode(ns3){
        //var buffer = Base64.decode(ns3).split('').map(v => v.charCodeAt(0));
        var buffer = Base64.decode(ns3).split('');
        for (var i = 0; i < buffer.length; i++) buffer[i] = buffer[i].charCodeAt(0);

        var s4_size = buffer.length;
        var s2_size = s4_size - 2;
        var size24924925 = s4_size * 0x24924925;

        var hi_size24924925 = size24924925 / 0x100000000;
        var tmp = s4_size - hi_size24924925;

        var addr_move_to = (hi_size24924925 + (tmp / 2 )) / 4;
        var s0 = buffer.splice(addr_move_to, 1); 
        buffer.push(buffer[buffer.length - 1]);

        var mod = s0 % (s4_size - 1);

        var addr_move_to = mod;

        var s2 = buffer.splice(addr_move_to, 1); 
        buffer.push(buffer[buffer.length - 1]);

        xor1(s4_size, buffer);
        xor2(s4_size, buffer, s2, s0);
        //var str = buffer.map(v => String.fromCharCode(v)).join('');
        var arr = [];
        for (var i = 0; i < buffer.length; i++) arr[i] = String.fromCharCode(buffer[i]);
        var str = arr.join('');

        for(var i = 0; i < buffer.length; i++){
          if (!buffer[i]) {
            str = str.substr(0, i);
            break;
          }
        }

        return str;
      }
        
      function calculate(){
        var ns3 = document.getElementById("input").value;

        if(ns3.length == 0){
          document.getElementById("output").hidden = true;
          return;
        }

        var decoded = ns3decode(ns3);
        var box = document.getElementById("output");
        box.value = decoded;
        box.hidden = false;
        box.select();
      }

      function init() {
        var box = document.getElementById("input");

        box.ondragenter = function() {
          this.value = '';
          document.getElementById("output").value = '';
        };

        box.ondrop = function() {
          calculate();
        };


        box.onclick = function() {
          this.select();
        };

        box.select();

        var box = document.getElementById("output");
        box.onclick = function() {
          this.select();
        };
      }

    </script>


  </head>
  
  <body onload="init()" >
    
    <h1>ns3 decoder</h1>
    <input type="text" id="input" style="border: solid 1px #EEEEEE" value="BiEjZE6GOPmOmTtDIO0Gtcw+"/>
    <input type="submit" id="button" onClick="calculate()" value="decode"/>
    <br>
    <input type="text" id="output" hidden="true" style="border: solid 1px #EEEEEE" />


</body>
</html>

Now just open the file in any web browser.

Enter the data to decode into the window and click the “decode” button.

Encoding and decoding NS3 (NDMS V2) on the command line

Create zyxel_scramble.py file

gedit zyxel_scramble.py

And copy into it:

#!/usr/bin/env python3
#
# zyxel_scramble.py
#
# author: Felis-Sapiens
#
# Decode password from ZyXEL config (NDMS V2)

import sys
from base64 import b64decode, b64encode
from hashlib import md5

def first_step(password, x1, x2):
    x1 &= 0xff
    x2 &= 0xff
    for i in range(len(password)):
        password[i] = (password[i] ^ x2) & 0xff
        x2, x1 = x1 + x2, x2
    return password

def second_step(password):
    x = 0
    for b in password:
        x ^= b
    for i in range(1,8):
        a = ((x >> i) ^ (~x << i)) & 0xff
        x = ((a << i) ^ (~a ^ x)) & 0xff
    for i in range(len(password)):
        password[i] ^= x
    return password


def scramble_decode(password):
    if len(password) % 4 != 0:
        password += '=' * (4 - len(password) % 4)
 
    password = list(b64decode(password))
    length = len(password)
    if length not in [0x12, 0x24, 0x48]:
        print('ERROR: invalid input length')
        return
 
    a2 = length // 7
    x2 = password[a2]
    del password[a2]
 
    a1 = x2 % (length - 1)
    x1 = password[a1]
    del password[a1]
 
    password = second_step(password)
    password = first_step(password, x1, x2)
 
    zero_pos = password.index(0)
    if zero_pos != -1:
        length = zero_pos
    return bytes(password[:length]).decode()

def scramble_encode(password):
    old_length = len(password)
    length = len(password) + 3
    if length < 0x13:
        length = 0x12
    elif length < 0x25:
        length = 0x24
    elif length < 0x49:
        length = 0x48
    else:
        print('ERROR: password is too long')
        return
 
    password = password.encode()
    md5_digest = md5(password).digest()
 
    password = list(password)
    if length != old_length:
        password.append(0)
        for i in range(old_length+1, length-2):
            password.append((password[i-old_length-1] * 2 + md5_digest[2 + i%14]) & 0xff)
 
    x1 = md5_digest[0]
    x2 = md5_digest[1]
    password = first_step(password, x1, x2)
    password = second_step(password)
    password.insert(x2 % (length - 1), x1)
    password.insert(length // 7, x2)
    return b64encode(bytes(password))

def main():
    if len(sys.argv) < 3:
        print('Usage: zyxel_scramble.py decode|encode password')
        return
 
    if sys.argv[1] == 'decode':
        password = scramble_decode(sys.argv[2])
    elif sys.argv[1] == 'encode':
        password = scramble_encode(sys.argv[2])
    else:
        print('ERROR: unknown command', sys.argv[1])
        return
    if password is not None:
        print(password)

if __name__ == '__main__':
    main()

To decode, use a command like:

python3 zyxel_scramble.py decode 'STRING'

For example:

python3 zyxel_scramble.py decode '9IpS3eYkkEN3Vld3CXRZtnBz'

To encode to NS3 (NDMS V2) use a command of the form:

python3 zyxel_scramble.py encode 'STRING'

For example:

python3 zyxel_scramble.py encode 'HackWare'

A string like “b'HfNV/Uq8ubal+UXhXERgHRdO'” will be displayed – you need to take from it what is in single quotes.

Conclusion

Thus, you can recover the password for connecting to the Internet service provider if you accidentally reset your router to factory settings (or you have problems when updating the firmware), but you still have the router's configuration file.

Recommended for you:

Leave a Reply

Your email address will not be published. Required fields are marked *