Decrypt configuration files like exactly how Huawei ONT does

July 28, 2021 // echel0n

Decrypt configuration files like exactly how Huawei ONT does,

Huawei's aescrypt2 emulation with Qiling Framework

Hello guys! In this blog i will show you how to decrypt Huawei ONT configuration files like exactly how the device handle operations of encryption/decryption. TLDR version is at the end.

aescrypt2

The binary is well-known and already reverse-engineered to death. It has also an alternative version to encrypt/decrypt files (ex: hw_ctree.xml) on your computer


palmerc's AESCrypt2 tool

I wanted more natural solution because the device's filesystem was there and the original aescrypt2 also was there then i thought it could be great exercise for emulating with qiling framework. By the way, while i was working on this binary, I did my first contribution to an open-source project, i've implemented 2 syscalls. (go me!) All work done in Huawei OptiXstar HG8245X6. I wont explain the steps of obtaining the needed files. It is a different story.

Lazy Man's Work

Actually, i did not analyze the binary file with r2 or cutter for even once. I just checked how the device calls that binary and found some examples like below.

  1. $ grep -R aescrypt2
  2. .
  3. .
  4. .
  5. bin/restorehwmode.sh: $var_pack_temp_dir/aescrypt2 0 $2 $2"_tmp"
  6. bin/restorehwmode.sh: $var_pack_temp_dir/aescrypt2 1 $1 $1"_tmp"
  7. .
  8. .

Then I checked the lines in scripts that uses aescrypt2.

  1. HW_Script_Encrypt()
  2. {
  3. if [ $1 -eq 1 ]
  4. then
  5. gzip -f $2
  6. mv $2".gz" $2
  7. $var_pack_temp_dir/aescrypt2 0 $2 $2"_tmp"
  8. fi
  9. }
  1. ...
  2. ...
  3. ...
  4. # decrypt var_ctree
  5. $var_pack_temp_dir/aescrypt2 1 $1 $1"_tmp"
  6. if [ 0 -ne $? ]
  7. then
  8. echo $1" Is not Encrypted!" >> $var_upgrade_log
  9. else
  10. echo $1" Is Encrypted!" >> $var_upgrade_log
  11. varIsXmlEncrypted=1
  12. mv $1 $1".gz"
  13. gunzip -f $1".gz"
  14. fi
  15. ...
  16. ...
  17. ...

I understood how encryption and decryption operations work quickly, it explains itself. When encryption is needed, first it will be zipped then it will be encrypted. Cha cha real smooth Huawei! They answered the legendary job interview "Is it better to encrypt before compression or vice versa?" question. It may work but only when the keys are not lying around and not plaintext. (The key in-use is in /etc/wap/aes_string) (It's okay tho)

The Action

My strategy was a mess. Firstly, I just copied all filesystem including /dev and /proc directories to make it feel like in home. Literally, i didn't do a proper static analysis of it even no readelf or objdump. I just fired Qiling() class with suitable arguments.

  1. def prepare() -> Qiling:
  2. #
  3. # decrypt var_ctree
  4. # var_pack_temp_dir=/bin/
  5. # var_default_ctree=/mnt/jffs2/customize_xml/hw_default_ctree.xml
  6. # var_temp_ctree=/mnt/jffs2/customize_xml/hw_default_ctree_tem.xml
  7. # $var_pack_temp_dir/aescrypt2 1 $var_default_ctree $var_temp_ctree
  8. cur_dir = os.path.abspath(".")
  9. ql = Qiling(
  10. [
  11. cur_dir + "/rootfs/bin/aescrypt2",
  12. "1", # decrypt flag for encryption set to 0
  13. "/mnt/jffs2/hw_ctree.xml", # encrypted file
  14. # i couldnt understand what second arg's purpose because
  15. # aescrypt2 renames this field to original file
  16. # it is like a dummy file imo.
  17. "/tmp/hw_ctree.gz",
  18. ],
  19. cur_dir + "/rootfs/",
  20. verbose=QL_VERBOSE.DEBUG,
  21. console=True,
  22. )
  23. return ql

I literally was waiting to qiling could do the rest magically. I was wrong and started to pay attention to details. The first problem was readv syscall was not implemented, i worked to implement it properly. It was not that hard, checked the man pages to know how it operates.

  1. def ql_syscall_readv(ql, fd, vec, vlen, *args, **kw):
  2. # it is like read() but reads into more than one buffer.
  3. # if you use dev branch while using qiling you already have it.
  4. regreturn = 0
  5. size_t_len = ql.pointersize
  6. iov = ql.mem.read(vec, vlen * size_t_len * 2)
  7. ql.log.debug("readv() CONTENT:")
  8. for i in range(vlen):
  9. addr = ql.unpack(
  10. iov[i * size_t_len * 2 : i * size_t_len * 2 + size_t_len]
  11. )
  12. l = ql.unpack(
  13. iov[
  14. i * size_t_len * 2
  15. + size_t_len : i * size_t_len * 2
  16. + size_t_len * 2
  17. ]
  18. )
  19. regreturn += l
  20. if hasattr(ql.os.fd[fd], "read"):
  21. data = ql.os.fd[fd].read(l)
  22. ql.log.debug(data)
  23. ql.mem.write(addr, data)
  24. return regreturn

I wish I could say i've set this syscall then it worked but another problem was occured after this one.

  1. [+] 0x04814230: readv(fd = 0x6, vec = 0x7ff3bb04, vlen = 0x2) = 0xdff
  2. [+] [+] Received Interupt: 2 Hooked Interupt: 2
  3. [x]
  4. Traceback (most recent call last):
  5. File "/usr/lib/python3.9/site-packages/qiling/os/posix/posix.py", line 184, in load_syscall
  6. retval = syscall_hook(self.ql, *params)
  7. File "/usr/lib/python3.9/site-packages/qiling/os/posix/syscall/unistd.py", line 159, in ql_syscall__llseek
  8. ret = ql.os.fd[fd].lseek(offset, origin)
  9. File "/usr/lib/python3.9/site-packages/qiling/os/filestruct.py", line 46, in lseek
  10. return os.lseek(self.__fd, lseek_offset, lseek_origin)
  11. OverflowError: Python int too large to convert to C long
  12. [=] Syscall ERROR: ql_syscall__llseek DEBUG: Python int too large to convert to C long

As every professional does, immediately called print function with this value in that function. I found out this value actually was a negative number but the function assumes it is unsigned. Then i've implemented my _llseek syscall like this, i don't know if it is a proper patch but it worked for me.

  1. def ql_syscall__llseek(ql, fd, offset_high, offset_low, result, whence, *args, **kw):
  2. # _llseek negative seek bug fix
  3. offset_high = int.from_bytes(
  4. offset_high.to_bytes(ql.pointersize, "little"), "little", signed=True
  5. )
  6. offset_low = int.from_bytes(
  7. offset_low.to_bytes(ql.pointersize, "little"), "little", signed=True
  8. )
  9. offset = offset_high << 32 | offset_low
  10. origin = whence
  11. regreturn = 0
  12. try:
  13. ret = ql.os.fd[fd].lseek(offset, origin)
  14. except OSError:
  15. regreturn = -1
  16. if regreturn == 0:
  17. ql.mem.write(result, ql.pack64(ret))
  18. ql.log.debug(
  19. "_llseek(%d, 0x%x, 0x%x, 0x%x) = %d"
  20. % (fd, offset_high, offset_low, origin, regreturn)
  21. )
  22. return regreturn

After patching this call succesfully, I realized aescrypt2 reads first 8 bytes of given file, first 4 bytes are -if I understood correctly- file encryption type or smt like that and other 4 bytes are for checksum.

(I didn't analyze it, if something messed up while decrypting it, suprisingly it throws a checksum error, lol.)

Then i noticed that rename() syscall also was not implemented (it's implemented in dev branch now) then wrote it down too but it was not necessary because given file is already decrypted at that time.


The Euphoria


When it exited with exit code 0, i felt that. I quickly tried to gunzip it.

  1. $ mv hw_ctree.xml hw_ctree.gz
  2. $ gunzip -f hw_ctree.gz
  3. $ # when command is executed succesfully, i slammed my fist on the table
  4. $ cat hw_ctree



Too Long Didn't Read

I've done something unnecessary emulation while having aescrypt2 alternative tool and felt awesome and decided to brag about it.

Note: I created the needed file list from open call. In case of missing something, if you are able to pull all files(shared libraries, /proc/wap_proc and /dev included), get all of them. kmc_store_* files are not included in repository because i didnt analyze it, i dont know yet if it is unique file or not. kmc_store_* files are vital. if it is blank or random it will check another directories for backup. If it could not find any, it will throw this error;

  1. hw_ssp_ctool.c:934:HMAC check failed: wrong key, or file corrupted.
If you like to do this way too, you need to get these file from the device to emulate aescrypt2;
  1. Succesful decryption operation will open these files.
  2. /etc/ld-musl-arm.path,
  3. /lib/libhw_ssp_basic.so,
  4. /lib/libclang_rt.builtins_s.so,
  5. /lib/libunwind_s.so.1,
  6. /lib/librtos_musl_extend.so,
  7. /dev/hlp,
  8. /proc/wap_proc/proc_dbg,
  9. /lib/libpolarssl.so,
  10. /proc/wap_proc/wap_msg_group
  11. /mnt/jffs2/kmc_store_A3,
  12. /mnt/jffs2/kmc_store_A3,
  13. /mnt/jffs2/kmc_store_B3,
  14. /mnt/jffs2/kmc_store_B3,
  15. /mnt/jffs2/kmc_store_A,
  16. /mnt/jffs2/kmc_store_B,
  17. /var/ksf_check_done,

Although it was not necessary, i am happy to being able to do this way, it feels like, it is a more elegant way to accomplish this.

Full script is available in my having-fun-with-qiling repo.


Here


Bonus

I also saw another AES key in /bin/clid binary but i dont know which part of it is using the key and how. I am leaving it here. If you already know how and where this key is used please contact me on twitter @echel0n_1881

  1. Df7!ui%s9(lmV1L8

Thank you for reading my blog, have a nice day absolute legends!