HTB Headache

Kaddate |

Difficulty : Medium
Category : Reversing


Let's start by inspecting the given ELF

  Headache readelf --wide --use-dynamic -a ./headache
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1190
  Start of program headers:          64 (bytes into file)
  Start of section headers:          18856 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         11
  Size of section headers:           64 (bytes)
  Number of section headers:         4
  Section header string table index: 3

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .data             PROGBITS        0000000000001000 001000 001695 00  WA  0   0  4
  [ 2] .text             PROGBITS        0000000000000000 000000 000a60 00  AX  0   0  4
  [ 3] .shstrtab         STRTAB          0000000000000000 004aa8 000017 00      0   0  4

We are facing a stripped binary where even the optionnal sections have been stripped. Since this is the second Binary with this kind of obfuscation I am facing, I will try to reconstruct them using the lief framework by analyzing the memory segments. My objective it to recreate the .plt and .plt.got so that Ghidra can resolve external calls, making the static analysis way easier.

Reconstructing sections :

here is the script, basically I calculate the size of .plt and .plt.got and deduce their offset in the binary.

import lief
from lief import ELF
import inspect

def print_object_fields(o):
    for name, member in inspect.getmembers(o):
        if name.startswith('_'):
            continue
        print(f"{name!s:30} = {member!r}")


def va_to_offset(va: int) -> int:
    for segment in elf.segments:
        start = segment.virtual_address
        end = start + segment.virtual_size
        if start <= va < end:
            return segment.file_offset + (va - start)
    raise ValueError(f"VA {hex(va)} not found in any LOAD segment.")

def new_plt(size, va):
    plt = ELF.Section(".plt")
    plt.type = ELF.Section.TYPE.PROGBITS
    plt.flags = ELF.Section.FLAGS.ALLOC | ELF.Section.FLAGS.EXECINSTR
    plt.alignment = 0x10
    plt.size = int(size)
    plt.virtual_address = va
    plt.offset = va_to_offset(va)
    return plt

def new_got_plt(size, va):
    gotplt = ELF.Section(".got.plt")
    gotplt.type = ELF.Section.TYPE.PROGBITS
    gotplt.flags = ELF.Section.FLAGS.ALLOC | ELF.Section.FLAGS.WRITE
    gotplt.alignment = POINTER_SIZE
    gotplt.size = int(size)
    gotplt.virtual_address = va  # use correct VA
    return gotplt

if __name__ == "__main__":
    elf: lief.ELF.Binary = lief.ELF.parse("./headache")
    print(elf)

    global POINTER_SIZE
    if elf.header.identity_class == lief.ELF.Header.CLASS.ELF64:
        POINTER_SIZE = 8
    else:
        POINTER_SIZE = 4

    dynamic_dict = {entry.tag.name: entry.value for entry in elf.dynamic_entries}
    number_of_stubs = dynamic_dict['PLTRELSZ'] / dynamic_dict['RELAENT']

    size_of_plt = 16 * (number_of_stubs +1)
    size_of_got_plt = POINTER_SIZE * (number_of_stubs + 3)

    elf.add(new_plt(size_of_plt, dynamic_dict['INIT'] + 0x20), loaded=True)
    elf.add(new_got_plt(size_of_got_plt, dynamic_dict['PLTGOT']), loaded=True)

    builder = lief.ELF.Builder(elf)
    builder.build()
    builder.write("new.elf")

Let's now look at the new ELF :

(qilingenv)   Headache readelf --wide -S ./new.elf
There are 6 section headers, starting at offset 0x6020:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .data             PROGBITS        0000000000002000 002000 001695 00  WA  0   0  4
  [ 2] .text             PROGBITS        0000000000000000 000000 000a60 00  AX  0   0  4
  [ 3] .plt              PROGBITS        000000000000e000 006000 000000 00  AX  0   0 16
  [ 4] .got.plt          PROGBITS        0000000000016000 006000 000000 00  WA  0   0  8
  [ 5] .shstrtab         STRTAB          0000000000000000 006000 000020 00      0   0  0

Ghidra FTW :

Great ! relocations Symbols are resolved in Ghidra :

ab0720e73c4bdd58dd348dea38166924.png

Looking at the old binary, we can see that ghidra could not resolve them :

628307117ba27250e468da8df494b123.png

The main function looks pretty weird, it is probably encrypted.

fff5f580e9c4b552d26beb44a39e2ecd.png

So let's have a look at the _init() function :

c12fbc4e9f977e8691229665776da2a9.png

We indeed have a custom function called by _init() and it has some xrefs to the main() function.

This function does a syscall ptrace(TRACE_ME) and if the program is not in a debug state, it seems to decrpyt main with a given key, and then calls it.

Since I am lazy, I'll just get the decrypted main using Qiling and a breakpoint :

def print_size_of_decrypted_memory(ql: Qiling) -> None:
    global decrypted_size
    decrypted_size = ql.arch.regs.rsi
    ql.log.warning(f"number of byte decrypted : {decrypted_size}")

def read_decrypted_code(ql: Qiling) -> None:
    buf = ql.mem.read(image_base + 0x1faf, decrypted_size)
    ql.log.warning(buf.hex())

ql = Qiling(["./headache"], "/", verbose=QL_VERBOSE.OFF)
global image_base
image_base = ql.loader.images[0].base
ql.hook_address(print_size_of_decrypted_memory, image_base + 0x1eca)
ql.hook_address(read_decrypted_code, image_base + 0x134b)

Once done, I just put it into Ghidra.

The real main

fae4170f59db8b08339f937c1fdd6dec.png

Looking at the main, we can see that it does another ptrace(TRACE_ME) and does some annoying sleep().

It then asks the user for an input and test with a very complicated algorythm :

cf986af353b9be9003afba2e8c5e6698.png

But we can see that even tho the loop does a lot of fancy transformations, in the end, the way to check if the password is correct is to check if the direct user input is correct. Just setting a breakpoint to this CMP and getting the content of EDX should be enough.

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.log import QL_VERBOSE
from qiling.os.const import POINTER

def my_syscall_nanosleep(ql: Qiling):
    ql.log.warning("Hooked syscall clock_nanosleep")
    return

def my_syscall_ptrace(ql: Qiling, request: int):
    if request == 0:
        ql.log.warning("Hooked syscall ptrace(TRACE_ME)")
        return 0

# ---

def print_size_of_decrypted_memory(ql: Qiling) -> None:
    global decrypted_size
    decrypted_size = ql.arch.regs.rsi
    ql.log.warning(f"number of byte decrypted : {decrypted_size}")

def read_decrypted_code(ql: Qiling) -> None:
    buf = ql.mem.read(image_base + 0x1faf, decrypted_size)
    ql.log.warning(buf.hex())

# ---

flag = ""

def get_flag(ql: Qiling) -> None:
    ql.os.set_api("fgets", my_fgets, QL_INTERCEPT.CALL)
    loop_condition: int = image_base + 0x2646
    ql.hook_address(address=loop_condition, callback=construct_flag)

def construct_flag(ql: Qiling) -> None:
    global flag
    flag += chr(ql.arch.regs.edx)
    ql.arch.regs.edx = ql.arch.regs.eax

def my_fgets(ql: Qiling):
    params = ql.os.resolve_fcall_params({'s': POINTER})
    s = params['s']
    input_data = b"A" * 20 + b"\n"
    ql.mem.write(s, input_data + b"\x00")  # Null-terminate
    return s

# ---

def my_sandbox(binary, rootfs):
    ql = Qiling(binary, rootfs, verbose=QL_VERBOSE.OFF)

    # patch annoying syscalls
    ql.os.set_syscall('clock_nanosleep', my_syscall_nanosleep)
    ql.os.set_syscall('ptrace', my_syscall_ptrace)

    ql.hook_address(print_size_of_decrypted_memory, image_base + 0x1eca)
    ql.hook_address(read_decrypted_code, image_base + 0x134b)
    return ql

if __name__ == "__main__":
    ql = my_sandbox(["./headache"], "/")
    global image_base
    image_base = ql.loader.images[0].base

    get_flag(ql)

    ql.run()
    ql.log.warning(f"flag = {flag}")

And we have the flag :)

➜  Headache python3 test_qiling.py 
[!]     0x7ffff7de7b9d: syscall ql_syscall_rseq number = 0x14e(334) not implemented
[!]     Hooked syscall ptrace(TRACE_ME)
[!]     number of byte decrypted : 1749
[!]     554889e54881ecd000000089bd3cffffff4889b530ffffffb900000000ba01000000be00000000bf00000000b800000000e80bf1ffff8945f4488d3d3a100000b800000000e887f0ffff488b05503100004889c7e8c8f0ffffc745fc00000000eb2cbf01000000b800000000e850f1ffffbf2e000000e816f0ffff488b051f3100004889c7e897f0ffff8345fc01837dfc047ecebf0a000000e8f3efffff488d3de20f0000b800000000e822f0ffff488b15fb300000488d45c0be1e0000004889c7e82af0ffff488d45c0488d35c50f00004889c7e8a7f0ffffc645a048c645a154c645a242c645a37bc645a477c645a530c645a677c645a75fc645a874c645a968c645aa34c645ab74c645ac73c645ad5fc645ae63c645af30c645b030c645b130c645b26cc645b37d488d45c04889c7e87befffff4883f8147416488d3d560f0000e859efffffbf01000000e84ff0ffff48b8534652436533526f48ba4d584e664d584e66488945804889558848b86447677a58325a7348894590c745984e476439c6459c00488d4580ba0a000000be0a0000004889c7e896f6ffff48b82935231a1509551548ba3e1655123e09551348898560ffffff48899568ffffff66c78570ffffff051cc68572ffffff00488d8560ffffff4889c7e8b5f3ffff8b853cffffff83c0026bc06483f8647543488d55a0488d45c04889d64889c7e8ffeeffff85c07516488d3d9a0e0000e88feeffffbf00000000e885efffff488d3d760e0000e879eeffffbf01000000e86fefffff8b853cffffff8945f08b853cffffff83c0026bc06483f864751248c745e800000000488b45e8c70053080000c68540ffffff07c68541fffffffec68542ffffff8dc68543ffffff0dc68544ffffffd2c68545ffffffe4c68546fffffffec68547ffffff08c68548ffffffefc68549ffffff57c6854affffff47c6854bffffffdbc6854cffffff4dc6854dffffff96c6854effffff2fc6854fffffff3bc68550ffffffa8c68551ffffffbbc68552ffffff70c68553ffffff11c68554ffffffadc745f800000000e9b80300008b45f80fb6840540ffffff8845e70fb645e7c0e80589c20fb645e7c1e00309d08845e7f655e78045e72f8075e7260fb645e7c0e80589c20fb645e7c1e00309d08845e7806de702f65de70fb645e7c0e80289c20fb645e7c1e00609d08845e7f655e78045e747f655e78075e743806de77b0fb645e7c0e80589c20fb645e7c1e00309d08845e7f655e78045e7510fb645e7c0e80389c20fb645e7c1e00509d08845e78045e7740fb645e7c0e80289c20fb645e7c1e00609d08845e7806de7260fb645e7c0e80689c20fb645e7c1e00209d08845e78b45f80045e78b45f83045e7f65de78b45f83045e78045e7790fb645e7c0e80789c20fb645e701c009d08845e7806de7498b45f83045e7806de775f655e78045e765f65de78045e708f655e7f65de78b45f83045e78b45f82845e7f655e78045e7618075e7818b45f82845e78b45f83045e78b45f80045e7f655e78b45f80045e7f65de78b45f80045e7f655e7806de705f65de78b45f83045e7f65de78075e7158b45f80045e7f65de78b45f82845e78075e7228045e772f655e7f65de7806de7668075e7618b45f82845e78075e72c8b45f80045e78075e701806de74df655e78075e73cf65de78075e719f655e78045e7710fb645e7c0e80789c20fb645e701c009d08845e78b45f80045e78075e7400fb645e7c0e80789c20fb645e701c009d08845e78045e75c8075e76d0fb645e7c0e80689c20fb645e7c1e00209d08845e7f65de78b45f83045e78b45f80045e70fb645e7c0e80589c20fb645e7c1e00309d08845e7806de71bf655e7f65de78b45f82845e7f655e78b45f80045e7f655e78075e737806de716f65de78b45f82845e7f65de78b45f83045e78045e7638075e7d9f655e78075e73ff655e78b45f80045e78075e7398b45f82845e7f655e78b45f82845e7f65de7f655e7806de7480fb645e7d0e889c20fb645e7c1e00709d08845e78b45f83045e7f65de78b45f83045e78b45f80045e7f655e78b45f80045e78075e7588045e7630fb645e7c0e80289c20fb645e7c1e00609d08845e7f65de78045e70af655e78b45f82845e7f655e70fb645e7c0e80389c20fb645e7c1e00509d08845e78b45f83045e78b45f80045e7f65de7806de778f655e78075e759f65de7f655e78045e72af65de78b45f80045e78075e715806de72ff655e78045e71e8075e7178b45f82845e7f655e7806de71e8b45f83045e70fb645e7c0e80689c20fb645e7c1e00209d08845e7806de746f65de70fb655e78b45f80fb64405c00fbec039c27416488d3df7090000e8fae9ffffbf01000000e8f0eaffff8345f801837df8140f863efcffff488d3de1090000e8d6e9ffffbf00000000e8cceaffff
[!]     Hooked syscall ptrace(TRACE_ME)
Initialising[!]     Hooked syscall clock_nanosleep
.[!]    Hooked syscall clock_nanosleep
.[!]    Hooked syscall clock_nanosleep
.[!]    Hooked syscall clock_nanosleep
.[!]    Hooked syscall clock_nanosleep
.
Enter the key: Login success!
[!]     flag = HTB{l4yl3_w4s_h3r3!}

References

  • https://lief.re//doc/latest/installation.html
  • https://docs.angr.io/en/latest/index.html
  • https://docs.qiling.io/en/latest/