Reverse Engineering TP-Link Archer AX18 (EU) Backup Configuration Encryption and Decryption

45 minute read

Published:

Reverse Engineering TP-Link Archer AX18 (EU) Backup Configuration Encryption and Decryption

First off, download the firmware from the TP-Link website and extract the downloaded zip file. After extraction, there is *.bin file that we’re supposed to upload to TPLink’s web portal to update the firmware.

Running binwalk on this *.bin file gave me this result:

$ binwalk AX18V1-*.bin 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
18476         0x482C          LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 12215964 bytes
3681319       0x382C27        Squashfs filesystem, little endian, version 4.0, compression:xz, size: 10011368 bytes, 2492 inodes, blocksize: 262144 bytes, created: 2038-06-24 01:57:20
13708730      0xD12DBA        Intel HEX data, record type: start linear address

We are interested in the Squashfs filesystem so we go ahead and extract the contents of the *.bin file with the command:

$ binwalk -e AX18V1-*.bin

The extracted files will be available in a directory with the same name of our *.bin file but with an underscore _ prepended and .extracted appended in the name.

Let’s change directory into that:

$ cd _AX18V1*.bin.extracted
$ ls -l
total 35148
-rw-rw-r--  1 kali kali 10011368 Sep  4 02:51 382C27.squashfs
-rw-rw-r--  1 kali kali 12215964 Sep  4 02:51 482C
-rw-rw-r--  1 kali kali 13721451 Sep  4 02:51 482C.7z
-rw-rw-r--  1 kali kali    31197 Sep  4 02:51 D12DBA.hex
drwxr-xr-x 17 kali kali     4096 Sep  4 02:51 squashfs-root

Change directory to squashfs-root directory:

$ cd squashfs-root

Then, find files relating to “backup”.

Here is the result of my search:

$ find . -name "*backup*" -exec file {} \;
./usr/bin/tr069/mybackup: Lua script, Unicode text, UTF-8 text executable
./www/webpages/modules/advanced/system/backupRestore: directory
./lib/sync-server/scripts/trans_backup_wcfg: Lua script, Unicode text, UTF-8 text executable

Of the three files in the search result, the most interesting file is ./usr/bin/tr069/mybackup.

#!/usr/bin/lua

local firm = require "luci.controller.admin.firmware"
local util = require "luci.util"
local cry = require "luci.model.crypto"
local configtool = require "luci.sys.config"
local fs = require "luci.fs"
local uci = require "luci.model.uci"
local uci_r = uci.cursor()

function backup()
        local product_info_md5 = firm.md5_product_info()
        local product_info_md5_file = io.open("/tmp/product_info_md5_file", "w")
        for num in string.gmatch(product_info_md5, "%x%x") do
                local number = "0x"..num
                product_info_md5_file:write(string.char(number))
        end
        product_info_md5_file:close()

        -- 备份extern分区, 如有特殊情况的分区,再特殊处理
        local extern_partitions = uci_r:get_profile("backup_restore", "extern_partition") or nil
        if extern_partitions ~= nil then
                extern_partitions = util.split(extern_partitions, " ")
                os.execute("mkdir /tmp/backup >/dev/null 2>&1")

                for i, v in ipairs(extern_partitions) do
                        if v ~= nil then
                                local externname = "/tmp/backup/ori-backup-" .. v .. ".bin"
                                luci.sys.exec("nvrammanager -r " .. externname .. " -p " .. v .. " >/dev/null 2>&1")
                                local filesize = fs.stat(externname).size
                                if ( v == 'router-config' or v == 'ap-config') and filesize > 0 then
                                        firm.hide_info(externname)
                                end
                        end
                end

                luci.sys.exec("nvrammanager -r /tmp/backup/ori-backup-user-config.bin -p user-config >/dev/null 2>&1")
                firm.hide_info("/tmp/backup/ori-backup-user-config.bin")

                --打包
                os.execute("tar -cf /tmp/ori-backup-userconf.bin -C /tmp/backup . >/dev/null 2>&1")
                luci.sys.exec("rm -rf /tmp/backup >/dev/null 2>&1")
        else
                luci.sys.exec("nvrammanager -r /tmp/ori-backup-userconf.bin -p user-config >/dev/null 2>&1")
                cry.dec_file_entry("/tmp/ori-backup-userconf.bin", "/tmp/tmp-backup-userconf.xml")
                luci.sys.exec("mkdir -p /tmp/backupcfg")
                configtool.xmlToFile("/tmp/tmp-backup-userconf.xml", "/tmp/backupcfg")
                -- hide cloud info config
                local hide_files = {"accountmgnt", "cloud_config"}
                for _, f in ipairs(hide_files) do 
                        luci.sys.exec("rm -f /tmp/backupcfg/config/" .. f)
                end 
                -- recreate xml config files
                luci.sys.exec("rm -f /tmp/ori-backup-userconf.bin;rm -f /tmp/tmp-backup-userconf.xml")
                configtool.convertFileToXml("/tmp/backupcfg/config", "/tmp/tmp-backup-userconf.xml")
                cry.enc_file_entry("/tmp/tmp-backup-userconf.xml", "/tmp/ori-backup-userconf.bin")
                luci.sys.exec("rm -rf /tmp/backupcfg;rm -f /tmp/tmp-backup-userconf.xml")
        end

        luci.sys.exec("cat /tmp/product_info_md5_file /tmp/ori-backup-userconf.bin > /tmp/mid-backup-userconf.bin")
        cry.enc_file_entry("/tmp/mid-backup-userconf.bin", "/tmp/save-backup-userconf.bin")
end

backup()

From the ./usr/bin/tr069/mybackup lua script above, we can see that it depends on luci.model.crypto and it calls the function enc_file_entry and dec_file_entry to encrypt/decrypt the user configuration file.

Looking at the crypto.lua file, we can see that it is a compiled Lua and meant to run on MIPS32 Little Endian architecture.

$ find . -name "crypto.lua" -exec file {} \;
./usr/lib/lua/luci/model/crypto.lua: Lua bytecode, version 5.1

We need to decompile this Lua bytecode, however, to do that, we need to emulate a MIPS32 Little Endian architecture virtual machine.

Enter QEMU

Decompiling Lua Bytecode

The first step to this goal is to install the required dependencies. Then, we package the firmware into an image file and convert the image file to a qcow2 file format. We then download a MIPS kernel and I chose vmlinux-3.2.0-4-4kc-malta. Finally, we can start emulating the MIPS32 Little Endian virtual machine with QEMU.

Putting this all together, here is the bash script I used.

sudo apt update
sudo apt install qemu-system-mips qemu-utils bridge-utils uml-utilities

# Create working directory
cd ..
mkdir firmware-emulation
cd firmware-emulation

# Copy your extracted squash-fs to this directory
cp -r ../squashfs-root ./rootfs

# Create necessary directories if missing
cd rootfs
sudo mkdir -p dev proc sys tmp var/run var/log

cd dev
sudo mknod console c 5 1
sudo mknod null c 1 3
sudo mknod zero c 1 5
sudo mknod random c 1 8
sudo mknod urandom c 1 9
sudo mknod tty c 5 0
sudo mknod tty0 c 4 0
sudo mknod tty1 c 4 1
sudo mknod ttyS0 c 4 64
sudo chmod 666 console null zero random urandom tty*
cd ..

dd if=/dev/zero of=rootfs.img bs=1M count=256
mkfs.ext2 rootfs.img
mkdir -p mnt
sudo mount -o loop rootfs.img mnt
sudo cp -a rootfs/* mnt/
# Create a simple init script
sudo cat > mnt/sbin/init << 'EOF'
#!/bin/sh

# Mount essential filesystems
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev

# Create basic device nodes if they don't exist
[ ! -e /dev/console ] && mknod /dev/console c 5 1
[ ! -e /dev/null ] && mknod /dev/null c 1 3

# Set up environment
export PATH="/bin:/sbin:/usr/bin:/usr/sbin"
export HOME="/root"

# Try to run the original init if it exists
if [ -x /etc/init.d/rcS ]; then
    exec /etc/init.d/rcS
elif [ -x /bin/busybox ]; then
    exec /bin/busybox sh
else
    # Drop to shell
    echo "Starting shell..."
    exec /bin/sh
fi
EOF

sudo chmod +x mnt/sbin/init

# Also ensure shell exists and is executable
ls -la mnt/bin/sh
sudo chmod +x mnt/bin/sh 2>/dev/null || true
sudo umount mnt
qemu-img convert -f raw -O qcow2 rootfs.img rootfs.qcow2

wget https://people.debian.org/~aurel32/qemu/mipsel/vmlinux-3.2.0-4-4kc-malta
mv vmlinux-3.2.0-4-4kc-malta vmlinux

cat > launch-firmware.sh << 'EOF'
#!/bin/bash

qemu-system-mipsel \
    -M malta \
    -kernel vmlinux \
    -hda rootfs.qcow2 \
    -append "root=/dev/sda rw console=ttyS0 init=/sbin/init" \
    -nographic \
    -serial stdio \
    -monitor telnet:127.0.0.1:1234,server,nowait \
    -no-reboot \
    -m 256M

EOF
chmod +x launch-firmware.sh
./launch-firmware.sh

If all goes well, we will be presented with a shell to the virtual machine.

# uname -a
Linux (none) 3.2.0-4-4kc-malta #1 Debian 3.2.51-1 mips GNU/Linux

UPDATE: I later learned that instead of emulating a MIPS32 Little Endian virtual machine with QEMU, I can just run a binary that is compiled with the target architecture, e.g. qemu-mipsel ./luac -l /usr/lib/lua/luci/model/crypto.lua. It is still a learning experience for me as I was new to QEMU when I used it and wrote this blog post.

Perfect! At this point, we can use the luac tool to display the Lua bytecode with the command:

# luac -l /usr/lib/lua/luci/model/crypto.lua

main <?:0,0> (115 instructions, 460 bytes at 0x879d28)
0+ params, 21 slots, 0 upvalues, 0 locals, 35 constants, 15 functions
1       [-]     GETGLOBAL       0 -1    ; require
2       [-]     LOADK           1 -2    ; "luci.sys"
3       [-]     CALL            0 2 2
4       [-]     GETGLOBAL       1 -1    ; require
5       [-]     LOADK           2 -3    ; "nixio"
6       [-]     CALL            1 2 2
7       [-]     GETGLOBAL       2 -1    ; require
8       [-]     LOADK           3 -4    ; "luci.util"
9       [-]     CALL            2 2 2
10      [-]     GETGLOBAL       3 -5    ; module
11      [-]     LOADK           4 -6    ; "luci.model.crypto"
12      [-]     GETGLOBAL       5 -7    ; package
13      [-]     GETTABLE        5 5 -8  ; "seeall"
14      [-]     CALL            3 3 1
15      [-]     LOADK           3 -9    ; "aes-256-cbc"
16      [-]     LOADK           4 -10   ; "openssl zlib -e %s | openssl "
17      [-]     MOVE            5 3
18      [-]     LOADK           6 -11   ; " -e %s"
19      [-]     CONCAT          4 4 6
20      [-]     LOADK           5 -12   ; "openssl "
21      [-]     MOVE            6 3
22      [-]     LOADK           7 -13   ; " -d %s %s | openssl zlib -d"
23      [-]     CONCAT          5 5 7
24      [-]     LOADK           6 -12   ; "openssl "
25      [-]     MOVE            7 3
26      [-]     LOADK           8 -14   ; " -e %s %s"
27      [-]     CONCAT          6 6 8
28      [-]     LOADK           7 -12   ; "openssl "
29      [-]     MOVE            8 3
30      [-]     LOADK           9 -15   ; " -d %s %s"
31      [-]     CONCAT          7 7 9
32      [-]     LOADK           8 -16   ; "-in %q"
33      [-]     LOADK           9 -17   ; "-k %q"
34      [-]     LOADK           10 -18  ; "-kfile /etc/secretkey"
35      [-]     LOADK           11 -10  ; "openssl zlib -e %s | openssl "
36      [-]     MOVE            12 3
37      [-]     LOADK           13 -11  ; " -e %s"
38      [-]     CONCAT          11 11 13
39      [-]     LOADK           12 -12  ; "openssl "
40      [-]     MOVE            13 3
41      [-]     LOADK           14 -13  ; " -d %s %s | openssl zlib -d"
42      [-]     CONCAT          12 12 14
43      [-]     LOADK           13 -12  ; "openssl "
44      [-]     MOVE            14 3
45      [-]     LOADK           15 -14  ; " -e %s %s"
46      [-]     CONCAT          13 13 15
47      [-]     LOADK           14 -12  ; "openssl "
48      [-]     MOVE            15 3
49      [-]     LOADK           16 -15  ; " -d %s %s"
50      [-]     CONCAT          14 14 16
51      [-]     LOADK           15 -19  ; "2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836"
52      [-]     LOADK           16 -20  ; "360028C9064242F81074F4C127D299F6"
53      [-]     LOADK           17 -21  ; "-K "
54      [-]     MOVE            18 15
55      [-]     LOADK           19 -22  ; " -iv "
56      [-]     MOVE            20 16
57      [-]     CONCAT          17 17 20
58      [-]     CLOSURE         18 0    ; 0x87a840
59      [-]     MOVE            0 6
60      [-]     MOVE            0 4
61      [-]     MOVE            0 7
62      [-]     MOVE            0 5
63      [-]     MOVE            0 8
64      [-]     MOVE            0 9
65      [-]     MOVE            0 10
66      [-]     CLOSURE         19 1    ; 0x87a920
67      [-]     MOVE            0 13
68      [-]     MOVE            0 11
69      [-]     MOVE            0 14
70      [-]     MOVE            0 12
71      [-]     MOVE            0 8
72      [-]     MOVE            0 17
73      [-]     CLOSURE         20 2    ; 0x87aa00
74      [-]     MOVE            0 1
75      [-]     SETGLOBAL       20 -23  ; crypt_used_openssl
76      [-]     CLOSURE         20 3    ; 0x87ab20
77      [-]     MOVE            0 19
78      [-]     MOVE            0 0
79      [-]     SETGLOBAL       20 -24  ; enc_file
80      [-]     CLOSURE         20 4    ; 0x87ac70
81      [-]     MOVE            0 15
82      [-]     MOVE            0 16
83      [-]     SETGLOBAL       20 -25  ; wolfssl_enc_dec_file
84      [-]     CLOSURE         20 5    ; 0x87afb0
85      [-]     MOVE            0 19
86      [-]     MOVE            0 0
87      [-]     SETGLOBAL       20 -26  ; dec_file
88      [-]     CLOSURE         20 6    ; 0x87b0a0
89      [-]     MOVE            0 18
90      [-]     MOVE            0 0
91      [-]     SETGLOBAL       20 -27  ; enc
92      [-]     CLOSURE         20 7    ; 0x87b180
93      [-]     MOVE            0 18
94      [-]     MOVE            0 0
95      [-]     SETGLOBAL       20 -28  ; dec
96      [-]     CLOSURE         20 8    ; 0x87b260
97      [-]     MOVE            0 1
98      [-]     SETGLOBAL       20 -29  ; onemesh_ltn12_open
99      [-]     CLOSURE         20 9    ; 0x87b7b8
100     [-]     MOVE            0 18
101     [-]     SETGLOBAL       20 -30  ; onemesh_enc
102     [-]     CLOSURE         20 10   ; 0x87b890
103     [-]     MOVE            0 18
104     [-]     SETGLOBAL       20 -31  ; onemesh_dec
105     [-]     CLOSURE         20 11   ; 0x87b968
106     [-]     MOVE            0 15
107     [-]     MOVE            0 16
108     [-]     SETGLOBAL       20 -32  ; wolfssl_enc_dec
109     [-]     CLOSURE         20 12   ; 0x87bbb8
110     [-]     SETGLOBAL       20 -33  ; dump_to_file
111     [-]     CLOSURE         20 13   ; 0x87bcc0
112     [-]     SETGLOBAL       20 -34  ; enc_file_entry
113     [-]     CLOSURE         20 14   ; 0x87bdb8
114     [-]     SETGLOBAL       20 -35  ; dec_file_entry
115     [-]     RETURN          0 1
function <?:36,45> (34 instructions, 136 bytes at 0x87a840)
4 params, 8 slots, 7 upvalues, 0 locals, 1 constant, 0 functions
1       [-]     TEST            3 0 0
2       [-]     JMP             7       ; to 10
3       [-]     TEST            2 0 0
4       [-]     JMP             3       ; to 8
5       [-]     GETUPVAL        5 0     ; -
6       [-]     TESTSET         4 5 1
7       [-]     JMP             1       ; to 9
8       [-]     GETUPVAL        4 1     ; -
9       [-]     JMP             6       ; to 16
10      [-]     TEST            2 0 0
11      [-]     JMP             3       ; to 15
12      [-]     GETUPVAL        5 2     ; -
13      [-]     TESTSET         4 5 1
14      [-]     JMP             1       ; to 16
15      [-]     GETUPVAL        4 3     ; -
16      [-]     NEWTABLE        5 2 0
17      [-]     TEST            0 0 0
18      [-]     JMP             4       ; to 23
19      [-]     GETUPVAL        6 4     ; -
20      [-]     MOD             6 6 0
21      [-]     TEST            6 0 1
22      [-]     JMP             1       ; to 24
23      [-]     LOADK           6 -1    ; ""
24      [-]     TEST            1 0 0
25      [-]     JMP             4       ; to 30
26      [-]     GETUPVAL        7 5     ; -
27      [-]     MOD             7 7 1
28      [-]     TEST            7 0 1
29      [-]     JMP             1       ; to 31
30      [-]     GETUPVAL        7 6     ; -
31      [-]     SETLIST         5 2 1   ; 1
32      [-]     MOD             5 4 5
33      [-]     RETURN          5 2
34      [-]     RETURN          0 1
function <?:47,55> (28 instructions, 112 bytes at 0x87a920)
3 params, 7 slots, 6 upvalues, 0 locals, 1 constant, 0 functions
1       [-]     TEST            2 0 0
2       [-]     JMP             7       ; to 10
3       [-]     TEST            1 0 0
4       [-]     JMP             3       ; to 8
5       [-]     GETUPVAL        4 0     ; -
6       [-]     TESTSET         3 4 1
7       [-]     JMP             1       ; to 9
8       [-]     GETUPVAL        3 1     ; -
9       [-]     JMP             6       ; to 16
10      [-]     TEST            1 0 0
11      [-]     JMP             3       ; to 15
12      [-]     GETUPVAL        4 2     ; -
13      [-]     TESTSET         3 4 1
14      [-]     JMP             1       ; to 16
15      [-]     GETUPVAL        3 3     ; -
16      [-]     NEWTABLE        4 2 0
17      [-]     TEST            0 0 0
18      [-]     JMP             4       ; to 23
19      [-]     GETUPVAL        5 4     ; -
20      [-]     MOD             5 5 0
21      [-]     TEST            5 0 1
22      [-]     JMP             1       ; to 24
23      [-]     LOADK           5 -1    ; ""
24      [-]     GETUPVAL        6 5     ; -
25      [-]     SETLIST         4 2 1   ; 1
26      [-]     MOD             4 3 4
27      [-]     RETURN          4 2
28      [-]     RETURN          0 1
function <?:57,63> (13 instructions, 52 bytes at 0x87aa00)
0 params, 2 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1       [-]     GETUPVAL        0 0     ; -
2       [-]     GETTABLE        0 0 -1  ; "fs"
3       [-]     GETTABLE        0 0 -2  ; "stat"
4       [-]     LOADK           1 -3    ; "/usr/bin/openssl"
5       [-]     CALL            0 2 2
6       [-]     TEST            0 0 0
7       [-]     JMP             3       ; to 11
8       [-]     LOADBOOL        0 1 0
9       [-]     RETURN          0 2
10      [-]     JMP             2       ; to 13
11      [-]     LOADBOOL        0 0 0
12      [-]     RETURN          0 2
13      [-]     RETURN          0 1
function <?:80,86> (21 instructions, 84 bytes at 0x87ab20)
2 params, 6 slots, 2 upvalues, 0 locals, 4 constants, 0 functions
1       [-]     GETGLOBAL       2 -1    ; type
2       [-]     MOVE            3 0
3       [-]     CALL            2 2 2
4       [-]     EQ              0 2 -2  ; - "string"
5       [-]     JMP             3       ; to 9
6       [-]     LEN             2 0
7       [-]     EQ              0 2 -3  ; - 0
8       [-]     JMP             2       ; to 11
9       [-]     LOADNIL         2 2
10      [-]     RETURN          2 2
11      [-]     GETUPVAL        2 0     ; -
12      [-]     MOVE            3 0
13      [-]     MOVE            4 1
14      [-]     LOADBOOL        5 1 0
15      [-]     CALL            2 4 2
16      [-]     GETUPVAL        3 1     ; -
17      [-]     GETTABLE        3 3 -4  ; "ltn12_popen"
18      [-]     MOVE            4 2
19      [-]     TAILCALL        3 2 0
20      [-]     RETURN          3 0
21      [-]     RETURN          0 1
function <?:96,137> (83 instructions, 332 bytes at 0x87ac70)
6 params, 14 slots, 2 upvalues, 0 locals, 14 constants, 0 functions
1       [-]     GETGLOBAL       6 -1    ; type
2       [-]     MOVE            7 0
3       [-]     CALL            6 2 2
4       [-]     EQ              0 6 -2  ; - "string"
5       [-]     JMP             11      ; to 17
6       [-]     LEN             6 0
7       [-]     EQ              1 6 -3  ; - 0
8       [-]     JMP             8       ; to 17
9       [-]     GETGLOBAL       6 -1    ; type
10      [-]     MOVE            7 1
11      [-]     CALL            6 2 2
12      [-]     EQ              0 6 -2  ; - "string"
13      [-]     JMP             3       ; to 17
14      [-]     LEN             6 1
15      [-]     EQ              0 6 -3  ; - 0
16      [-]     JMP             2       ; to 19
17      [-]     LOADBOOL        6 0 0
18      [-]     RETURN          6 2
19      [-]     EQ              0 2 -4  ; - nil
20      [-]     JMP             2       ; to 23
21      [-]     LOADBOOL        6 0 0
22      [-]     RETURN          6 2
23      [-]     EQ              0 3 -4  ; - nil
24      [-]     JMP             2       ; to 27
25      [-]     GETUPVAL        3 0     ; -
26      [-]     JMP             16      ; to 43
27      [-]     GETGLOBAL       6 -1    ; type
28      [-]     MOVE            7 3
29      [-]     CALL            6 2 2
30      [-]     EQ              0 6 -2  ; - "string"
31      [-]     JMP             9       ; to 41
32      [-]     LEN             6 3
33      [-]     EQ              1 6 -5  ; - 32
34      [-]     JMP             8       ; to 43
35      [-]     LEN             6 3
36      [-]     EQ              1 6 -6  ; - 48
37      [-]     JMP             5       ; to 43
38      [-]     LEN             6 3
39      [-]     EQ              1 6 -7  ; - 64
40      [-]     JMP             2       ; to 43
41      [-]     LOADBOOL        6 0 0
42      [-]     RETURN          6 2
43      [-]     EQ              0 4 -4  ; - nil
44      [-]     JMP             2       ; to 47
45      [-]     GETUPVAL        4 1     ; -
46      [-]     JMP             10      ; to 57
47      [-]     GETGLOBAL       6 -1    ; type
48      [-]     MOVE            7 4
49      [-]     CALL            6 2 2
50      [-]     EQ              0 6 -2  ; - "string"
51      [-]     JMP             3       ; to 55
52      [-]     LEN             6 4
53      [-]     EQ              1 6 -5  ; - 32
54      [-]     JMP             2       ; to 57
55      [-]     LOADBOOL        6 0 0
56      [-]     RETURN          6 2
57      [-]     EQ              0 5 -4  ; - nil
58      [-]     JMP             1       ; to 60
59      [-]     LOADK           5 -3    ; 0
60      [-]     GETGLOBAL       6 -8    ; require
61      [-]     LOADK           7 -9    ; "luarsa"
62      [-]     CALL            6 2 2
63      [-]     GETTABLE        7 6 -10 ; "aes_enc_file"
64      [-]     MOVE            8 0
65      [-]     MOVE            9 1
66      [-]     MOVE            10 3
67      [-]     MOVE            11 4
68      [-]     MOVE            12 2
69      [-]     MOVE            13 5
70      [-]     CALL            7 7 2
71      [-]     EQ              0 7 -4  ; - nil
72      [-]     JMP             9       ; to 82
73      [-]     GETGLOBAL       8 -11   ; io
74      [-]     GETTABLE        8 8 -12 ; "open"
75      [-]     MOVE            9 1
76      [-]     LOADK           10 -13  ; "w"
77      [-]     CALL            8 3 2
78      [-]     TEST            8 0 0
79      [-]     JMP             2       ; to 82
80      [-]     SELF            9 8 -14 ; "close"
81      [-]     CALL            9 2 1
82      [-]     RETURN          7 2
83      [-]     RETURN          0 1
function <?:143,149> (21 instructions, 84 bytes at 0x87afb0)
2 params, 6 slots, 2 upvalues, 0 locals, 4 constants, 0 functions
1       [-]     GETGLOBAL       2 -1    ; type
2       [-]     MOVE            3 0
3       [-]     CALL            2 2 2
4       [-]     EQ              0 2 -2  ; - "string"
5       [-]     JMP             3       ; to 9
6       [-]     LEN             2 0
7       [-]     EQ              0 2 -3  ; - 0
8       [-]     JMP             2       ; to 11
9       [-]     LOADNIL         2 2
10      [-]     RETURN          2 2
11      [-]     GETUPVAL        2 0     ; -
12      [-]     MOVE            3 0
13      [-]     MOVE            4 1
14      [-]     LOADBOOL        5 0 0
15      [-]     CALL            2 4 2
16      [-]     GETUPVAL        3 1     ; -
17      [-]     GETTABLE        3 3 -4  ; "ltn12_popen"
18      [-]     MOVE            4 2
19      [-]     TAILCALL        3 2 0
20      [-]     RETURN          3 0
21      [-]     RETURN          0 1
function <?:156,162> (20 instructions, 80 bytes at 0x87b0a0)
3 params, 8 slots, 2 upvalues, 0 locals, 3 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; type
2       [-]     MOVE            4 0
3       [-]     CALL            3 2 2
4       [-]     EQ              1 3 -2  ; - "string"
5       [-]     JMP             2       ; to 8
6       [-]     LOADNIL         3 3
7       [-]     RETURN          3 2
8       [-]     GETUPVAL        3 0     ; -
9       [-]     LOADNIL         4 4
10      [-]     MOVE            5 1
11      [-]     MOVE            6 2
12      [-]     LOADBOOL        7 1 0
13      [-]     CALL            3 5 2
14      [-]     GETUPVAL        4 1     ; -
15      [-]     GETTABLE        4 4 -3  ; "ltn12_popen"
16      [-]     MOVE            5 3
17      [-]     MOVE            6 0
18      [-]     TAILCALL        4 3 0
19      [-]     RETURN          4 0
20      [-]     RETURN          0 1
function <?:169,175> (20 instructions, 80 bytes at 0x87b180)
3 params, 8 slots, 2 upvalues, 0 locals, 3 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; type
2       [-]     MOVE            4 0
3       [-]     CALL            3 2 2
4       [-]     EQ              1 3 -2  ; - "string"
5       [-]     JMP             2       ; to 8
6       [-]     LOADNIL         3 3
7       [-]     RETURN          3 2
8       [-]     GETUPVAL        3 0     ; -
9       [-]     LOADNIL         4 4
10      [-]     MOVE            5 1
11      [-]     MOVE            6 2
12      [-]     LOADBOOL        7 0 0
13      [-]     CALL            3 5 2
14      [-]     GETUPVAL        4 1     ; -
15      [-]     GETTABLE        4 4 -3  ; "ltn12_popen"
16      [-]     MOVE            5 3
17      [-]     MOVE            6 0
18      [-]     TAILCALL        4 3 0
19      [-]     RETURN          4 0
20      [-]     RETURN          0 1
function <?:177,222> (67 instructions, 268 bytes at 0x87b260)
2 params, 11 slots, 1 upvalue, 0 locals, 11 constants, 1 function
1       [-]     GETUPVAL        2 0     ; -
2       [-]     GETTABLE        2 2 -1  ; "pipe"
3       [-]     CALL            2 1 3
4       [-]     LOADNIL         4 5
5       [-]     TEST            1 0 0
6       [-]     JMP             5       ; to 12
7       [-]     GETUPVAL        6 0     ; -
8       [-]     GETTABLE        6 6 -1  ; "pipe"
9       [-]     CALL            6 1 3
10      [-]     MOVE            5 7
11      [-]     MOVE            4 6
12      [-]     GETUPVAL        6 0     ; -
13      [-]     GETTABLE        6 6 -2  ; "fork"
14      [-]     CALL            6 1 2
15      [-]     LT              0 -3 6  ; 0 -
16      [-]     JMP             20      ; to 37
17      [-]     TEST            1 0 0
18      [-]     JMP             7       ; to 26
19      [-]     SELF            7 5 -4  ; "write"
20      [-]     MOVE            9 1
21      [-]     CALL            7 3 1
22      [-]     SELF            7 4 -5  ; "close"
23      [-]     CALL            7 2 1
24      [-]     SELF            7 5 -5  ; "close"
25      [-]     CALL            7 2 1
26      [-]     SELF            7 3 -5  ; "close"
27      [-]     CALL            7 2 1
28      [-]     LOADNIL         7 7
29      [-]     CLOSURE         8 0     ; 0x87b598
30      [-]     MOVE            0 2
31      [-]     GETUPVAL        0 0     ; -
32      [-]     MOVE            0 6
33      [-]     MOVE            0 7
34      [-]     RETURN          8 2
35      [-]     CLOSE           7
36      [-]     JMP             30      ; to 67
37      [-]     EQ              0 6 -3  ; - 0
38      [-]     JMP             28      ; to 67
39      [-]     GETUPVAL        7 0     ; -
40      [-]     GETTABLE        7 7 -6  ; "dup"
41      [-]     MOVE            8 3
42      [-]     GETUPVAL        9 0     ; -
43      [-]     GETTABLE        9 9 -7  ; "stdout"
44      [-]     CALL            7 3 1
45      [-]     SELF            7 2 -5  ; "close"
46      [-]     CALL            7 2 1
47      [-]     SELF            7 3 -5  ; "close"
48      [-]     CALL            7 2 1
49      [-]     TEST            1 0 0
50      [-]     JMP             10      ; to 61
51      [-]     GETUPVAL        7 0     ; -
52      [-]     GETTABLE        7 7 -6  ; "dup"
53      [-]     MOVE            8 4
54      [-]     GETUPVAL        9 0     ; -
55      [-]     GETTABLE        9 9 -8  ; "stdin"
56      [-]     CALL            7 3 1
57      [-]     SELF            7 4 -5  ; "close"
58      [-]     CALL            7 2 1
59      [-]     SELF            7 5 -5  ; "close"
60      [-]     CALL            7 2 1
61      [-]     GETUPVAL        7 0     ; -
62      [-]     GETTABLE        7 7 -9  ; "exec"
63      [-]     LOADK           8 -10   ; "/bin/sh"
64      [-]     LOADK           9 -11   ; "-c"
65      [-]     MOVE            10 0
66      [-]     CALL            7 4 1
67      [-]     RETURN          0 1
function <?:193,210> (44 instructions, 176 bytes at 0x87b598)
1 param, 6 slots, 4 upvalues, 0 locals, 9 constants, 0 functions
1       [-]     TEST            0 0 1
2       [-]     JMP             1       ; to 4
3       [-]     LOADK           0 -1    ; 2048
4       [-]     GETUPVAL        1 0     ; -
5       [-]     SELF            1 1 -2  ; "read"
6       [-]     MOVE            3 0
7       [-]     CALL            1 3 2
8       [-]     GETUPVAL        2 1     ; -
9       [-]     GETTABLE        2 2 -3  ; "waitpid"
10      [-]     GETUPVAL        3 2     ; -
11      [-]     LOADK           4 -4    ; "nohang"
12      [-]     CALL            2 3 3
13      [-]     GETUPVAL        4 3     ; -
14      [-]     TEST            4 0 1
15      [-]     JMP             4       ; to 20
16      [-]     TEST            2 0 0
17      [-]     JMP             2       ; to 20
18      [-]     EQ              1 3 -5  ; - "exited"
19      [-]     JMP             4       ; to 24
20      [-]     TEST            2 0 1
21      [-]     JMP             4       ; to 26
22      [-]     EQ              0 3 -6  ; - 10
23      [-]     JMP             2       ; to 26
24      [-]     LOADBOOL        4 1 0
25      [-]     SETUPVAL        4 3     ; -
26      [-]     TEST            1 0 0
27      [-]     JMP             5       ; to 33
28      [-]     LEN             4 1
29      [-]     LT              0 -7 4  ; 0 -
30      [-]     JMP             2       ; to 33
31      [-]     RETURN          1 2
32      [-]     JMP             11      ; to 44
33      [-]     GETUPVAL        4 3     ; -
34      [-]     TEST            4 0 0
35      [-]     JMP             6       ; to 42
36      [-]     GETUPVAL        4 0     ; -
37      [-]     SELF            4 4 -8  ; "close"
38      [-]     CALL            4 2 1
39      [-]     LOADNIL         4 4
40      [-]     RETURN          4 2
41      [-]     JMP             2       ; to 44
42      [-]     LOADK           4 -9    ; ""
43      [-]     RETURN          4 2
44      [-]     RETURN          0 1
function <?:224,230> (19 instructions, 76 bytes at 0x87b7b8)
3 params, 8 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; type
2       [-]     MOVE            4 0
3       [-]     CALL            3 2 2
4       [-]     EQ              1 3 -2  ; - "string"
5       [-]     JMP             2       ; to 8
6       [-]     LOADNIL         3 3
7       [-]     RETURN          3 2
8       [-]     GETUPVAL        3 0     ; -
9       [-]     LOADNIL         4 4
10      [-]     MOVE            5 1
11      [-]     MOVE            6 2
12      [-]     LOADBOOL        7 1 0
13      [-]     CALL            3 5 2
14      [-]     GETGLOBAL       4 -3    ; onemesh_ltn12_open
15      [-]     MOVE            5 3
16      [-]     MOVE            6 0
17      [-]     TAILCALL        4 3 0
18      [-]     RETURN          4 0
19      [-]     RETURN          0 1
function <?:232,238> (19 instructions, 76 bytes at 0x87b890)
3 params, 8 slots, 1 upvalue, 0 locals, 3 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; type
2       [-]     MOVE            4 0
3       [-]     CALL            3 2 2
4       [-]     EQ              1 3 -2  ; - "string"
5       [-]     JMP             2       ; to 8
6       [-]     LOADNIL         3 3
7       [-]     RETURN          3 2
8       [-]     GETUPVAL        3 0     ; -
9       [-]     LOADNIL         4 4
10      [-]     MOVE            5 1
11      [-]     MOVE            6 2
12      [-]     LOADBOOL        7 0 0
13      [-]     CALL            3 5 2
14      [-]     GETGLOBAL       4 -3    ; onemesh_ltn12_open
15      [-]     MOVE            5 3
16      [-]     MOVE            6 0
17      [-]     TAILCALL        4 3 0
18      [-]     RETURN          4 0
19      [-]     RETURN          0 1
function <?:246,277> (64 instructions, 256 bytes at 0x87b968)
4 params, 9 slots, 2 upvalues, 0 locals, 11 constants, 0 functions
1       [-]     GETGLOBAL       4 -1    ; type
2       [-]     MOVE            5 0
3       [-]     CALL            4 2 2
4       [-]     EQ              1 4 -2  ; - "string"
5       [-]     JMP             2       ; to 8
6       [-]     LOADNIL         4 4
7       [-]     RETURN          4 2
8       [-]     EQ              0 1 -3  ; - nil
9       [-]     JMP             2       ; to 12
10      [-]     LOADNIL         4 4
11      [-]     RETURN          4 2
12      [-]     EQ              0 2 -3  ; - nil
13      [-]     JMP             2       ; to 16
14      [-]     GETUPVAL        2 0     ; -
15      [-]     JMP             16      ; to 32
16      [-]     GETGLOBAL       4 -1    ; type
17      [-]     MOVE            5 2
18      [-]     CALL            4 2 2
19      [-]     EQ              0 4 -2  ; - "string"
20      [-]     JMP             9       ; to 30
21      [-]     LEN             4 2
22      [-]     EQ              1 4 -4  ; - 32
23      [-]     JMP             8       ; to 32
24      [-]     LEN             4 2
25      [-]     EQ              1 4 -5  ; - 48
26      [-]     JMP             5       ; to 32
27      [-]     LEN             4 2
28      [-]     EQ              1 4 -6  ; - 64
29      [-]     JMP             2       ; to 32
30      [-]     LOADNIL         4 4
31      [-]     RETURN          4 2
32      [-]     EQ              0 3 -3  ; - nil
33      [-]     JMP             2       ; to 36
34      [-]     GETUPVAL        3 1     ; -
35      [-]     JMP             10      ; to 46
36      [-]     GETGLOBAL       4 -1    ; type
37      [-]     MOVE            5 3
38      [-]     CALL            4 2 2
39      [-]     EQ              0 4 -2  ; - "string"
40      [-]     JMP             3       ; to 44
41      [-]     LEN             4 3
42      [-]     EQ              1 4 -4  ; - 32
43      [-]     JMP             2       ; to 46
44      [-]     LOADNIL         4 4
45      [-]     RETURN          4 2
46      [-]     GETGLOBAL       4 -7    ; require
47      [-]     LOADK           5 -8    ; "luarsa"
48      [-]     CALL            4 2 2
49      [-]     EQ              0 1 -9  ; - true
50      [-]     JMP             7       ; to 58
51      [-]     GETTABLE        5 4 -10 ; "aes_enc"
52      [-]     MOVE            6 0
53      [-]     MOVE            7 2
54      [-]     MOVE            8 3
55      [-]     TAILCALL        5 4 0
56      [-]     RETURN          5 0
57      [-]     JMP             6       ; to 64
58      [-]     GETTABLE        5 4 -11 ; "aes_dec"
59      [-]     MOVE            6 0
60      [-]     MOVE            7 2
61      [-]     MOVE            8 3
62      [-]     TAILCALL        5 4 0
63      [-]     RETURN          5 0
64      [-]     RETURN          0 1
function <?:282,291> (22 instructions, 88 bytes at 0x87bbb8)
2 params, 7 slots, 0 upvalues, 0 locals, 5 constants, 0 functions
1       [-]     GETGLOBAL       2 -1    ; io
2       [-]     GETTABLE        2 2 -2  ; "open"
3       [-]     MOVE            3 1
4       [-]     LOADK           4 -3    ; "w"
5       [-]     CALL            2 3 2
6       [-]     TEST            2 0 1
7       [-]     JMP             1       ; to 9
8       [-]     RETURN          0 1
9       [-]     MOVE            3 0
10      [-]     CALL            3 1 2
11      [-]     TEST            3 0 0
12      [-]     JMP             7       ; to 20
13      [-]     SELF            4 2 -4  ; "write"
14      [-]     MOVE            6 3
15      [-]     CALL            4 3 1
16      [-]     MOVE            4 0
17      [-]     CALL            4 1 2
18      [-]     MOVE            3 4
19      [-]     JMP             -9      ; to 11
20      [-]     SELF            4 2 -5  ; "close"
21      [-]     CALL            4 2 1
22      [-]     RETURN          0 1
function <?:298,306> (19 instructions, 76 bytes at 0x87bcc0)
3 params, 7 slots, 0 upvalues, 0 locals, 5 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; crypt_used_openssl
2       [-]     CALL            3 1 2
3       [-]     TEST            3 0 0
4       [-]     JMP             8       ; to 13
5       [-]     GETGLOBAL       3 -2    ; enc_file
6       [-]     MOVE            4 0
7       [-]     CALL            3 2 2
8       [-]     GETGLOBAL       4 -3    ; dump_to_file
9       [-]     MOVE            5 3
10      [-]     MOVE            6 1
11      [-]     CALL            4 3 1
12      [-]     JMP             5       ; to 18
13      [-]     GETGLOBAL       3 -4    ; wolfssl_enc_dec_file
14      [-]     MOVE            4 0
15      [-]     MOVE            5 1
16      [-]     LOADK           6 -5    ; 1
17      [-]     CALL            3 4 1
18      [-]     RETURN          0 1
19      [-]     RETURN          0 1
function <?:313,321> (19 instructions, 76 bytes at 0x87bdb8)
3 params, 7 slots, 0 upvalues, 0 locals, 5 constants, 0 functions
1       [-]     GETGLOBAL       3 -1    ; crypt_used_openssl
2       [-]     CALL            3 1 2
3       [-]     TEST            3 0 0
4       [-]     JMP             8       ; to 13
5       [-]     GETGLOBAL       3 -2    ; dec_file
6       [-]     MOVE            4 0
7       [-]     CALL            3 2 2
8       [-]     GETGLOBAL       4 -3    ; dump_to_file
9       [-]     MOVE            5 3
10      [-]     MOVE            6 1
11      [-]     CALL            4 3 1
12      [-]     JMP             5       ; to 18
13      [-]     GETGLOBAL       3 -4    ; wolfssl_enc_dec_file
14      [-]     MOVE            4 0
15      [-]     MOVE            5 1
16      [-]     LOADK           6 -5    ; 0
17      [-]     CALL            3 4 1
18      [-]     RETURN          0 1
19      [-]     RETURN          0 1

The Lua bytecode is similar to Java bytecode. In order to make sense of all this, we need to learn how to decompile this back to a Lua script. However, to save us time, here is the decompiled Lua bytecode:

require("luci.sys")
require("nixio")
require("luci.util")
module("luci.model.crypto", package.seeall)

local openssl_aes_256_cbc = "aes-256-cbc"

local enc_cmd_zlib_openssl = "openssl zlib -e %s | openssl " .. openssl_aes_256_cbc .. " -e %s"
local dec_cmd_zlib_openssl = "openssl " .. openssl_aes_256_cbc .. " -d %s %s | openssl zlib -d"
local enc_cmd_openssl = "openssl " .. openssl_aes_256_cbc .. " -e %s %s"
local dec_cmd_openssl = "openssl " .. openssl_aes_256_cbc .. " -d %s %s"

local in_file_arg = "-in %q"
local key_arg = "-k %q"
local key_file_arg = "-kfile /etc/secretkey"

-- Duplicate commands for encryption/decryption (likely for different modes or fallback)
local enc_cmd_zlib_openssl_dup = "openssl zlib -e %s | openssl " .. openssl_aes_256_cbc .. " -e %s"
local dec_cmd_zlib_openssl_dup = "openssl " .. openssl_aes_256_cbc .. " -d %s %s | openssl zlib -d"
local enc_cmd_openssl_dup = "openssl " .. openssl_aes_256_cbc .. " -e %s %s"
local dec_cmd_openssl_dup = "openssl " .. openssl_aes_256_cbc .. " -d %s %s"

-- AES key and IV
local aes_key = "2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836"
local aes_iv = "360028C9064242F81074F4C127D299F6"
local key_iv_args = "-K " .. aes_key .. " -iv " .. aes_iv

-- Function definitions
local function func_0_at_87a840(arg_0, arg_1, arg_2, arg_3)
    local v5, v4
    if arg_3 then
        if arg_2 then
            v5 = nil -- UPVAL 0
            v4 = v5 and arg_1 or arg_2
        else
            v4 = nil -- UPVAL 1
        end
    else
        if arg_2 then
            v5 = nil -- UPVAL 2
            v4 = v5 and arg_1 or arg_2
        else
            v4 = nil -- UPVAL 3
        end
    end

    local new_table = {0, 0}
    local v6, v7

    if arg_0 then
        v6 = nil -- UPVAL 4
        v6 = v6 % arg_0
        if v6 then
            -- JMP to 24
        end
    end
    v6 = ""

    if arg_1 then
        v7 = nil -- UPVAL 5
        v7 = v7 % arg_1
        if v7 then
            -- JMP to 31
        end
    end
    v7 = nil -- UPVAL 6

    new_table[1] = v6
    new_table[2] = v7

    return v4 % new_table[1] % new_table[2]
end

local function func_1_at_87a920(arg_0, arg_1, arg_2)
    local v4, v3
    if arg_2 then
        if arg_1 then
            v4 = nil -- UPVAL 0
            v3 = v4 and arg_0 or arg_1
        else
            v3 = nil -- UPVAL 1
        end
    else
        if arg_1 then
            v4 = nil -- UPVAL 2
            v3 = v4 and arg_0 or arg_1
        else
            v3 = nil -- UPVAL 3
        end
    end

    local new_table = {0, 0}
    local v5, v6

    if arg_0 then
        v5 = nil -- UPVAL 4
        v5 = v5 % arg_0
        if v5 then
            -- JMP to 24
        end
    end
    v5 = ""

    v6 = nil -- UPVAL 5

    new_table[1] = v5
    new_table[2] = v6

    return v3 % new_table[1] % new_table[2]
end

crypt_used_openssl = function()
    local nixio_fs = nixio.fs
    local stat_result = nixio_fs.stat("/usr/bin/openssl")
    if stat_result then
        return true
    else
        return false
    end
end

enc_file = function(arg_0, arg_1)
    if type(arg_0) == "string" and #arg_0 == 0 then
        return nil, nil
    end

    local v2 = func_0_at_87a840(arg_0, arg_1, true)
    local nixio_ltn12_popen = nixio.ltn12_popen
    return nixio_ltn12_popen(v2)
end

wolfssl_enc_dec_file = function(arg_0, arg_1, arg_2, arg_3, arg_4, arg_5)
    if (type(arg_0) == "string" and #arg_0 == 0) or (type(arg_1) == "string" and #arg_1 == 0) then
        return false
    end

    if arg_2 == nil or arg_3 == nil then
        return false
    end

    if type(arg_3) ~= "string" or (#arg_3 ~= 32 and #arg_3 ~= 48 and #arg_3 ~= 64) then
        return false
    end

    if arg_4 == nil then
        -- This part might be setting `arg_4` from an upvalue if nil
    end
    if type(arg_4) ~= "string" or #arg_4 ~= 32 then
        return false
    end

    if arg_5 == nil then
        arg_5 = 0
    end

    local luarsa = require("luarsa")
    local result = luarsa.aes_enc_file(arg_0, arg_1, arg_3, arg_4, arg_2, arg_5)

    if result ~= nil then
        local io_open = io.open(arg_1, "w")
        if io_open then
            io_open:close()
        end
    end
    return result
end

dec_file = function(arg_0, arg_1)
    if type(arg_0) == "string" and #arg_0 == 0 then
        return nil, nil
    end

    local v2 = func_0_at_87a840(arg_0, arg_1, false)
    local nixio_ltn12_popen = nixio.ltn12_popen
    return nixio_ltn12_popen(v2)
end

enc = function(arg_0, arg_1, arg_2)
    if type(arg_0) ~= "string" then
        return nil, nil, nil
    end

    local v3 = func_1_at_87a920(arg_0, arg_1, arg_2, true)
    local nixio_ltn12_popen = nixio.ltn12_popen
    return nixio_ltn12_popen(v3, arg_0)
end

dec = function(arg_0, arg_1, arg_2)
    if type(arg_0) ~= "string" then
        return nil, nil, nil
    end

    local v3 = func_1_at_87a920(arg_0, arg_1, arg_2, false)
    local nixio_ltn12_popen = nixio.ltn12_popen
    return nixio_ltn12_popen(v3, arg_0)
end

onemesh_ltn12_open = function(arg_0, arg_1)
    local r2, r3 = nixio.pipe()
    local r4, r5

    if arg_1 then
        r4, r5 = nixio.pipe()
    end

    local pid = nixio.fork()
    if pid < 0 then
        if arg_1 then
            r5:write(arg_1)
            r4:close()
            r5:close()
        end
        r2:close()
        return function(size)
            if size == nil then
                size = 2048
            end
            local data = r3:read(size)
            local status, code, reason = nixio.waitpid(pid, "nohang")

            local finished = nil -- This upvalue likely tracks if the child process has finished

            if finished then
                -- if status == "exited"
            end
            -- if code == 10

            if data then
                if #data < size then
                    -- return data
                end
            end

            if finished then
                if finished == false then
                    r3:close()
                    return nil, nil
                end
            end
            return ""
        end, r3, r2
    elseif pid == 0 then
        nixio.dup(r3, nixio.stdout)
        r2:close()
        r3:close()

        if arg_1 then
            nixio.dup(r4, nixio.stdin)
            r4:close()
            r5:close()
        end
        nixio.exec("/bin/sh", "-c", arg_0)
    end
end

onemesh_enc = function(arg_0, arg_1, arg_2)
    if type(arg_0) ~= "string" then
        return nil, nil, nil
    end

    local v3 = func_1_at_87a920(arg_0, arg_1, arg_2, true)
    return onemesh_ltn12_open(v3, arg_0)
end

onemesh_dec = function(arg_0, arg_1, arg_2)
    if type(arg_0) ~= "string" then
        return nil, nil, nil
    end

    local v3 = func_1_at_87a920(arg_0, arg_1, arg_2, false)
    return onemesh_ltn12_open(v3, arg_0)
end

wolfssl_enc_dec = function(arg_0, arg_1, arg_2, arg_3)
    if type(arg_0) ~= "string" then
        return nil, nil, nil
    end

    if arg_1 == nil or arg_2 == nil then
        return nil, nil, nil
    end

    if type(arg_2) ~= "string" or (#arg_2 ~= 32 and #arg_2 ~= 48 and #arg_2 ~= 64) then
        return nil, nil, nil
    end

    if arg_3 == nil then
        -- This part might be setting `arg_3` from an upvalue if nil
    end
    if type(arg_3) ~= "string" or #arg_3 ~= 32 then
        return nil, nil, nil
    end

    local luarsa = require("luarsa")
    if arg_1 == true then
        return luarsa.aes_enc(arg_0, arg_2, arg_3)
    else
        return luarsa.aes_dec(arg_0, arg_2, arg_3)
    end
end

dump_to_file = function(arg_0, arg_1)
    local file_handle = io.open(arg_1, "w")
    if not file_handle then
        return
    end

    local data = arg_0(0)
    while data do
        if not file_handle:write(data) then
            break
        end
        data = arg_0(0) -- Read next chunk
    end
    file_handle:close()
end

enc_file_entry = function(arg_0, arg_1, arg_2)
    local result
    if crypt_used_openssl() then
        result = enc_file(arg_0, arg_1)
    else
        result = wolfssl_enc_dec_file(arg_0, arg_1, arg_2, key_iv_args, 1) -- 1 for encryption
    end
    dump_to_file(result, arg_1)
end

dec_file_entry = function(arg_0, arg_1, arg_2)
    local result
    if crypt_used_openssl() then
        result = dec_file(arg_0, arg_1)
    else
        result = wolfssl_enc_dec_file(arg_0, arg_1, arg_2, key_iv_args, 0) -- 0 for decryption
    end
    dump_to_file(result, arg_1)
end

It looks like the encryption used is AES-256-CBC and the key and IV are statically defined. However, there are two branches in the code. If /usr/bin/openssl exist, enc_file or dec_file is used, otherwise, wolfssl_enc_dec_file or wolfssl_enc_dec_file is used. We can try both branches to see which routines are actually being used by the firmware.

To do this, let’s download a backup configuration from our TPLink’s web portal. Then let’s try to decrypt the backup configuration with openssl.

$ IV=360028C9064242F81074F4C127D299F6
$ KEY=2EB38F7EC41D4B8E1422805BCD5F740BC3B95BE163E39D67579EB344427F7836
$ openssl enc -d -aes-256-cbc -in ArcherAX18*.bin -out ArcherAX18*.decrypted.bin -K $KEY -iv $IV
$ file ArcherAX1*.decrypted.bin 
ArcherAX18*.decrypted.bin: zlib compressed data

Success! Now, let’s decompress this file:

$ openssl zlib -d -in ArcherAX18*.decrypted.bin -out ArcherAX18*.decrypted.decompressed.bin
$ file ArcherAX18*.decrypted.decompressed.bin
ArcherAX18v*.decrypted.decompressed.bin: OpenPGP Public Key

Now, why would the decompressed file be an OpenPGP Public Key? To investigate further on this, I ran binwalk on the decompressed file.

$ binwalk ArcherAX18*.decrypted.decompressed.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
16            0x10            POSIX tar archive (GNU)

The output revealed that inside this file, there is a POSIX tar archive which starts at 16 (0x10) byte offset. The first 16 (0x10) byte is a header-like data. We can save this header in a file with the command:

$ dd if=ArcherAX18*.tar of=fw_header.bin bs=1 count=16

With this command, we are creating a file named fw_header.bin. When we get to the part of restoring the backup configuration, we will use this file to prepend to the tar file.

We can extract the POSIX tar achive with binwalk:

$ binwalk -e ArcherAX18*.decrypted.decompressed.bin
$ ls -l _ArcherAX18*.decrypted.decompressed.bin.extracted 
total 36
-rw-rw-r-- 1 kali kali 16896 Sep  4 04:12 10.tar
----r--r-x 1 kali kali     0 Nov 26  2023 ori-backup-ap-config.bin
----r--r-x 1 kali kali     0 Nov 26  2023 ori-backup-certificate.bin
----r--r-x 1 kali kali     0 Nov 26  2023 ori-backup-router-config.bin
----r--r-x 1 kali kali 13088 Nov 26  2023 ori-backup-user-config.bin
$ file _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin
_ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin: regular file, no read permission

At this point, it looks like the ori-backup-user-config.bin is also encrypted. To decrypt the file, we use the same openssl command with the key and IV but first, we need to adjust the file permissions

$ chmod 660 _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin
$ openssl enc -d -aes-256-cbc -in _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin -out _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin.decrypted -K $KEY -iv $IV
$ file _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin.decrypted 
_ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin.decrypted: zlib compressed data

The decryption is successful! Now, we can uncompress the file.

$ openssl zlib -d -in _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin.decrypted -out _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.xml
$ $ file _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.xml
_ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.xml: XML 1.0 document, ASCII text

Finally, we get the unencrypted backup configuration!

$ cat _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.xml
<?xml version="1.0" encoding="utf-8"?>
<config>
<access_control>
<global name="settings">
<access_mode>black</access_mode>
<enable>off</enable>
<guest_enable>on</guest_enable>
</global>
</access_control>
<accountmgnt>
<rsa name="keys">
<e>010001</e>
<d>[snipped...]</d>
<n>[snipped...]</n>
</rsa>
<meshrsa name="meshkeys">
<e>010001</e>
<d>[snipped...]</d>
<n>[snipped...]</n>
</meshrsa>
<account name="admin">
<username>admin</username>
<password>[snipped...]</password>
</account>
</accountmgnt>
<administration>
[snipped...]

At this point, we are free to modify the backup configuration to our heart’s content. When we are ready to upload the backup configuration, we do the same steps, but in reverse:

  1. Encrypt each *.xml file but leave empty *.xml files as is.
    $ openssl enc -aes-256-cbc -in _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.xml -out _ArcherAX18*.decrypted.decompressed.bin.extracted/ori-backup-user-config.bin -K $KEY -iv $IV
    
  2. Create a tar containing the *.xml files:
    $ tar -cf config.tar ./ori-backup-ap-config.bin ./ori-backup-certificate.bin ./ori-backup-router-config.bin ./ori-backup-user-config.bin
    
  3. Prepend the fw_header.bin:
    $ cat fw_header.bin config.tar > ArcherAX18*.updated.tar
    
  4. Encrypt the resulting tar:
    $ openssl enc -aes-256-cbc -in ArcherAX18*.updated.tar -out ArcherAX18*.updated.bin -K $KEY -iv $IV
    
  5. Restore the resulting *.bin file using TPLink’s web portal.

Automating Encryption and Decryption

To automate all the steps needed, I created a small Python script to execute the exact same steps needed to encrypt/decrypt the Backup Configuration: GitHub.

The workflow should be like this:

  1. Perform a backup configuration
  2. Run the Python script to decrypt the backup
  3. Modify the xml configuration.
  4. Encrypt the backup
  5. Restore the backup using the TPLink’s web portal

Future Work

I just realized that I can also download and upload the backup configuration automatically and I might add that if and when I have time.