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}