Linux/OpenWrt on the Alcatel HH40V

I needed a cheap LTE router for some quick tests and bought a used Alcatel HH40V for cheap. It's based on a QCA9531 SoC with 128MiB RAM and 16MiB flash and it's running an old, customized version of OpenWrt on this SoC. The LTE module is based on a Qualcomm module and is running its own, customized Android stack.

Getting access to the LTE module is documented here:

However, getting access to the SoC is neither documented nor easily possible - up to now.

Getting UART access

There are two pads for UART on the PCB - orange is Rx, green is Tx and blue is GND (that's not a pad):

UART on the PCB

The serial console is then available with 115200 baud and no password.

Getting SSH access

Getting SSH access is required in order to install OpenWrt without having to solder to the pads. I poked around in the filesystem and on the Wiki and found out that it is possible to download a configuration backup - unfortunately, it is encrypted. Someone in the Wiki mentioned it's based on OpenSSL, so I searched for OpenSSL in the file core_app, one of the biggest applications of the OEM firmware. It contained the hardcoded password for the for the backup file - yeah!

Long story short, with the information in this core_app, I could come up with a Python script that modifies the backup in a way that it enabled the Dropbear SSH server upon startup.

This is the script (also available at, run it as configure.bin. Then, upload the modified backup file (configure.bin_mod.bin) to your router, reboot and you have SSH access. Credentials: User “root”, Password “oelinux123”.

  1. #!/usr/bin/env python
  3. import os
  4. import sys
  5. import subprocess
  6. import tempfile
  7. import struct
  8. import shutil
  9. import hashlib
  11. class SSHEnabler(object):
  12. def __init__(self, filepath, directory):
  13. self.openssl = None
  14. self.filepath = filepath
  15. = directory
  16. self.check_openssl()
  18. def decrypt_file(self):
  19. if not os.path.exists(self.filepath):
  20. print(f"Input file does not exist: {self.filepath}")
  21. return False
  22. ret =[self.openssl, "aes-128-cbc", "-d", "-k", "oacgnahsnauyilil", "-base64", "-in", self.filepath, "-out", + os.path.sep + "decrypted.tar.gz", "-md", "md5"])
  23. if ret.returncode != 0:
  24. print("Error decrypting file")
  25. return False
  26. ret =["tar", "zxf", + os.path.sep + "decrypted.tar.gz", "-C",])
  27. if ret.returncode != 0:
  28. print("Error extracting file")
  29. return False
  30. if not os.path.exists( + os.path.sep + "configure.bin"):
  31. print("Extracted file configure.bin does not exist")
  32. return False
  33. os.remove( + os.path.sep + "decrypted.tar.gz")
  34. with open( + os.path.sep + "configure.bin", "rb") as f:
  35. head = # The first 45 bytes contain ALCATEL BACKUP HEAD, the filename of the extracted file and the lenght of it
  36. if head[0:24] != b"ALCATEL BACKUP FILE HEAD":
  37. print("Head does not start with ALCATEL BACKUP FILE HEAD")
  38. return False
  39. length, = struct.unpack(">h", head[43:45])
  40. ret =["dd", "if=" + os.path.sep + "configure.bin", "of=" + os.path.sep + "backup.tar.gz", "bs=1", "skip=45", "count=" + str(length)])
  41. ret =["dd", "if=" + os.path.sep + "configure.bin", "of=" + os.path.sep + "configure.bin.head", "bs=1", "count=43"])
  42. ret =["dd", "if=" + os.path.sep + "configure.bin", "of=" + os.path.sep + "configure.bin.tail", "bs=1", "skip=" + str(45 + length)])
  43. os.remove( + os.path.sep + "configure.bin")
  44. os.remove( + os.path.sep + "configure.md5")
  45. ret =["tar", "zxf", + os.path.sep + "backup.tar.gz", "-C",])
  46. if ret.returncode != 0:
  47. print("Error extracting backup.tar.gz")
  48. return False
  49. if not os.path.exists( + os.path.sep + "backup_dir" + os.path.sep + "sysconfig.tar.gz"):
  50. print("sysconfig.tar.gz in backup file not found")
  51. return False
  52. os.remove( + os.path.sep + "backup.tar.gz")
  53. ret =["tar", "zxf", + os.path.sep + "backup_dir" + os.path.sep + "sysconfig.tar.gz", "-C", + os.path.sep + "backup_dir"])
  54. if ret.returncode != 0:
  55. print("Error extracting sysconfig.tar.gz")
  56. return False
  57. if not os.path.exists( + os.path.sep + "backup_dir" + os.path.sep + "etc" + os.path.sep + "rc.local"):
  58. print("rc.local not found")
  59. return False
  60. os.remove( + os.path.sep + "backup_dir" + os.path.sep + "etc" + os.path.sep + "rc.local")
  61. with open( + os.path.sep + "backup_dir" + os.path.sep + "etc" + os.path.sep + "rc.local", "w") as f:
  62. f.write("# Put your custom commands here that should be executed once\n")
  63. f.write("# the system init finished. By default this file does nothing.\n")
  64. f.write("\n")
  65. f.write("/etc/init.d/dropbear start\n")
  66. f.write("\n")
  67. f.write("exit 0\n")
  68. os.remove( + os.path.sep + "backup_dir" + os.path.sep + "sysconfig.tar.gz")
  69. ret =["tar", "zcf", + os.path.sep + "backup_dir" + os.path.sep + "sysconfig.tar.gz", "-C", + os.path.sep + "backup_dir", "etc"])
  70. if ret.returncode != 0:
  71. print("Error creating sysconfig.tar.gz")
  72. return False
  73. shutil.rmtree( + os.path.sep + "backup_dir" + os.path.sep + "etc")
  74. ret =["tar", "zcf", + os.path.sep + "backup.tar.gz", "-C",, "backup_dir"])
  75. if ret.returncode != 0:
  76. print("Error creating backup.tar.gz")
  77. return False
  78. shutil.rmtree( + os.path.sep + "backup_dir")
  79. size = os.path.getsize( + os.path.sep + "backup.tar.gz")
  80. with open( + os.path.sep + "configure.bin.head", "ab") as f:
  81. f.write(struct.pack(">h", size))
  82. with open( + os.path.sep + "configure.bin", "wb") as f:
  83. with open( + os.path.sep + "configure.bin.head", "rb") as g:
  84. f.write(
  85. with open( + os.path.sep + "backup.tar.gz", "rb") as g:
  86. f.write(
  87. with open( + os.path.sep + "configure.bin.tail", "rb") as g:
  88. f.write(
  89. os.remove( + os.path.sep + "configure.bin.head")
  90. os.remove( + os.path.sep + "configure.bin.tail")
  91. os.remove( + os.path.sep + "backup.tar.gz")
  92. md5 = hashlib.md5(open( + os.path.sep + "configure.bin", "rb").read()).hexdigest()
  93. with open( + os.path.sep + "configure.md5", "w") as f:
  94. f.write(md5 + "\n")
  95. ret =["tar", "zcf", + os.path.sep + "decrypted.tar.gz", "-C",, "configure.bin", "configure.md5"])
  96. if ret.returncode != 0:
  97. print("Error creating final .tar.gz file")
  98. return False
  99. ret =[self.openssl, "aes-128-cbc", "-e", "-k", "oacgnahsnauyilil", "-base64", "-in", + os.path.sep + "decrypted.tar.gz", "-out", self.filepath + "_mod.bin", "-md", "md5"])
  102. def which(self, program):
  103. def is_exe(fpath):
  104. return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
  106. fpath, fname = os.path.split(program)
  107. if fpath:
  108. if is_exe(program):
  109. return program
  110. else:
  111. for path in os.environ["PATH"].split(os.pathsep):
  112. path = path.strip('"')
  113. exe_file = os.path.join(path, program)
  114. if is_exe(exe_file):
  115. return exe_file
  117. return None
  119. def check_openssl(self):
  120. self.openssl = self.which("openssl")
  121. if self.openssl:
  122. ret =[self.openssl, "version"], stdout = subprocess.PIPE,
  123. universal_newlines = True)
  124. if ret.returncode == 0:
  125. version = ret.stdout.replace('\n', '')
  126. return version
  128. return False
  130. if len(sys.argv) < 2:
  131. print("Usage: configure.bin")
  132. sys.exit(1)
  134. with tempfile.TemporaryDirectory() as tempdir:
  135. enabler = SSHEnabler(sys.argv[1], tempdir)
  136. enabler.decrypt_file()

Once you have SSH access, you can proceed with installing OpenWrt.

Backup file format

Very brief overview of the file format after decryption.

0xzz length of filename (without 0 terminator - maybe it's a ushort with the previous 0x00)
String: filename
0xzz file
0xzz length (ushort)
0xzz length of filename (without 0 terminator - maybe it's a ushort with the previous 0x00)
Binary: file content
String: filename
0xzz number of bytes remaining in file (ushort)
0xf7 Lenght of file, different endian (originates from modem)
Binary: file content
Binary: Some sort of trailer, didn't attempt to decode

Installing OpenWrt

OpenWrt support to snapshot was added on 2023/04/23 with commit

Running OpenWrt on this device requires a slightly different partition layout, because the kernel partition is fixed with 1.5MB and thus too small to run a recent kernel. However, there is enough space and initial installation requires a small modification of one U-Boot variable. As long as the system boots normally, there are no problems with this approach. Installing via TFTP (recovery) restores the variable back to default, forcing the installation of a stock firmware.

NB: Although the systems is running a variation of OpenWrt, the sysupgrade process is heavily modified and cannot be used as-is to install OpenWrt.

  1. Boot the stock firmware
  2. Take a configuration backup
  3. Modify the backup to enable SSH access
  4. Restore the configuration using the modified backup
  5. Reboot and log in via SSH - User “root”, password “oelinux123”
  6. Transfer the OpenWrt -factory.bin image to the router: scp -O -o HostKeyAlgorithms=ssh-rsa -o KexAlgorithms=diffie-hellman-group1-sha1 -o UserKnownHostsFile=/dev/null openwrt-ath79-generic-alcatel_hh40v-squashfs-factory.bin root@
  7. Transfer the following script named “” to the router: scp -O -o HostKeyAlgorithms=ssh-rsa -o KexAlgorithms=diffie-hellman-group1-sha1 -o UserKnownHostsFile=/dev/null root@
  8. On the router, make the file executable: chmod +x /tmp/
  9. Run the script with the new firmware to install as parameter: /tmp/ /tmp/openwrt-ath79-generic-alcatel_hh40v-squashfs-factory.bin
  10. Wait for the router to upgrade and to boot into OpenWrt

Contents of /tmp/

if [ ! -e ${IMAGE_NAME} ]; then
  echo "Image file not found: ${IMAGE_NAME}"
  exit 1                
. /lib/upgrade/
fw_setenv bootcmd "bootm 0x9f150000"
kill_remaining TERM              
sleep 3                          
kill_remaining KILL              
run_ramfs mtd write ${IMAGE_NAME} firmware
sleep 2
reboot -f

Configuring the LTE modem

The LTE modem is an RNDIS device and works out-of-the-box if it was properly set up for the stock firmware. Limited configuration can be achieved using AT commands over USB (/dev/ttyUSB1).

Restoring Stock

To restore stock, you need a system image. Unfortunately, you can't backup without disassembling and soldering serial headers. However, a firmware upgrade can be downloaded from Look for the file Firmware/DownloadImage/uprade_hh40v_all.tar.gz. Inside, you will find the file “sysupgrade/firmware-system.bin”. This firmware-system.bin can be installed via TFTP recovery. The TFTP recovery also resets any U-Boot variables so it will boot normally.

  1. Power off the device
  2. Put firwmare-system.bin to your TFTP server
  3. Configure a static IP of
  4. Hold the buttons “Power” and “WPS” and plug back the power
  5. Keep holding for about 10 seconds, until U-Boot has booted
  6. Once transfer is completed, the system starts flashing, this takes some time
  7. Ultimately, you are back to stock
This website uses cookies. By using the website, you agree with storing cookies on your computer. Also you acknowledge that you have read and understand our Privacy Policy. If you do not agree leave the website.More information about cookies