Reverse Engineering TP-Link Archer AX18 (EU) Backup Configuration Encryption and Decryption
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:
- Encrypt each
*.xmlfile but leave empty*.xmlfiles 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 - Create a tar containing the
*.xmlfiles:$ tar -cf config.tar ./ori-backup-ap-config.bin ./ori-backup-certificate.bin ./ori-backup-router-config.bin ./ori-backup-user-config.bin - Prepend the
fw_header.bin:$ cat fw_header.bin config.tar > ArcherAX18*.updated.tar - Encrypt the resulting tar:
$ openssl enc -aes-256-cbc -in ArcherAX18*.updated.tar -out ArcherAX18*.updated.bin -K $KEY -iv $IV - Restore the resulting
*.binfile 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:
- Perform a backup configuration
- Run the Python script to decrypt the backup
- Modify the
xmlconfiguration. - Encrypt the backup
- 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.
