HTB Headache
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 :

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

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

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

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

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 :

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/