Last active
November 28, 2024 06:32
-
-
Save NeKzor/d1e7b4be34f07b21cc2cc44e49cfc4b6 to your computer and use it in GitHub Desktop.
process_vm_readv feat. Deno FFI+ iced-x86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export const libc = Deno.dlopen(Deno.env.get("LIBC_PATH") ?? "libc.so.6", { | |
ptrace: { | |
parameters: ["i32", "i32", "pointer", "pointer"], | |
result: "i32", | |
}, | |
process_vm_readv: { | |
parameters: ["i32", "buffer", "i32", "buffer", "i32", "i32"], | |
result: "i32", | |
}, | |
}); | |
export const closeLibc: () => void = () => libc.close(); | |
const { ptrace, process_vm_readv } = libc.symbols; | |
const PTRACE_ATTACH = 16; | |
const PTRACE_DETACH = 17; | |
export interface iovec { | |
iov_base: bigint; | |
iov_len: number; | |
} | |
export const iovecToBuffer = (iov: iovec): Uint8Array => { | |
const buffer = new Uint8Array(12); | |
const dv = new DataView(buffer.buffer); | |
dv.setBigUint64(0, iov.iov_base, true); | |
dv.setUint32(8, iov.iov_len, true); | |
return buffer; | |
}; | |
export const asU32 = (buffer: Uint8Array | null): number | null => { | |
if (!buffer) return null; | |
const dv = new DataView(buffer.buffer); | |
return dv.getUint32(0, true); | |
}; | |
export const asU64 = (buffer: Uint8Array | null): bigint | null => { | |
if (!buffer) return null; | |
const dv = new DataView(buffer.buffer); | |
return dv.getBigUint64(0, true); | |
}; | |
export const readProcessMemory = ( | |
pid: number, | |
address: bigint, | |
length: number, | |
): Uint8Array | null => { | |
const memory = new Uint8Array(length); | |
const local = iovecToBuffer({ | |
iov_base: Deno.UnsafePointer.value(Deno.UnsafePointer.of(memory)), | |
iov_len: memory.byteLength, | |
}); | |
const remote = iovecToBuffer({ | |
iov_base: address, | |
iov_len: memory.byteLength, | |
}); | |
return process_vm_readv(pid, local, 1, remote, 1, 0) !== -1 ? memory : null; | |
}; | |
export const launchProcess = ( | |
path: string, | |
args: string[], | |
): Deno.ChildProcess => { | |
const cmd = new Deno.Command(path, { args, stdout: "piped" }); | |
return cmd.spawn(); | |
}; | |
export const findProcess = async (procName: string): Promise<number | null> => { | |
const cmd = new Deno.Command("pidof", { args: [procName] }); | |
const { stdout, code } = await cmd.output(); | |
return code !== -1 | |
? parseInt(new TextDecoder().decode(stdout).trim(), 10) ?? null | |
: null; | |
}; | |
const dumpMain = async ( | |
code: Uint8Array, | |
rip: bigint, | |
is32Bit: boolean, | |
): Promise<void> => { | |
const { | |
Decoder, | |
DecoderOptions, | |
Formatter, | |
FormatterSyntax, | |
} = await import("npm:iced-x86@^1"); | |
const hexBytesColumnByteLength = 10; | |
const decoder = new Decoder(is32Bit ? 32 : 64, code, DecoderOptions.None); | |
decoder.ip = rip; | |
const instructions = decoder.decodeAll(); | |
const formatter = new Formatter(FormatterSyntax.Intel); | |
formatter.firstOperandCharIndex = 10; | |
formatter.spaceAfterOperandSeparator = true; | |
formatter.addLeadingZeroToHexNumbers = false; | |
formatter.spaceBetweenMemoryAddOperators = true; | |
instructions.forEach((instruction) => { | |
const disasm = formatter.format(instruction); | |
let line = instruction.ip.toString(16) | |
.padStart(16, "0") | |
.toUpperCase(); | |
line += " "; | |
const startIndex = Number(instruction.ip - rip); | |
code.slice(startIndex, startIndex + instruction.length).forEach( | |
(b) => { | |
line += (b.toString(16).padStart(2, "0")).toUpperCase() + " "; | |
}, | |
); | |
line += " "; | |
for (let i = instruction.length; i < hexBytesColumnByteLength; i++) { | |
line += " "; | |
} | |
line += " "; | |
line += disasm; | |
console.log(line); | |
}); | |
instructions.forEach((instruction) => instruction.free()); | |
formatter.free(); | |
decoder.free(); | |
}; | |
const findMain = async ( | |
pid: number, | |
procName: string, | |
is32Bit: boolean, | |
): Promise<void> => { | |
let foundHeader = false; | |
for ( | |
const line of Deno.readTextFileSync(`/proc/${pid}/maps`).split("\n") | |
) { | |
const module = line.slice(line.indexOf("/")).trim(); | |
const perm = line.split(" ").at(1); | |
if ( | |
(module.endsWith("/" + procName) || module.endsWith(".so")) && | |
(perm === "r-xp" || (perm === "r--p" && !foundHeader)) | |
) { | |
console.log("[+]", module); | |
const start = "0x" + line.split("-").at(0)!; | |
const address = BigInt(start); | |
const buffer = readProcessMemory(pid, address, 4); | |
if (buffer) { | |
console.log( | |
`[+] read ${buffer.byteLength} bytes:`, | |
"0x" + | |
[...buffer].map((x) => x.toString(16).padStart(2, "0")).join(" 0x"), | |
"at", | |
"0x" + address.toString(16), | |
); | |
const isHeader = buffer[0] === 0x7f && buffer[1] === 0x45 && | |
buffer[2] === 0x4c && buffer[3] === 0x46; | |
foundHeader ||= isHeader; | |
if (isHeader) { | |
const e_entry = is32Bit | |
? asU32(readProcessMemory(pid, address + 0x18n, 4)) | |
: asU64(readProcessMemory(pid, address + 0x18n, 8)); | |
if (e_entry) { | |
const _startAddress = address + BigInt(e_entry); | |
const _start = readProcessMemory(pid, _startAddress, 42); | |
_start | |
? await dumpMain(_start, _startAddress, is32Bit) | |
: console.log("[-] failed to read main"); | |
} else { | |
console.log("[-] failed to read e_entry"); | |
} | |
} else { | |
const func = readProcessMemory(pid, address, 42); | |
func | |
? await dumpMain(func, address, is32Bit) | |
: console.log("[-] failed to read main"); | |
} | |
} else { | |
console.error("[-] failed to read process memory"); | |
} | |
console.log(); | |
} | |
} | |
closeLibc(); | |
}; | |
if (import.meta.main) { | |
if (Deno.args.at(0) === "--self") { | |
await findMain(Deno.pid, "deno", true); | |
console.log("[+] done"); | |
Deno.exit(0); | |
} | |
const procPath = Deno.env.get("HOME") + | |
"/.local/share/Steam/steamapps/common/Portal 2/portal2.sh"; | |
const args = [ | |
"-game", | |
"portal2", | |
"-novid", | |
"-windowed", | |
"-w", | |
"1280", | |
"-h", | |
"720", | |
"-vulkan", | |
]; | |
const procName = "portal2_linux"; | |
const is32Bit = true; | |
let usePtrace = false; | |
let pid = await findProcess(procName); | |
if (!pid) { | |
console.log("[-] launching process", procName); | |
const { pid: _bashPid } = launchProcess(procPath, args); | |
await new Promise((resolve) => setTimeout(() => resolve(0), 5_000)); | |
pid = await findProcess(procName); | |
if (!pid) { | |
console.log("[-] unable to find process", procName); | |
Deno.exit(1); | |
} | |
} else { | |
if (Deno.uid() !== 0) { | |
console.log("[-] requires sudo because process already launched"); | |
Deno.exit(1); | |
} | |
usePtrace = true; | |
} | |
console.log("[+] found", procName, pid); | |
usePtrace && ptrace(PTRACE_ATTACH, pid, null, null); | |
console.log("[*] dumping main"); | |
await findMain(pid, procName, is32Bit); | |
usePtrace && ptrace(PTRACE_DETACH, pid, null, null); | |
console.log("[+] done"); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment