code wiki / (root) / nx_ssh_probe.nx

nx_ssh_probe.nx source

↩ module page · 105 lines · 4496 B

1// nx_ssh_probe.nx -- sovereign SSH-2 reconnaissance (phase 0 of the bits-up 2// SSH client). The SSH version-exchange line AND the server's first 3// SSH_MSG_KEXINIT are sent in PLAINTEXT before any key exchange, so this 4// connects raw-TCP to host:22, sends our version string, and parses the 5// server's KEXINIT algorithm name-lists -- telling us EXACTLY which KEX / 6// host-key / cipher / MAC to implement for interop. Grounds the full 7// client build in verified facts, not assumptions. 8// 9// Bits-up: raw syscalls only, no openssh/libssh. Oracle externals are for 10// comparison only, never on the path. 11// 12// license_tier: ORIGINAL 13 14import "nx_syscalls.nx" 15import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 16const K_MAGIC_8192: i64 = 8192 17 18func P(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 } 19 20// fill sockaddr_in (16 bytes) for ip:port. 21func ssh_sockaddr(sa: *u8, ip: i64, port: i64) -> i64 { 22 sa[0] = (AF_INET & 0xff) as u8 23 sa[1] = ((AF_INET >> 8) & 0xff) as u8 24 sa[2] = ((port >> 8) & 0xff) as u8 25 sa[3] = (port & 0xff) as u8 26 sa[4] = ((ip >> 24) & 0xff) as u8 27 sa[5] = ((ip >> 16) & 0xff) as u8 28 sa[6] = ((ip >> 8) & 0xff) as u8 29 sa[7] = (ip & 0xff) as u8 30 var p: i64 = 8 31 while p < 16 { sa[p] = 0 as u8; p = p + 1 } 32 return 0 33} 34 35func u32be(b: *u8, off: i64) -> i64 { 36 return ((b[off] as i64) << 24) | ((b[off+1] as i64) << 16) | ((b[off+2] as i64) << 8) | (b[off+3] as i64) 37} 38 39// print a name-list at offset `off`; return the offset just past it. 40func print_namelist(label: *u8, llen: i64, b: *u8, off: i64) -> i64 { 41 let n: i64 = u32be(b, off) 42 P(label, llen) 43 sys_write(1, b as *u8, 0) // noop to keep types happy 44 let start: i64 = off + 4 45 sys_write(1, (b + start) as *u8, n) 46 P("\n" as *u8, 1) 47 return start + n 48} 49 50func main() -> i64 { 51 let ip: i64 = (192 << 24) | (168 << 16) | (8 << 8) | 240 // NAS 52 let port: i64 = 22 53 54 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 55 if fd < 0 { P("socket fail\n" as *u8, 11); return 1 } 56 let sa: *u8 = sys_mmap(16) 57 ssh_sockaddr(sa, ip, port) 58 if nx_connect_bounded(fd, sa, 16, NX_CONN_DEFAULT_MS) != 0 { P("connect fail (is :22 open?)\n" as *u8, 27); return 2 } 59 60 // 1) send our identification string (must end CR LF). 61 let id: *u8 = "SSH-2.0-NishiSSH_0.1 probe\r\n" as *u8 62 sys_write(fd, id, 28) 63 64 // 2) read server banner + first KEXINIT into one buffer. 65 let buf: *u8 = sys_mmap(K_MAGIC_8192) 66 var total: i64 = 0 67 var tries: i64 = 0 68 while tries < 8 { 69 let got: i64 = sys_read(fd, (buf + total) as *u8, K_MAGIC_8192 - total) 70 if got <= 0 { tries = tries + 1 } 71 else { total = total + got; if total > 600 { tries = 8 } } 72 } 73 sys_close(fd) 74 if total < 40 { P("short read\n" as *u8, 11); return 3 } 75 76 // 3) print the server version line (up to LF). 77 P("server id : " as *u8, 12) 78 var i: i64 = 0 79 while i < total { if buf[i] == 10 { i = i + 1; break } i = i + 1 } 80 sys_write(1, buf as *u8, i) // includes the trailing \n 81 82 // 4) KEXINIT binary packet begins at i. Layout: 83 // u32 packet_length | u8 padding_length | payload[...] | padding 84 // payload[0]=20 (KEXINIT), [1..17]=cookie, then 10 name-lists. 85 let pkt: i64 = i 86 let plen: i64 = u32be(buf, pkt) 87 let msg: i64 = buf[pkt + 5] as i64 88 P("kexinit : packet_len=" as *u8, 24) 89 // tiny decimal 90 var m: i64 = plen; let d: *u8 = sys_mmap(16); var k: i64 = 0 91 if m == 0 { d[0] = 0x30 as u8; k = 1 } else { while m > 0 { d[k] = (0x30 + (m % 10)) as u8; m = m/10; k = k+1 } } 92 var j: i64 = k - 1; while j >= 0 { let o: *u8 = sys_mmap(1); o[0] = d[j]; sys_write(1, o, 1); j = j - 1 } 93 if msg == 20 { P(" (msg=20 KEXINIT OK)\n" as *u8, 21) } else { P(" (unexpected msg type)\n" as *u8, 23) } 94 95 // name-lists start after payload[0] (1) + cookie (16) = pkt + 5 + 17. 96 var off: i64 = pkt + 5 + 17 97 off = print_namelist("kex : " as *u8, 13, buf, off) 98 off = print_namelist("hostkey : " as *u8, 13, buf, off) 99 off = print_namelist("cipher c2s : " as *u8, 13, buf, off) 100 off = print_namelist("cipher s2c : " as *u8, 13, buf, off) 101 off = print_namelist("mac c2s : " as *u8, 13, buf, off) 102 off = print_namelist("mac s2c : " as *u8, 13, buf, off) 103 P("\n-> implement the FIRST mutually-supported of each (we have X25519/Ed25519/ChaCha20-Poly1305/SHA2)\n" as *u8, 96) 104 return 0 105}