1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| """Rebuild run-time-built password strings from a Xojo/REALbasic native exe.
REALbasic never stores the level password as a plain string constant. It builds it one character at a time at run time:
mov ecx, 73h ; 's' push ecx call StringDBCSChr ; -> "s" ; 每个字符重复 mov/push/call 这三步 call RuntimeAddString ; append to the message
so the password is invisible to `strings`. This script finds the `mov reg,imm32 ; push reg ; call rel32` idiom, groups call sites by call target, and prints the immediate characters in address order for every target fed printable bytes. The really long runs are the built strings.
usage: ./rebuild_schr.py <exe> [min_run_len] """ import struct import sys from collections import defaultdict
def load(path): """Return (image_base, sections, overlay_file_offset, overlay_bytes).""" data = open(path, 'rb').read() e_lfanew = struct.unpack_from('<I', data, 0x3C)[0] assert data[e_lfanew:e_lfanew + 4] == b'PE\0\0' coff = e_lfanew + 4 nsec = struct.unpack_from('<H', data, coff + 2)[0] opt_size = struct.unpack_from('<H', data, coff + 16)[0] opt = coff + 20 magic = struct.unpack_from('<H', data, opt)[0] image_base = (struct.unpack_from('<I', data, opt + 28)[0] if magic == 0x10b else struct.unpack_from('<Q', data, opt + 24)[0]) sec = opt + opt_size sections = [] end = 0 for i in range(nsec): s = sec + i * 40 name = data[s:s + 8].rstrip(b'\0').decode('latin1') vsize, va, rawsize, rawptr = struct.unpack_from('<IIII', data, s + 8) sections.append((name, image_base + va, vsize, rawptr, rawsize)) end = max(end, rawptr + rawsize) return image_base, sections, end, data[end:]
def char_chains(overlay, base_va, gap=0x120): """Find `mov r32,imm ; push r32 ; call rel32` sites and chain their chars.""" by_target = defaultdict(list) i = 0 while True: i = overlay.find(b'\xe8', i) if i < 0 or i + 5 > len(overlay): break if i >= 2 and 0x50 <= overlay[i - 1] <= 0x57: r = overlay[i - 1] - 0x50 if i >= 7 and overlay[i - 6] == 0xB8 + r: val = struct.unpack_from('<I', overlay, i - 5)[0] if val <= 0xff: rel = struct.unpack_from('<i', overlay, i + 1)[0] by_target[i + 5 + rel].append((i - 6, val)) i += 1
all_sites = sorted(s for sites in by_target.values() for s in sites) runs = [] cur, prev = [], None for off, val in all_sites: if prev is not None and off - prev > gap: if cur: runs.append((0, cur)) cur = [] cur.append((off, val)) prev = off if cur: runs.append((0, cur)) return runs
def main(): path = sys.argv[1] min_len = int(sys.argv[2]) if len(sys.argv) > 2 else 8 image_base, sections, ov_off, overlay = load(path) base_va = 0x55E000 print(f'[*] {path}') print(f'[*] overlay at file 0x{ov_off:x}, {len(overlay)} bytes, mapped at 0x{base_va:x}') for target, run in sorted(char_chains(overlay, base_va), key=lambda r: -len(r[1])): if len(run) < min_len: continue s = ''.join(chr(v) for _, v in run) print(f'\n=== run of {len(run)} chars -> {s!r} ===') for off, val in run: print(f' file {ov_off + off:#08x} overlay+{off:#06x} mov reg, {val:#04x} {chr(val)!r}')
if __name__ == '__main__': main()
|