code wiki / (root) / nx_pe_compile_win_call.nx

nx_pe_compile_win_call.nx source

↩ module page · 214 lines · 11807 B

1// nx_pe_compile_win_call.nx -- W3b-2: nxc2-COMPILED code issues a kernel32 call ITSELF. 2// 3// W3b-1 (nx_pe_compile_win_io) proved a compiled-organ PE does native kernel32 I/O, but the call 4// was in the hand-laid ENTRY STUB. This rung closes the gap W3b-1 deferred: the nxc2-COMPILED 5// `main` calls a NishiLang shim `nx_win_exit(code)` and THAT call reaches kernel32 ExitProcess via 6// a keystone-synthesized SysV->MS-x64 thunk -- with NO compiler or assembler edits. 7// 8// THE LINKER TRICK (why this is clean, not byte-surgery): the shared assembler axc_label_resolve 9// LOUD-FAILS (sys_exit 102) on any undefined symbol -- so a kernel32 import cannot be left as an 10// unresolved extern. Instead `nx_win_exit` is a REAL bodied NishiLang function: the compiler emits 11// `call nx_win_exit` (verified: direct call, not DCE'd, not tail-called) and the assembler resolves 12// the label normally. The keystone then acts as the LINKER: 13// 1. resolve nx_win_exit's offset in the assembled .text (alongside main's), 14// 2. APPEND a 13-byte MS-x64 thunk after the compiled code, 15// 3. OVERWRITE the shim's first 5 bytes with `jmp rel32` -> the thunk. 16// No size-matching (the rest of the shim body just becomes unreached), no NOP-pad needed, no 17// undefined symbols. The thunk (at a keystone-known RVA) can form the RIP-relative IAT call disp, 18// which the compiler could not (it does not know the PE's IAT layout -- binding is the linker's job). 19// 20// shim entry (overwritten): jmp <thunk> ; E9 rel32 21// thunk (appended to .text): mov ecx, edi ; SysV arg0 (rdi) -> MS-x64 arg0 (ecx) 22// sub rsp, 0x28 ; shadow + align 23// call [rip+ExitProcess] ; ExitProcess(code) -- never returns 24// int3 25// 26// NO-FALSE-GREEN: source is `{ let r = nx_win_exit(7); return r + 92 }`. r+92 = 99. If the redirect 27// did NOT happen, nx_win_exit would return 7, r=7, main returns 99, entry stub exits 99. Observing 28// exit 7 (NOT 99) proves the compiled call reached ExitProcess(7) and terminated the process BEFORE 29// `return 99`. Arg-vary (nx_win_exit(13) -> exit 13) proves the arg flows from compiled code through 30// the SysV->MS-x64 thunk. Tamper (corrupt ExitProcess import name) -> 0xC0000139 = REAL OS binding. 31// 32// PIPELINE (build WSL sovereign, run native): src.nx 33// -> ./_offc/nx_compile_x86_native.elf <src> > /tmp/nxwin.s 34// -> ./_offc/nx_sov_build_run.elf nx_pe_compile_win_call (reads /tmp/nxwin.s) 35// -> _offc/nx_win_compiled_call.exe -> run native on Windows 11. 36// 37// HONEST SCOPE: one kernel32 import (ExitProcess), one no-result thunk, fixed shim name. Generalizes 38// to WriteFile/read/etc. by widening the thunk's ABI marshaling (rdx/r8/r9 + stack) and a shim->import 39// table -> then an I/O organ's own sys_write maps here. Replicates the keystone assemble (reuses 40// axc_pass; no shared-assembler edit). lineage_id: substrate_pe_compile_win_call_v1 41 42import "nx_syscalls.nx" 43import "nxasm_x86.nx" 44import "nx_pe_writer.nx" 45 46const NXC_CODE_CAP: i64 = 1048576 47const NXC_FILE_SIZE: i64 = 0x600 // headers + .text + .idata (keystone single-import layout) 48const NXC_STUB_LEN: i64 = 18 // keystone entry stub: sub rsp,0x28;call main;mov ecx,eax;call ExitProcess;int3 49const NXC_THUNK_LEN: i64 = 13 // mov ecx,edi;sub rsp,0x28;call [rip+ExitProcess];int3 50const NXC_TEXT_CAP: i64 = 0x200 // one .text file chunk (stub + compiled code + thunk) 51 52// Assemble AT&T x86_64 `.s`; report `main` AND `nx_win_exit` offsets. Mirrors the keystone assemble 53// (reuses axc_pass; resolves named labels instead of _start; no shared-assembler edit). 54func nxc_assemble(src: *u8, n: i64, out: *u8, out_cap: i64, p_main: *i64, p_winexit: *i64) -> i64 { 55 let lab_off: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 56 let lab_len: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 57 let lab_addr: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 58 let lab_sec: *i64 = sys_mmap(ASM_MAX_LABELS * 8) as *i64 59 let op0: *i64 = sys_mmap(72) as *i64 // 9 slots for SIB (matches nxasm_x86) 60 let op1: *i64 = sys_mmap(72) as *i64 61 let op2: *i64 = sys_mmap(72) as *i64 // API DRIFT FIX: axc_pass gained op2 62 let scratch: *u8 = sys_mmap(64) 63 let posbox: *i64 = sys_mmap(16) as *i64 64 let n_lab_box: *i64 = sys_mmap(16) as *i64 65 n_lab_box[0] = 0 66 let lh: *i64 = sys_mmap(ASM_LH_SIZE * 8) as *i64 67 68 let text_size: i64 = axc_pass(src, n, out, 0, 0, lab_off, lab_len, lab_addr, lab_sec, n_lab_box, lh, op0, op1, op2, scratch, posbox) 69 if text_size < 0 { return text_size } 70 let n_lab: i64 = n_lab_box[0] 71 72 var main_a: i64 = 0 - 1 73 var winexit_a: i64 = 0 - 1 74 var k: i64 = 0 75 while k < n_lab { 76 if lab_sec[k] == 1 { lab_addr[k] = lab_addr[k] + text_size } 77 if axc_tok_is(src, lab_off[k], lab_len[k], "main") == 1 { main_a = lab_addr[k] } 78 if axc_tok_is(src, lab_off[k], lab_len[k], "nx_win_exit") == 1 { winexit_a = lab_addr[k] } 79 k = k + 1 80 } 81 axc_lh_build(src, lab_off, lab_len, n_lab, lh) 82 83 let total: i64 = axc_pass(src, n, out, text_size, 1, lab_off, lab_len, lab_addr, lab_sec, n_lab_box, lh, op0, op1, op2, scratch, posbox) 84 if total < 0 { return total } 85 if total > out_cap { return 0 - 200 } 86 p_main[0] = main_a 87 p_winexit[0] = winexit_a 88 return total 89} 90 91// Wrap assembled `code` in a PE; redirect the compiled `nx_win_exit` shim (at winexit_off) to a 92// keystone-synthesized MS-x64 ExitProcess thunk appended after the code. Mirrors the keystone 93// single-import (.text + .idata) layout; ExitProcess IAT at RVA_IAT (0x2038). 94func nxc_emit_pe(buf: *u8, code: *u8, code_len: i64, main_off: i64, winexit_off: i64) -> i64 { 95 if (buf as i64) == 0 { return 0 - NX_PE_BAD_INPUT } 96 let thunk_off: i64 = NXC_STUB_LEN + code_len // byte offset of thunk within .text 97 let text_vsize: i64 = thunk_off + NXC_THUNK_LEN 98 if text_vsize > NXC_TEXT_CAP { return 0 - NX_PE_BAD_INPUT } 99 if winexit_off < 0 { return 0 - NX_PE_BAD_INPUT } 100 101 // ===== DOS + PE sig ===== 102 _w16(buf, 0, 0x5A4D) 103 _w32(buf, 0x3C, FOFF_PE_SIG) 104 _w32(buf, FOFF_PE_SIG, 0x00004550) 105 // ===== COFF (2 sections) ===== 106 _w16(buf, FOFF_COFF + 0, PE_MACHINE_AMD64) 107 _w16(buf, FOFF_COFF + 2, 2) 108 _w16(buf, FOFF_COFF + 16, 0xF0) 109 _w16(buf, FOFF_COFF + 18, PE_CHAR_EXEC | PE_CHAR_LARGE_ADDR) 110 // ===== Optional Header (PE32+) -- keystone/exit42 values ===== 111 _w16(buf, FOFF_OPT + 0, PE_OH_MAGIC_PEPLUS) 112 _w8(buf, FOFF_OPT + 2, 1) 113 _w32(buf, FOFF_OPT + 4, 0x200) 114 _w32(buf, FOFF_OPT + 8, 0x200) 115 _w32(buf, FOFF_OPT + 16, RVA_TEXT) 116 _w32(buf, FOFF_OPT + 20, RVA_TEXT) 117 _w64(buf, FOFF_OPT + 24, IMG_BASE_LO, IMG_BASE_HI) 118 _w32(buf, FOFF_OPT + 32, 0x1000) 119 _w32(buf, FOFF_OPT + 36, 0x200) 120 _w16(buf, FOFF_OPT + 40, 6) 121 _w16(buf, FOFF_OPT + 48, 6) 122 _w32(buf, FOFF_OPT + 56, 0x3000) 123 _w32(buf, FOFF_OPT + 60, 0x200) 124 _w16(buf, FOFF_OPT + 68, PE_SUBSYSTEM_CONSOLE) 125 _w64(buf, FOFF_OPT + 72, 0x100000, 0) 126 _w64(buf, FOFF_OPT + 80, 0x1000, 0) 127 _w64(buf, FOFF_OPT + 88, 0x100000, 0) 128 _w64(buf, FOFF_OPT + 96, 0x1000, 0) 129 _w32(buf, FOFF_OPT + 108, 16) 130 _w32(buf, FOFF_OPT + 112 + 8, RVA_IMP_DESC) 131 _w32(buf, FOFF_OPT + 112 + 12, 0x28) 132 // ===== Section headers: .text (dynamic vsize) + .idata ===== 133 _emit_section_header(buf, FOFF_SECT_TBL, 46, 116, 101, 120, 116, 0, 0, 0, text_vsize, RVA_TEXT, 0x200, FOFF_TEXT, PE_SECT_CODE_X_R) 134 _emit_section_header(buf, FOFF_SECT_TBL + 40, 46, 105, 100, 97, 116, 97, 0, 0, 0x63, RVA_IDATA, 0x200, FOFF_IDATA, PE_SECT_DATA_R) 135 136 // ===== .text: keystone entry stub (sub rsp,0x28;call main;mov ecx,eax;call ExitProcess;int3) ===== 137 let t: i64 = FOFF_TEXT 138 _w8(buf, t+0, 0x48); _w8(buf, t+1, 0x83); _w8(buf, t+2, 0xEC); _w8(buf, t+3, 0x28) // sub rsp,0x28 139 _w8(buf, t+4, 0xE8); _w32(buf, t+5, 0x9 + main_off) // call main 140 _w8(buf, t+9, 0x89); _w8(buf, t+10, 0xC1) // mov ecx,eax 141 _w8(buf, t+11, 0xFF); _w8(buf, t+12, 0x15); _w32(buf, t+13, 0x1027) // call [rip] ExitProcess (IAT 0x2038) 142 _w8(buf, t+17, 0xCC) // int3 143 // copy assembled compiled code after the 18-byte stub 144 var i: i64 = 0 145 while i < code_len { buf[t + NXC_STUB_LEN + i] = code[i]; i = i + 1 } 146 147 // ===== LINKER STEP: synthesize the MS-x64 ExitProcess thunk + redirect the shim ===== 148 // thunk RVA = 0x1000 + thunk_off ; ExitProcess IAT RVA = RVA_IAT (0x2038) 149 let thunk_rva: i64 = RVA_TEXT + thunk_off 150 let th: i64 = FOFF_TEXT + thunk_off 151 // mov ecx, edi (SysV arg0 -> MS-x64 arg0) 152 _w8(buf, th+0, 0x89); _w8(buf, th+1, 0xF9) 153 // sub rsp, 0x28 (shadow + 16-byte align) 154 _w8(buf, th+2, 0x48); _w8(buf, th+3, 0x83); _w8(buf, th+4, 0xEC); _w8(buf, th+5, 0x28) 155 // call [rip+disp] ExitProcess; call at th+6, next-insn RVA = thunk_rva+12; disp = 0x2038 - (thunk_rva+12) 156 _w8(buf, th+6, 0xFF); _w8(buf, th+7, 0x15); _w32(buf, th+8, RVA_IAT - (thunk_rva + 12)) 157 // int3 (never reached -- ExitProcess does not return) 158 _w8(buf, th+12, 0xCC) 159 // OVERWRITE the shim's first 5 bytes with `jmp rel32` -> thunk. 160 // jmp at shim RVA = 0x1000 + STUB_LEN + winexit_off; next-insn RVA = shim_rva + 5; rel = thunk_rva - (shim_rva+5) 161 let shim_rva: i64 = RVA_TEXT + NXC_STUB_LEN + winexit_off 162 let sh: i64 = FOFF_TEXT + NXC_STUB_LEN + winexit_off 163 _w8(buf, sh+0, 0xE9); _w32(buf, sh+1, thunk_rva - (shim_rva + 5)) 164 165 // ===== .idata: single ExitProcess import (keystone/exit42 layout) ===== 166 let d: i64 = FOFF_IDATA 167 _w32(buf, d + 0, RVA_INT) 168 _w32(buf, d + 12, RVA_DLL_STR) 169 _w32(buf, d + 16, RVA_IAT) 170 _w64(buf, d + 0x28, RVA_IMP_NAME, 0) 171 _w64(buf, d + 0x30, 0, 0) 172 _w64(buf, d + 0x38, RVA_IMP_NAME, 0) 173 _w64(buf, d + 0x40, 0, 0) 174 _w16(buf, d + 0x48, 0) 175 _w8(buf,d+0x4A,69);_w8(buf,d+0x4B,120);_w8(buf,d+0x4C,105);_w8(buf,d+0x4D,116) // Exit 176 _w8(buf,d+0x4E,80);_w8(buf,d+0x4F,114);_w8(buf,d+0x50,111);_w8(buf,d+0x51,99) // Proc 177 _w8(buf,d+0x52,101);_w8(buf,d+0x53,115);_w8(buf,d+0x54,115);_w8(buf,d+0x55,0) // ess\0 178 _w8(buf,d+0x56,107);_w8(buf,d+0x57,101);_w8(buf,d+0x58,114);_w8(buf,d+0x59,110) // kern 179 _w8(buf,d+0x5A,101);_w8(buf,d+0x5B,108);_w8(buf,d+0x5C,51);_w8(buf,d+0x5D,50) // el32 180 _w8(buf,d+0x5E,46);_w8(buf,d+0x5F,100);_w8(buf,d+0x60,108);_w8(buf,d+0x61,108);_w8(buf,d+0x62,0) // .dll\0 181 182 return NX_PE_OK 183} 184 185func main() -> i64 { 186 let lenbox: *i64 = sys_mmap(16) as *i64 187 let src: *u8 = sys_read_file("/tmp/nxwin.s" as *u8, lenbox) 188 if (src as i64) == 0 { return 1 } 189 let n: i64 = lenbox[0] 190 if n <= 0 { return 2 } 191 192 let code: *u8 = sys_mmap(NXC_CODE_CAP) 193 let mainbox: *i64 = sys_mmap(16) as *i64 194 let webox: *i64 = sys_mmap(16) as *i64 195 mainbox[0] = 0 - 1 196 webox[0] = 0 - 1 197 let code_len: i64 = nxc_assemble(src, n, code, NXC_CODE_CAP, mainbox, webox) 198 if code_len < 0 { return 3 } 199 let main_off: i64 = mainbox[0] 200 if main_off < 0 { return 4 } 201 let winexit_off: i64 = webox[0] 202 if winexit_off < 0 { return 6 } // shim nx_win_exit not found in compiled output 203 204 let buf: *u8 = sys_mmap(NXC_FILE_SIZE) 205 let rc: i64 = nxc_emit_pe(buf, code, code_len, main_off, winexit_off) 206 if rc != NX_PE_OK { return 5 } 207 208 let outp: *u8 = "/mnt/c/Users/elder/nishi-core/nxc2/_offc/nx_win_compiled_call.exe" as *u8 209 if nx_pe_write_to_file(outp, buf, NXC_FILE_SIZE) != NX_PE_OK { return 70 } 210 211 let msg: *u8 = "[substrate] nxc2-compiled native call PE written: nx_win_compiled_call.exe\n" as *u8 212 sys_write(1, msg, 74) 213 return 0 214}