diff --git a/README.md b/README.md index 84559c3..912389f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ Rd follows the conventional Plan 9 build workflow: ## Usage ``` -rd [-0A] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-s shell] [net!]server[!port] +rd [-0AN] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-s shell] [net!]server[!port] ``` Rd takes exactly **one non-optional argument**: a **server connection string**. @@ -82,13 +82,16 @@ Examples: ### Options - `-k keyspec` - Adds extra attributes to the **factotum** key query used to obtain credentials (the attributes are appended to the query passed to `auth_getuserpasswd`). + Adds extra attributes to the **factotum** key query used to obtain credentials. With plain TLS logon the attributes are appended to the `proto=pass service=rdp` query passed to `auth_getuserpasswd`. With `-N` (NLA) they are appended to the `proto=mschap service=rdp` query passed to `auth_respond`. - `-d dom` Specifies a **Windows domain name** to authenticate against (for domain logons). +- `-N` + Enables **Network Level Authentication (NLA)** using CredSSP/NTLM. After TLS is established, Rd performs an NTLM handshake encapsulated in ASN.1 DER `TSRequest` messages (MS-CSSP protocol). The NT response is computed by **factotum** via `auth_respond` with `proto=mschap`, so the plaintext password never leaves factotum. NLA is required by some Windows configurations and provides pre-session authentication. + - `-A` - Disables factotum-based “auto logon”. With `-A`, Rd requests the server to present an interactive **logon screen** instead, and credentials are entered and validated inside the remote GUI session. At that stage, credential submission occurs over an **encrypted channel**. + Disables factotum-based “auto logon”. With `-A`, Rd requests the server to present an interactive **logon screen** instead, and credentials are entered and validated inside the remote GUI session. At that stage, credential submission occurs over an **encrypted channel**. (Not applicable with `-N`: NLA always authenticates before the session starts.) - `-T title` Customizes the local window label. By default it is: @@ -122,12 +125,14 @@ A quick overview of the runtime flow and the major modules. Connection setup follows a staged pipeline: -1. Parse command-line options (title, depth, domain, shell override, working directory, console attach). -2. Optionally obtain credentials from factotum. +1. Parse command-line options (title, depth, domain, shell override, working directory, console attach, NLA flag). +2. Optionally obtain credentials from factotum (skipped when `-N` NLA is used, as factotum is consulted during the NLA handshake instead). 3. `dial()` the server (default TCP port 3389). -4. Perform **X.224** transport/session handshake. -5. Initialize the local screen/window. -6. Perform the main **RDP handshake** (negotiation/capabilities/licensing/activation). +4. Perform **X.224** transport/session handshake (negotiating TLS or CredSSP as the security protocol). +5. Upgrade the connection to **TLS** (`starttls`). +6. If `-N` was given, perform the **NLA (CredSSP/NTLM) handshake** (`nlahandshake`): a three-message NTLM exchange (Negotiate → Challenge → Authenticate) wrapped in ASN.1 DER `TSRequest` structures, with the NT response computed by factotum. +7. Initialize the local screen/window. +8. Perform the main **RDP handshake** (negotiation/capabilities/licensing/activation). ### 3) Concurrency model: network loop + local input helpers @@ -183,6 +188,7 @@ Rd includes a virtual-channel abstraction (`Vchan`) that: **Security:** - `tls.c` / `tls9f.c` — TLS support and certificate/thumbprint verification glue (build selects the appropriate file) +- `nla.c` — Network Level Authentication (NLA/CredSSP): NTLM message serialisers/parsers (`mkntnego`, `mkntauth`, `getntchal`) and `TSRequest` ASN.1 DER framing (`writetsreq`, `readtsreq`, `gettsreq`) **Rendering and graphics updates:** @@ -201,7 +207,7 @@ Rd includes a virtual-channel abstraction (`Vchan`) that: **Virtual channels and extensions:** - `vchan.c` — virtual channel table setup, fragmentation/defragmentation, and send/dispatch support -- `rpc.c` — higher-level request/response helpers used by some channel-style interactions +- `rpc.c` — higher-level request/response helpers; also contains `nlahandshake()`, the CredSSP orchestration that drives the three-message NTLM exchange via factotum - `audio.c` — audio virtual channel handling and client-side playback hooks - `efs.c` — extension/virtual channel support for device-redirection-style messages (see `Efsmsg` in `dat.h`) @@ -212,6 +218,7 @@ Rd includes a virtual-channel abstraction (`Vchan`) that: - `aud_test.c` — audio-related tests - `efs_test.c` — tests for EFS/extension encoding/decoding - `msg_test.c` — message parsing/encoding tests +- `nla_test.c` — NLA serialiser/parser tests (`writetsreq`/`gettsreq`, `mkntnego`, `getntchal`, `mkntauth`) **Headers:** diff --git a/dat.h b/dat.h index c13b812..0eb2c93 100644 --- a/dat.h +++ b/dat.h @@ -50,6 +50,11 @@ struct Rdp char *passwd; /* password for auto logon (sic) */ char *shell; /* remote shell override */ char *rwd; /* remote working directory */ + char *keyspec; /* factotum key spec */ + int nla; /* use NLA (CredSSP/NTLM) authentication */ + char *server; /* server hostname (for NTLM SPN: "TERMSRV/") */ + uchar *tlscert; /* TLS server certificate DER (for CredSSP pubKeyAuth) */ + int tlscertlen; int xsz; /* rfb dimensions */ int ysz; /* rfb dimensions */ int depth; /* rfb color depth */ diff --git a/efs_test.c b/efs_test.c index 067a7ca..7df001b 100644 --- a/efs_test.c +++ b/efs_test.c @@ -13,6 +13,7 @@ int audiotests(void); int egditests(void); int msgtests(void); int mppctests(void); +int nlatests(void); int rletests(void); int utf16tests(void); @@ -132,6 +133,7 @@ main(int, char**) egditests(); msgtests(); mppctests(); + nlatests(); rletests(); utf16tests(); print("ok\n"); diff --git a/fns.h b/fns.h index 6b3840d..b6e75df 100644 --- a/fns.h +++ b/fns.h @@ -39,6 +39,32 @@ int istpkt(uchar*,uchar*); int tptype(uchar*,uchar*); uchar* tpdat(uchar*,uchar*); +/* nla.c buffer sizes (also used by rpc.c) */ +enum +{ + MaxNTLMTargetInfo = 1024, /* maximum TargetInfo AvPairs length from challenge */ + MaxNTLMClientAvExtra = 8 + (4+16) + (4+512) + 4, /* MsvAvFlags+MsvAvChannelBindings+MsvAvTargetName+EOL */ + NTv2RespMax = 16 + 28 + MaxNTLMTargetInfo + MaxNTLMClientAvExtra, /* max NTLMv2 NtChallengeResponse */ +}; + +/* nla.c */ +int mkntnego(uchar*, int); +int getntchal(uchar[8], uchar*, int); +uchar* getntargetinfo(uchar*, int, int*); +int ntv2frompasswd(char*, char*, char*, uchar*, uchar*, uchar*, int, uchar*, int, uchar*, uchar*, uchar*, int, char*); +int mkntauth(uchar*, int, char*, char*, uchar*, int, uchar*, uchar*); +void ntrespfrompasswd(char*, uchar[8], uchar[24]); +int writetsreq(int, uchar*, int); +int writetsreqnonce(int, uchar*, int, uchar*, int); +int writetsreqdone(int, uchar*, int, uchar*, int); +int nlafinish(int, uchar*, int, uchar*, char*, char*, char*, uchar*); +int readtsreq(int, uchar*, int); +int readtsreq_oreuarp(int, uchar*, int, ulong*); +uchar* gettsreq(uchar*, int, int*); + +/* rpc.c */ +int nlahandshake(Rdp*); + /* rd.c */ void atexitkiller(void); void atexitkill(int pid); diff --git a/mkfile b/mkfile index 5de4bab..3cc394a 100644 --- a/mkfile +++ b/mkfile @@ -21,6 +21,7 @@ OFILES=\ mpas.$O\ mppc.$O\ msg.$O\ + nla.$O\ rd.$O\ rpc.$O\ utf16.$O\ @@ -31,12 +32,13 @@ OFILES=\ THREADOFILES=${OFILES:rd.$O=rd-thread.$O} CLEANFILES=$O.thread $O.test -TESTHFILES=audio.c mppc.c rle.c egdi.c +TESTHFILES=audio.c mppc.c rle.c egdi.c nla.c TESTOFILES=\ efs_test.$O errs.$O efs.$O utf16.$O \ aud_test.$O \ msg_test.$O x224.$O mcs.$O ele.$O mpas.$O alloc.$O cap.$O egdi_test.$O \ mppc_test.$O \ + nla_test.$O \ rle_test.$O \ utf16_test.$O \ diff --git a/msg_test.c b/msg_test.c index ba4f011..2edf697 100644 --- a/msg_test.c +++ b/msg_test.c @@ -22,6 +22,28 @@ testputmsgxconnect(void){ } +static int +testputmsgxconnectnla(void) +{ + /* Xconnect advertising HYBRID+HYBRID_EX (ProtoCSSP|ProtoUAUTH) for NLA */ + int n; + char *s, *want; + uchar buf[1042]; + Msg m; + + m.type = Xconnect; + m.negproto = ProtoCSSP | ProtoUAUTH; + n = putmsg(buf, sizeof buf, &m); + if(n < 0) + sysfatal("testputmsgxconnectnla: unexpected error: %r\n"); + s = smprint("%.*H", n, buf); + want = "0300002722E00000000000436F6F6B69653A206D737473686173683D780D0A010008000A000000"; + if(strcmp(s, want) != 0) + sysfatal("testputmsgxconnectnla: want %s, got %s", want, s); + free(s); + return 0; +} + static int testputmsgxhangup(void){ int n; @@ -572,6 +594,26 @@ testgetmsgxconntls(void) return 0; } +static int +testgetmsgxconnhybridex(void) +{ + /* X.224 connection confirm with HYBRID_EX (ProtoUAUTH) selected by server */ + int n, nb; + uchar buf[32]; + Msg m = {0}; + char *hex = "0300001306D000000000000200080008000000"; + + nb = dec16(buf, sizeof buf, hex, strlen(hex)); + n = getmsg(&m, buf, nb); + if(n <= 0) + sysfatal("testgetmsgxconnhybridex: unexpected error: %r"); + if(m.type != Xconnected) + sysfatal("testgetmsgxconnhybridex: type: want %d, got %d", Xconnected, m.type); + if(m.negproto != ProtoUAUTH) + sysfatal("testgetmsgxconnhybridex: negproto: want %d (ProtoUAUTH), got %d", ProtoUAUTH, m.negproto); + return 0; +} + static int testgetmsgxconnnonego(void) { @@ -1306,6 +1348,7 @@ msgtests(void) { fmtinstall('H', encodefmt); testputmsgxconnect(); + testputmsgxconnectnla(); testputmsgxhangup(); testputmsgmattach(); testputmsgmjoin(); @@ -1326,6 +1369,7 @@ msgtests(void) testgetmsgfp2(); testgetmsgfp3(); testgetmsgxconntls(); + testgetmsgxconnhybridex(); testgetmsgxconnnonego(); testgetmsgmattacheduid(); testgetmsgmattachednouid(); diff --git a/nla.c b/nla.c new file mode 100644 index 0000000..2106591 --- /dev/null +++ b/nla.c @@ -0,0 +1,1274 @@ +/* + * Network Level Authentication (NLA) via CredSSP [MS-CSSP]. + * + * After TLS is established, the client authenticates using NTLM + * (MS-NLMP) encapsulated in TSRequest ASN.1 DER messages, then + * delegates credentials via TSCredentials encrypted with the NTLM + * session key. The NT response is computed by factotum via + * auth_respond(2) with proto=mschap. The session key and TSCredentials + * password require a separate proto=pass factotum key (or -p flag). + * + * Exchange: + * Client → Server: TSRequest { version, negoTokens=[NTLM Negotiate], clientNonce } + * Server → Client: TSRequest { negoTokens=[NTLM Challenge] } + * Client → Server: TSRequest { negoTokens=[NTLM Authenticate] } + * Server → Client: TSRequest { pubKeyAuth } + * Client → Server: TSRequest { pubKeyAuth, authInfo=TSCredentials } + */ +#include +#include +#include +#include "dat.h" +#include "fns.h" + +enum +{ + /* NTLM NegotiateFlags (MS-NLMP §2.2.2.5) */ + NfUnicode = (1<<0), /* NTLMSSP_NEGOTIATE_UNICODE */ + NfReqTarget = (1<<2), /* NTLMSSP_REQUEST_TARGET */ + NfSign = (1<<4), /* NTLMSSP_NEGOTIATE_SIGN */ + NfSeal = (1<<5), /* NTLMSSP_NEGOTIATE_SEAL */ + NfNTLM = (1<<9), /* NTLMSSP_NEGOTIATE_NTLM */ + NfAlwaysSign = (1<<15), /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ + NfESS = (1<<19), /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ + Nf128 = (1<<29), /* NTLMSSP_NEGOTIATE_128 */ + NfKeyExch = (1<<30), /* NTLMSSP_NEGOTIATE_KEY_EXCH */ + + /* NTLM response sizes */ + NTRespLen = 24, /* NTLMv1 NT/LM response length */ + + /* ASN.1 Universal tags (BER/DER) */ + TagInt = 2, /* INTEGER */ + TagOctetString = 4, /* OCTET STRING */ + TagSeq = 16, /* SEQUENCE / SEQUENCE OF */ + + /* BER class/construction bits */ + BerConstructed = 0x20, /* constructed encoding bit */ + BerContext = 0xa0, /* context-specific + constructed base */ + + /* BER long-form length octets */ + BerShortMax = 0x80, /* values below this fit in one byte */ + BerLen1 = 0x81, /* long form: length in next 1 byte */ + BerLen2 = 0x82, /* long form: length in next 2 bytes */ + + /* CredSSP TSRequest context-specific field tags (gbtag returns 5-bit tag number) */ + TSSnegoTokens = 1, /* TSRequest [1] negoTokens field */ + TSSnegoToken = 0, /* NegoDataItem [0] negoToken field */ + TSSauthInfo = 2, /* TSRequest [2] authInfo field */ + TSSpubKeyAuth = 3, /* TSRequest [3] pubKeyAuth field */ + TSSclientNonce = 5, /* TSRequest [5] clientNonce field (version 5+) */ + + /* CredSSP version advertised in TSRequest (v5 = required by Windows 10+) */ + CredSSPVer = 5, +}; + +static int +sizeder(int n) +{ + if(n < BerShortMax) + return 1; + if(n < 0x100) + return 2; + return 3; +} + +static uchar* +putder(uchar *p, int n) +{ + if(n < BerShortMax){ + *p++ = n; + }else if(n < 0x100){ + *p++ = BerLen1; + *p++ = n; + }else{ + *p++ = BerLen2; + *p++ = n >> 8; + *p++ = n; + } + return p; +} + +/* + * Extract SubjectPublicKeyInfo (SPKI) from a DER-encoded X.509 certificate. + * Returns a pointer to the SPKI TLV within cert and sets *spkilen. + * Returns nil on error (cert malformed or SPKI not found). + */ +static uchar* +certspki(uchar *cert, int certlen, int *spkilen) +{ + uchar *p, *ep, *tbsep, *start; + int tag, len, i; + + p = cert; + ep = cert + certlen; + + /* Certificate SEQUENCE */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) + return nil; + ep = p + len; + + /* tbsCertificate SEQUENCE */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) + return nil; + tbsep = p + len; + + /* optional [0] version (context tag 0) */ + { + uchar *q; + int t, l; + q = gbtag(p, tbsep, &t); + if(q != nil && t == 0){ + if((q = gblen(q, tbsep, &l)) == nil) + return nil; + p = q + l; + } + } + + /* serialNumber INTEGER, signature SEQUENCE, issuer SEQUENCE, + * validity SEQUENCE, subject SEQUENCE: skip 5 fields */ + for(i = 0; i < 5; i++){ + uchar *q; + int t, l; + if((q = gbtag(p, tbsep, &t)) == nil + || (q = gblen(q, tbsep, &l)) == nil) + return nil; + p = q + l; + } + + /* subjectPublicKeyInfo SEQUENCE is next */ + start = p; + if((p = gbtag(p, tbsep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, tbsep, &len)) == nil) + return nil; + *spkilen = (p + len) - start; + return start; +} + +/* + * Derive the NTLMv1 ExportedSessionKey from a plaintext password. + * sesskey = MD4(MD4(unicode(password))) + * The inner MD4 is the NT hash; the outer MD4 is the SessionBaseKey. + */ +static void +ntsesskey(char *pass, uchar sesskey[MD5dlen]) +{ + Rune r; + int i, n; + uchar *w, unipass[256], nthash[MD5dlen]; + + n = strlen(pass); + if(n > 128) + n = 128; + for(i = 0, w = unipass; i < n; i++){ + pass += chartorune(&r, pass); + *w++ = r & 0xff; + *w++ = r >> 8; + } + md4(unipass, w - unipass, nthash, nil); + md4(nthash, MD5dlen, sesskey, nil); +} + +/* + * Expand a 7-byte NTLM DES key to 8 bytes by inserting parity bits. + */ +static void +des7to8(uchar key7[7], uchar key8[8]) +{ + key8[0] = key7[0] & 0xfe; + key8[1] = ((key7[0]<<7)|(key7[1]>>1)) & 0xfe; + key8[2] = ((key7[1]<<6)|(key7[2]>>2)) & 0xfe; + key8[3] = ((key7[2]<<5)|(key7[3]>>3)) & 0xfe; + key8[4] = ((key7[3]<<4)|(key7[4]>>4)) & 0xfe; + key8[5] = ((key7[4]<<3)|(key7[5]>>5)) & 0xfe; + key8[6] = ((key7[5]<<2)|(key7[6]>>6)) & 0xfe; + key8[7] = (key7[6]<<1) & 0xfe; +} + +/* + * Apply DES-ECB to an 8-byte block using a 7-byte (56-bit) NTLM DES key. + * DES is mandated by MS-NLMP §3.3.1 (DESL function) and cannot be avoided + * in NTLMv1; this is a known protocol weakness. + */ +static void +ntlmdes(uchar *key7, uchar chal[8], uchar out[8]) +{ + DESstate ds; + uchar key8[8]; + + des7to8(key7, key8); + setupDESstate(&ds, key8, nil); + memmove(out, chal, 8); + desECBencrypt(out, 8, &ds); +} + +/* + * Compute the 24-byte NTLMv1 NT response from a plaintext password and + * challenge. Implements DESL(MD4(UNICODE(pass)), chal) per MS-NLMP §3.3.1. + * ntresp must be at least 24 bytes. + */ +void +ntrespfrompasswd(char *pass, uchar chal[8], uchar ntresp[24]) +{ + Rune r; + int i, n; + uchar *w, unipass[256], nthash[MD4dlen], padded[21]; + + n = strlen(pass); + if(n > 128) /* 128 chars × 2 bytes UTF-16 = 256 byte unipass[] */ + n = 128; + for(i = 0, w = unipass; i < n; i++){ + pass += chartorune(&r, pass); + *w++ = r & 0xff; + *w++ = r >> 8; + } + md4(unipass, w - unipass, nthash, nil); + memset(padded, 0, sizeof padded); + memmove(padded, nthash, MD4dlen); + ntlmdes(padded+0, chal, ntresp+0); + ntlmdes(padded+7, chal, ntresp+8); + ntlmdes(padded+14, chal, ntresp+16); +} + +/* + * Derive NTLM SignKey and SealKey from the ExportedSessionKey (MS-NLMP). + * Magic strings include the explicit NUL terminator per spec. + */ +static void +ntlmkeys(uchar *sesskey, uchar signkey[MD5dlen], uchar sealkey[MD5dlen]) +{ + DigestState *ds; + static char signmagic[] = "session key to client-to-server signing key magic constant"; + static char sealmagic[] = "session key to client-to-server sealing key magic constant"; + + ds = md5((uchar*)sesskey, MD5dlen, nil, nil); + md5((uchar*)signmagic, sizeof signmagic, signkey, ds); /* sizeof includes '\0' */ + ds = md5((uchar*)sesskey, MD5dlen, nil, nil); + md5((uchar*)sealmagic, sizeof sealmagic, sealkey, ds); +} + +/* + * NTLM EncryptMessage (NTLMv1 without ESS) per MS-NLMP Table 3. + * Output: NTLMSSP_MESSAGE_SIGNATURE (16 bytes) || encrypted message. + * RC4 state is shared: message is encrypted first, checksum second. + */ +static int +ntlmseal(uchar *out, int nout, uchar *signkey, uchar *sealkey, + ulong seqno, uchar *msg, int nmsg) +{ + RC4state h; + uchar hmac[MD5dlen], seqbuf[4]; + DigestState *ds; + + if(nout < 16 + nmsg){ + werrstr("ntlmseal: buffer too small"); + return -1; + } + /* Encrypt message using SealKey; RC4 state advances */ + setupRC4state(&h, sealkey, MD5dlen); + memmove(out+16, msg, nmsg); + rc4(&h, out+16, nmsg); + + /* HMAC_MD5(SignKey, seqno_le || plaintext) – first 4 bytes used */ + seqbuf[0] = seqno; seqbuf[1] = seqno>>8; + seqbuf[2] = seqno>>16; seqbuf[3] = seqno>>24; + ds = hmac_md5(seqbuf, 4, signkey, MD5dlen, nil, nil); + hmac_md5(msg, nmsg, signkey, MD5dlen, hmac, ds); + + /* Encrypt 4-byte checksum using the continued RC4 state */ + rc4(&h, hmac, 4); + + /* NTLMSSP_MESSAGE_SIGNATURE: Version | RandomPad=0 | Checksum | SeqNum */ + PLONG(out, 0x00000001); + PLONG(out+4, 0); + memmove(out+8, hmac, 4); + PLONG(out+12, seqno); + + return 16 + nmsg; +} + +/* + * Encode TSPasswordCreds { domainName [0], userName [1], password [2] } + * All string fields are UTF-16LE OCTET STRINGs wrapped in EXPLICIT context tags. + */ +static int +mktspasswdcreds(uchar *buf, int nbuf, char *dom, char *user, char *pass) +{ + uchar d16[512], u16[512], p16[512]; + int dlen, ulen, plen; + int f0sz, f1sz, f2sz, seqbody, total; + uchar *p; + + dlen = toutf16(d16, sizeof d16, dom, strlen(dom)); + ulen = toutf16(u16, sizeof u16, user, strlen(user)); + plen = toutf16(p16, sizeof p16, pass, strlen(pass)); + + /* Each field: [n] EXPLICIT OCTET STRING = context_tag + len(inner) + 0x04 + len(val) + val */ +#define FIELDSZ(vlen) (1 + sizeder(1 + sizeder(vlen) + (vlen)) + 1 + sizeder(vlen) + (vlen)) + f0sz = FIELDSZ(dlen); + f1sz = FIELDSZ(ulen); + f2sz = FIELDSZ(plen); +#undef FIELDSZ + + seqbody = f0sz + f1sz + f2sz; + total = 1 + sizeder(seqbody) + seqbody; + if(total > nbuf){ + werrstr("mktspasswdcreds: buffer too small"); + return -1; + } + + p = buf; + *p++ = BerConstructed|TagSeq; p = putder(p, seqbody); + +#define PUTFIELD(tag, v16, vlen) \ + do { \ + *p++ = BerContext|(tag); p = putder(p, 1 + sizeder(vlen) + (vlen)); \ + *p++ = TagOctetString; p = putder(p, vlen); \ + memmove(p, v16, vlen); p += vlen; \ + } while(0) + + PUTFIELD(0, d16, dlen); + PUTFIELD(1, u16, ulen); + PUTFIELD(2, p16, plen); +#undef PUTFIELD + + return p - buf; +} + +/* + * Encode TSCredentials { credType [0] INTEGER 1, credentials [1] OCTET STRING }. + * The credentials field contains the DER encoding of TSPasswordCreds. + */ +static int +mktscreds(uchar *buf, int nbuf, char *dom, char *user, char *pass) +{ + uchar pwdbuf[2048]; + int pwdlen; + int a0body, a0sz, a1octsz, a1sz, seqbody, total; + uchar *p; + + pwdlen = mktspasswdcreds(pwdbuf, sizeof pwdbuf, dom, user, pass); + if(pwdlen < 0) + return -1; + + /* [0] EXPLICIT INTEGER 1 – always 5 bytes: a0 03 02 01 01 */ + a0body = 3; /* TagInt(1) + len(1) + value(1) */ + a0sz = 1 + sizeder(a0body) + a0body; + + /* [1] EXPLICIT OCTET STRING (DER of TSPasswordCreds) */ + a1octsz = 1 + sizeder(pwdlen) + pwdlen; + a1sz = 1 + sizeder(a1octsz) + a1octsz; + + seqbody = a0sz + a1sz; + total = 1 + sizeder(seqbody) + seqbody; + if(total > nbuf){ + werrstr("mktscreds: buffer too small"); + return -1; + } + + p = buf; + *p++ = BerConstructed|TagSeq; p = putder(p, seqbody); + /* [0] credType = 1 (password) */ + *p++ = BerContext|0; p = putder(p, a0body); + *p++ = TagInt; *p++ = 1; *p++ = 1; + /* [1] credentials = DER(TSPasswordCreds) */ + *p++ = BerContext|1; p = putder(p, a1octsz); + *p++ = TagOctetString; p = putder(p, pwdlen); + memmove(p, pwdbuf, pwdlen); p += pwdlen; + + return p - buf; +} + +/* + * Compute the CredSSP v5 client-to-server pubKeyAuth: + * ClientServerHashKey = HMAC_SHA256(sesskey, "CredSSP Client-To-Server Binding Hash\0") + * pubKeyAuth = HMAC_SHA256(ClientServerHashKey, cnonce || spki) + * cnonce is the 32-byte client nonce sent in Phase A. + * spki is the SubjectPublicKeyInfo DER from the server's TLS certificate. + */ +static int +mkpubkeyauth(uchar *out, int nout, uchar *sesskey, uchar *cnonce, + uchar *spki, int spkilen) +{ + uchar hashkey[SHA2_256dlen]; + DigestState *ds; + /* sizeof includes the terminating NUL, which is the explicit \0 in the spec */ + static char csmagic[] = "CredSSP Client-To-Server Binding Hash"; + + if(nout < SHA2_256dlen){ + werrstr("mkpubkeyauth: buffer too small"); + return -1; + } + hmac_sha2_256((uchar*)csmagic, sizeof csmagic, sesskey, MD5dlen, hashkey, nil); + ds = hmac_sha2_256(cnonce, 32, hashkey, SHA2_256dlen, nil, nil); + hmac_sha2_256(spki, spkilen, hashkey, SHA2_256dlen, out, ds); + return SHA2_256dlen; +} + +/* + * Build Phase A TSRequest: version + negoTokens + clientNonce [5]. + * (Same as mktsreq but also includes [5] OCTET STRING clientNonce for CredSSP v5.) + */ +static int +mktsreqA(uchar *buf, int nbuf, uchar *tok, int toklen, uchar *nonce, int noncelen) +{ + int octetsz, a0toksz, itemsz, datasz, a1sz, nonceoct, a5sz, bodysz, total; + uchar *p; + + octetsz = 1 + sizeder(toklen) + toklen; + a0toksz = 1 + sizeder(octetsz) + octetsz; + itemsz = 1 + sizeder(a0toksz) + a0toksz; + datasz = 1 + sizeder(itemsz) + itemsz; + a1sz = 1 + sizeder(datasz) + datasz; + /* [5] clientNonce OCTET STRING */ + nonceoct = 1 + sizeder(noncelen) + noncelen; + a5sz = 1 + sizeder(nonceoct) + nonceoct; + /* [0] INTEGER CredSSPVer: 5 bytes */ + bodysz = 5 + a1sz + a5sz; + total = 1 + sizeder(bodysz) + bodysz; + + if(total > nbuf){ + werrstr("mktsreqA: buffer too small (%d < %d)", nbuf, total); + return -1; + } + + p = buf; + *p++ = BerConstructed|TagSeq; p = putder(p, bodysz); + /* version [0] */ + *p++ = BerContext|TSSnegoToken; *p++ = 0x03; /* len */ + *p++ = TagInt; p = putder(p, 1); *p++ = CredSSPVer; + /* negoTokens [1] */ + *p++ = BerContext|TSSnegoTokens; p = putder(p, datasz); + *p++ = BerConstructed|TagSeq; p = putder(p, itemsz); + *p++ = BerConstructed|TagSeq; p = putder(p, a0toksz); + *p++ = BerContext|TSSnegoToken; p = putder(p, octetsz); + *p++ = TagOctetString; p = putder(p, toklen); + memmove(p, tok, toklen); p += toklen; + /* clientNonce [5] */ + *p++ = BerContext|TSSclientNonce; p = putder(p, nonceoct); + *p++ = TagOctetString; p = putder(p, noncelen); + memmove(p, nonce, noncelen); p += noncelen; + + return p - buf; +} + +/* + * Build Phase E TSRequest: version + authInfo [2] + pubKeyAuth [3]. + */ +static int +mktsreqE(uchar *buf, int nbuf, uchar *pubkey, int pubkeylen, uchar *auth, int authlen) +{ + int aucoctsz, a2sz, puoctsz, a3sz, bodysz, total; + uchar *p; + + /* [2] authInfo OCTET STRING */ + aucoctsz = 1 + sizeder(authlen) + authlen; + a2sz = 1 + sizeder(aucoctsz) + aucoctsz; + /* [3] pubKeyAuth OCTET STRING */ + puoctsz = 1 + sizeder(pubkeylen) + pubkeylen; + a3sz = 1 + sizeder(puoctsz) + puoctsz; + /* [0] version: 5 bytes */ + bodysz = 5 + a2sz + a3sz; + total = 1 + sizeder(bodysz) + bodysz; + + if(total > nbuf){ + werrstr("mktsreqE: buffer too small (%d < %d)", nbuf, total); + return -1; + } + + p = buf; + *p++ = BerConstructed|TagSeq; p = putder(p, bodysz); + /* version [0] */ + *p++ = BerContext|TSSnegoToken; *p++ = 0x03; /* len */ + *p++ = TagInt; p = putder(p, 1); *p++ = CredSSPVer; + /* authInfo [2] */ + *p++ = BerContext|TSSauthInfo; p = putder(p, aucoctsz); + *p++ = TagOctetString; p = putder(p, authlen); + memmove(p, auth, authlen); p += authlen; + /* pubKeyAuth [3] */ + *p++ = BerContext|TSSpubKeyAuth; p = putder(p, puoctsz); + *p++ = TagOctetString; p = putder(p, pubkeylen); + memmove(p, pubkey, pubkeylen); p += pubkeylen; + + return p - buf; +} + +/* + * ASN.1: + * TSRequest ::= SEQUENCE { + * version [0] INTEGER, + * negoTokens [1] SEQUENCE OF SEQUENCE { [0] OCTET STRING } OPTIONAL, + * ... + * } + */ +static int +mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) +{ + int octetsz, a0toksz, itemsz, datasz, a1sz, bodysz, total; + uchar *p; + + /* OCTET STRING wrapping the NTLM token */ + octetsz = 1 + sizeder(toklen) + toklen; + /* [0] { octet } = negoToken field inside NegoDataItem */ + a0toksz = 1 + sizeder(octetsz) + octetsz; + /* SEQUENCE { a0tok } = NegoDataItem */ + itemsz = 1 + sizeder(a0toksz) + a0toksz; + /* SEQUENCE OF { item } = NegoData */ + datasz = 1 + sizeder(itemsz) + itemsz; + /* [1] { seqdata } = negoTokens field */ + a1sz = 1 + sizeder(datasz) + datasz; + /* [0] { INTEGER CredSSPVer } = version field; always 5 bytes: a0 03 02 01 vv */ + bodysz = 5 + a1sz; + total = 1 + sizeder(bodysz) + bodysz; + + if(total > nbuf){ + werrstr("mktsreq: buffer too small (%d < %d)", nbuf, total); + return -1; + } + + p = buf; + /* TSRequest SEQUENCE */ + *p++ = BerConstructed|TagSeq; p = putder(p, bodysz); + /* version [0] EXPLICIT INTEGER CredSSPVer */ + *p++ = BerContext|TSSnegoToken; *p++ = 0x03; /* len */ + *p++ = TagInt; p = putder(p, 1); *p++ = CredSSPVer; + /* negoTokens [1] EXPLICIT NegoData */ + *p++ = BerContext|TSSnegoTokens; p = putder(p, datasz); + /* NegoData SEQUENCE OF */ + *p++ = BerConstructed|TagSeq; p = putder(p, itemsz); + /* NegoDataItem SEQUENCE */ + *p++ = BerConstructed|TagSeq; p = putder(p, a0toksz); + /* negoToken [0] EXPLICIT OCTET STRING */ + *p++ = BerContext|TSSnegoToken; p = putder(p, octetsz); + *p++ = TagOctetString; p = putder(p, toklen); + memmove(p, tok, toklen); + p += toklen; + + return p - buf; +} + +/* + * Parse TSRequest and return a pointer to the NTLM token in negoTokens[0]. + * Writes the token length to *ntlenp. Returns nil on error. + */ +uchar* +gettsreq(uchar *buf, int n, int *ntlenp) +{ + uchar *p, *ep, *q; + int tag, len; + + p = buf; + ep = buf + n; + + /* TSRequest SEQUENCE */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) + goto bad; + ep = p + len; + + /* walk SEQUENCE body looking for [1] negoTokens */ + while(p < ep){ + if((q = gbtag(p, ep, &tag)) == nil + || (q = gblen(q, ep, &len)) == nil) + goto bad; + if(tag == TSSnegoTokens){ + /* NegoData SEQUENCE OF */ + if((p = gbtag(q, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) + goto bad; + /* NegoDataItem SEQUENCE */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) + goto bad; + /* negoToken [0] */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TSSnegoToken + || (p = gblen(p, ep, &len)) == nil) + goto bad; + /* OCTET STRING */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagOctetString + || (p = gblen(p, ep, &len)) == nil) + goto bad; + *ntlenp = len; + return p; + } + p = q + len; + } +bad: + werrstr("NLA: TSRequest parse error"); + return nil; +} + +/* + * Send a TSRequest wrapping the given NTLM token over the TLS fd. + */ +int +writetsreq(int fd, uchar *tok, int toklen) +{ + uchar buf[4096]; + int n; + + n = mktsreq(buf, sizeof buf, tok, toklen); + if(n < 0) + return -1; + if(write(fd, buf, n) != n){ + werrstr("NLA: write TSRequest: %r"); + return -1; + } + return 0; +} + +/* + * Read one raw TSRequest DER blob from fd. + * Returns the total number of bytes read, or -1 on error. + */ +int +readtsreq(int fd, uchar *buf, int nbuf) +{ + uchar hdr[4]; + int hlen, bodylen, total, n; + + /* Read tag + first length byte */ + n = readn(fd, hdr, 2); + if(n != 2){ + werrstr("NLA: read TSRequest header: %r"); + return -1; + } + if(hdr[0] != (BerConstructed|TagSeq)){ + werrstr("NLA: TSRequest not a SEQUENCE (got 0x%02x)", hdr[0]); + return -1; + } + if(hdr[1] < BerShortMax){ + bodylen = hdr[1]; + hlen = 2; + }else if(hdr[1] == BerLen1){ + if(readn(fd, hdr+2, 1) != 1){ + werrstr("NLA: read TSRequest length: %r"); + return -1; + } + bodylen = hdr[2]; + hlen = 3; + }else if(hdr[1] == BerLen2){ + if(readn(fd, hdr+2, 2) != 2){ + werrstr("NLA: read TSRequest length: %r"); + return -1; + } + bodylen = (hdr[2]<<8)|hdr[3]; + hlen = 4; + }else{ + werrstr("NLA: bad TSRequest length form 0x%02x", hdr[1]); + return -1; + } + total = hlen + bodylen; + if(total > nbuf){ + werrstr("NLA: TSRequest too large (%d)", total); + return -1; + } + memmove(buf, hdr, hlen); + n = readn(fd, buf+hlen, bodylen); + if(n != bodylen){ + werrstr("NLA: read TSRequest body: %r"); + return -1; + } + return total; +} + +/* + * readtsreq_oreuarp: Read a TSRequest or Early User Authorization Result PDU. + * + * In PROTOCOL_HYBRID_EX mode the server may send a 4-byte EUARP instead of + * (or before) the expected TSRequest. The two are unambiguously distinguished + * by their first byte: a TSRequest is always a BER SEQUENCE (0x30), whereas + * the EUARP authorizationResult is a little-endian ULONG whose low byte is + * 0x00 (ACCESS_GRANTED) or a non-zero error code (ACCESS_DENIED etc.). + * + * Reads exactly 4 bytes first, then: + * - If byte[0] == 0x30: it is a TSRequest; the remaining body is read and + * the full blob is stored in buf. Returns total byte count (> 0). + * - Otherwise: it is an EUARP; *euarp is set to the LE ULONG value. + * Returns 0. + * - Returns -1 on I/O error. + */ +int +readtsreq_oreuarp(int fd, uchar *buf, int nbuf, ulong *euarp) +{ + uchar hdr[4]; + int hlen, bodylen, total, n; + + n = readn(fd, hdr, 4); + if(n != 4){ + werrstr("NLA: read PDU: %r"); + return -1; + } + + if(hdr[0] != (BerConstructed|TagSeq)){ + /* Not a TSRequest SEQUENCE — interpret as EUARP authorizationResult */ + *euarp = GLONG(hdr); + return 0; + } + + /* TSRequest SEQUENCE: finish parsing the length using bytes already in hdr */ + if(hdr[1] < BerShortMax){ + bodylen = hdr[1]; + hlen = 2; + } else if(hdr[1] == BerLen1){ + bodylen = hdr[2]; + hlen = 3; + } else if(hdr[1] == BerLen2){ + bodylen = (hdr[2]<<8)|hdr[3]; + hlen = 4; + } else { + werrstr("NLA: bad TSRequest length form 0x%02x", hdr[1]); + return -1; + } + + total = hlen + bodylen; + if(total < 4){ + werrstr("NLA: TSRequest too short (%d)", total); + return -1; + } + if(total > nbuf){ + werrstr("NLA: TSRequest too large (%d)", total); + return -1; + } + + /* The first 4 bytes are already in hdr; read the remainder */ + memmove(buf, hdr, 4); + if(total > 4){ + n = readn(fd, buf+4, total-4); + if(n != total-4){ + werrstr("NLA: read TSRequest body: %r"); + return -1; + } + } + return total; +} + +/* + * Build NTLM Negotiate message (Type 1). + * This is a minimal negotiate with no domain or workstation names. + */ +int +mkntnego(uchar *buf, int nbuf) +{ + uchar *p; + + if(nbuf < 32){ + werrstr("mkntnego: buffer too small"); + return -1; + } + p = buf; + memmove(p, "NTLMSSP\0", 8); p += 8; + PLONG(p, 1); p += 4; /* MessageType */ + PLONG(p, NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch); p += 4; /* NegotiateFlags */ + memset(p, 0, 8); p += 8; /* DomainNameFields (empty) */ + memset(p, 0, 8); p += 8; /* WorkstationFields (empty) */ + return p - buf; +} + +/* + * Extract the 8-byte server challenge from an NTLM Challenge (Type 2) message. + */ +int +getntchal(uchar challenge[8], uchar *buf, int n) +{ + if(n < 32){ + werrstr("NTLM Challenge: too short (%d)", n); + return -1; + } + if(memcmp(buf, "NTLMSSP\0", 8) != 0){ + werrstr("NTLM Challenge: bad signature"); + return -1; + } + if(GLONG(buf+8) != 2){ + werrstr("NTLM Challenge: bad MessageType (%ld)", (long)GLONG(buf+8)); + return -1; + } + memmove(challenge, buf+24, 8); + return 0; +} + +/* + * Return a pointer into buf at the TargetInfo AvPairs from an NTLM Challenge, + * and set *tilen. Returns nil if absent or malformed. + */ +uchar * +getntargetinfo(uchar *buf, int n, int *tilen) +{ + int len, off; + + if(n < 48) + return nil; + len = GSHORT(buf + 40); + off = (int)GLONG(buf + 44); + if(len <= 0 || off < 32 || off + len > n) + return nil; + *tilen = len; + return buf + off; +} + +/* + * Scan TargetInfo AvPairs for MsvAvTimestamp (AvId=7). + * Returns 1 and fills ts[8] if found, else 0. + */ +static int +getavtimestamp(uchar *ti, int tilen, uchar ts[8]) +{ + uchar *p, *ep; + int avid, avlen; + + p = ti; + ep = ti + tilen; + while(p + 4 <= ep){ + avid = GSHORT(p); + avlen = GSHORT(p + 2); + p += 4; + if(avid == 0) + break; + if(avlen < 0 || p + avlen > ep) + break; + if(avid == 7 && avlen == 8){ + memmove(ts, p, 8); + return 1; + } + p += avlen; + } + return 0; +} + +/* + * Compute the NTLM MsvAvChannelBindings value (16 bytes) from the server's + * TLS certificate per RFC 5929 "tls-server-end-point" channel binding: + * + * hash = SHA-256(server_cert_DER) + * cbstruct = gss_channel_bindings_struct { + * initiator_addrtype = 0 (LE32), + * initiator_address = "" (LE32 len=0), + * acceptor_addrtype = 0 (LE32), + * acceptor_address = "" (LE32 len=0), + * application_data = "tls-server-end-point:" ‖ hash + * with its length as LE32 prefix + * } + * MsvAvChannelBindings = MD5(cbstruct) + * + * If cert is nil or certlen ≤ 0 all-zero SHA-256 is used, yielding a + * well-defined 16-byte value that signals to the server that no binding + * is available. + */ +static void +tlscbind(uchar *cert, int certlen, uchar cbind[MD5dlen]) +{ + uchar hash[SHA2_256dlen]; + static char prefix[] = "tls-server-end-point:"; /* 21 chars (no NUL) */ + /* 20 bytes fixed header + 21-char prefix + 32-byte SHA-256 hash = 73 bytes */ + uchar cbstruct[20 + sizeof prefix - 1 + SHA2_256dlen]; + int applen; + uchar *p; + + if(cert != nil && certlen > 0) + sha2_256(cert, certlen, hash, nil); + else + memset(hash, 0, SHA2_256dlen); + + applen = (int)(sizeof prefix - 1) + SHA2_256dlen; /* 21 + 32 = 53 */ + memset(cbstruct, 0, 20); /* initiator/acceptor addr types and lengths = 0 */ + p = cbstruct + 16; /* application_data length field at offset 16 */ + p[0] = applen & 0xff; + p[1] = (applen >> 8) & 0xff; + p[2] = (applen >> 16) & 0xff; + p[3] = (applen >> 24) & 0xff; + memmove(cbstruct + 20, prefix, sizeof prefix - 1); + memmove(cbstruct + 20 + (sizeof prefix - 1), hash, SHA2_256dlen); + md5(cbstruct, sizeof cbstruct, cbind, nil); +} + +/* + * Compute NTLMv2 NT and LM challenge responses and the SessionBaseKey. + * + * pass, user, domain — credentials + * svchal — 8-byte server challenge from the NTLM Challenge message + * cchal — 8-byte client challenge (caller-supplied random bytes) + * ti — TargetInfo from the NTLM Challenge (verbatim); nil → empty + * tilen — TargetInfo length; 0 if nil + * ntbuf — output buffer for NtChallengeResponse + * nntbuf — ntbuf size; must be ≥ NTv2RespMax + * lmbuf — output LmChallengeResponse (exactly 24 bytes) + * sesskey — output SessionBaseKey (16 bytes; = KeyExchangeKey for NTLMv2) + * cert — server TLS certificate DER for MsvAvChannelBindings (nil → zeros) + * certlen — cert length (0 if nil) + * spn — service principal name for MsvAvTargetName, e.g. "TERMSRV/host" + * (nil or empty → omit MsvAvTargetName AvPair) + * + * NtChallengeResponse = NtProofStr[16] ‖ Blob[32+mtilen] + * LmChallengeResponse = HMAC_MD5(ResponseKeyNT, svchal‖cchal) ‖ cchal (24 bytes) + * SessionBaseKey = HMAC_MD5(ResponseKeyNT, NtProofStr) + * + * Returns the length of NtChallengeResponse written, or -1 on error. + */ +int +ntv2frompasswd(char *pass, char *user, char *domain, + uchar svchal[8], uchar cchal[8], uchar *ti, int tilen, + uchar *ntbuf, int nntbuf, uchar lmbuf[24], uchar sesskey[MD5dlen], + uchar *cert, int certlen, char *spn) +{ + uchar nthash[MD4dlen], rkey[MD5dlen], ntproofstr[MD5dlen]; + uchar blob[32 + MaxNTLMTargetInfo + MaxNTLMClientAvExtra]; + uchar mti[MaxNTLMTargetInfo + MaxNTLMClientAvExtra]; + uchar cbind[MD5dlen]; + uchar spn16[512]; + uchar unidata[1024], unipass[256], ts[8]; + DigestState *ds; + Rune r; + char *p; + int n, bloblen, mtilen, eol, spn16len; + uchar *w; + + if(tilen > MaxNTLMTargetInfo){ + werrstr("ntv2frompasswd: TargetInfo too large (%d > %d)", tilen, MaxNTLMTargetInfo); + return -1; + } + + /* Compute TLS channel binding and SPN UTF-16 for EPA AvPairs */ + tlscbind(cert, certlen, cbind); + spn16len = 0; + if(spn != nil && spn[0] != '\0') + spn16len = toutf16(spn16, sizeof spn16, spn, strlen(spn)); + + /* + * Build modified TargetInfo with EPA AvPairs inserted before EOL: + * MsvAvFlags (AvId=6) — value=2 signals MIC is present + * MsvAvChannelBindings (AvId=10) — TLS channel binding hash (RFC 5929) + * MsvAvTargetName (AvId=9) — SPN, e.g. "TERMSRV/hostname" (if provided) + * Required by MS-NLMP §3.1.5.1.2.3 when NfESS is negotiated and + * MsvAvTimestamp is present in the challenge TargetInfo. + */ + mtilen = 0; + if(ti != nil && tilen > 0){ + /* Find the EOL AvPair (AvId=0) */ + for(eol = 0; eol + 4 <= tilen; ){ + int avid = (ti[eol+1]<<8)|ti[eol]; + int avlen = (ti[eol+3]<<8)|ti[eol+2]; + if(avid == 0) + break; + if(eol + 4 + avlen > tilen) + break; /* malformed; stop here */ + eol += 4 + avlen; + } + memmove(mti, ti, eol); + w = mti + eol; + /* MsvAvFlags (AvId=6, AvLen=4): value=2 = MIC present */ + w[0]=6; w[1]=0; w[2]=4; w[3]=0; + w[4]=2; w[5]=0; w[6]=0; w[7]=0; + w += 8; + /* MsvAvChannelBindings (AvId=10, AvLen=16): TLS channel binding hash */ + w[0]=10; w[1]=0; w[2]=MD5dlen; w[3]=0; + memmove(w+4, cbind, MD5dlen); + w += 4 + MD5dlen; + /* MsvAvTargetName (AvId=9): SPN in UTF-16LE, e.g. "TERMSRV/hostname" */ + if(spn16len > 0){ + w[0]=9; w[1]=0; + w[2]=spn16len & 0xff; w[3]=(spn16len>>8) & 0xff; + memmove(w+4, spn16, spn16len); + w += 4 + spn16len; + } + /* MsvAvEOL (AvId=0, AvLen=0) */ + w[0]=0; w[1]=0; w[2]=0; w[3]=0; + w += 4; + mtilen = (int)(w - mti); + } + + bloblen = 28 + mtilen; + if(MD5dlen + bloblen > nntbuf){ + werrstr("ntv2frompasswd: NT response buffer too small"); + return -1; + } + + /* NT hash = MD4(UNICODE(pass)) */ + n = strlen(pass); + if(n > 128) + n = 128; + w = unipass; + for(p = pass; p < pass+n; ){ + p += chartorune(&r, p); + *w++ = r & 0xff; + *w++ = r >> 8; + } + md4(unipass, w - unipass, nthash, nil); + + /* ResponseKeyNT = HMAC_MD5(nthash, UNICODE(uppercase(user)) ‖ UNICODE(domain)) */ + w = unidata; + n = strlen(user); + for(p = user; p < user+n; ){ + p += chartorune(&r, p); + r = toupperrune(r); + *w++ = r & 0xff; + *w++ = r >> 8; + } + n = strlen(domain); + for(p = domain; p < domain+n; ){ + p += chartorune(&r, p); + *w++ = r & 0xff; + *w++ = r >> 8; + } + hmac_md5(unidata, w - unidata, nthash, MD4dlen, rkey, nil); + + /* + * Build the NTLMv2 blob (MS-NLMP §3.3.2), 28-byte fixed header: + * [0] RespType = 0x01 + * [1] HiRespType = 0x01 + * [2-3] Reserved1 (2 zero bytes) + * [4-7] Reserved2 (4 zero bytes) + * [8-15] Timestamp (MsvAvTimestamp from Challenge, or zeros) + * [16-23] ClientChallenge + * [24-27] Reserved3 (4 zero bytes) + * [28..] TargetInfo (modified AvPairs ending with MsvAvEOL) + * Total blob length = 28 + mtilen (no trailing padding after EOL). + */ + memset(blob, 0, bloblen); + blob[0] = 0x01; + blob[1] = 0x01; + if(!getavtimestamp(ti, tilen, ts)) + memset(ts, 0, 8); + memmove(blob + 8, ts, 8); + memmove(blob + 16, cchal, 8); + if(mtilen > 0) + memmove(blob + 28, mti, mtilen); + + /* NtProofStr = HMAC_MD5(ResponseKeyNT, svchal ‖ blob) */ + ds = hmac_md5(svchal, 8, rkey, MD5dlen, nil, nil); + hmac_md5(blob, bloblen, rkey, MD5dlen, ntproofstr, ds); + + /* ExportedSessionKey = HMAC_MD5(ResponseKeyNT, NtProofStr) */ + hmac_md5(ntproofstr, MD5dlen, rkey, MD5dlen, sesskey, nil); + + /* NtChallengeResponse = NtProofStr ‖ blob */ + memmove(ntbuf, ntproofstr, MD5dlen); + memmove(ntbuf + MD5dlen, blob, bloblen); + + /* LmChallengeResponse = HMAC_MD5(ResponseKeyNT, svchal‖cchal) ‖ cchal */ + ds = hmac_md5(svchal, 8, rkey, MD5dlen, nil, nil); + hmac_md5(cchal, 8, rkey, MD5dlen, lmbuf, ds); + memmove(lmbuf + 16, cchal, 8); + + return MD5dlen + bloblen; +} + +int +mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntresplen, uchar *lmresp, uchar *eskresp) +{ + uchar dom16[512], usr16[512]; + int domlen, usrlen; + int domoff, usroff, lmoff, ntoff, eskoff; + int lmlen, esklen, total; + uchar *p; + + domlen = toutf16(dom16, sizeof dom16, domain, strlen(domain)); + usrlen = toutf16(usr16, sizeof usr16, user, strlen(user)); + + lmlen = NTRespLen; /* LmChallengeResponse is always 24 bytes */ + esklen = (eskresp != nil) ? MD5dlen : 0; + domoff = 88; /* 64 fixed + 8 Version + 16 MIC */ + usroff = domoff + domlen; + lmoff = usroff + usrlen; + ntoff = lmoff + lmlen; + eskoff = ntoff + ntresplen; + total = eskoff + esklen; + + if(total > nbuf){ + werrstr("mkntauth: buffer too small (%d < %d)", nbuf, total); + return -1; + } + + p = buf; + memmove(p, "NTLMSSP\0", 8); p += 8; + PLONG(p, 3); p += 4; /* MessageType */ + + /* LmChallengeResponseFields */ + PSHORT(p, lmlen); + PSHORT(p+2, lmlen); + PLONG(p+4, lmoff); + p += 8; + + /* NtChallengeResponseFields */ + PSHORT(p, ntresplen); + PSHORT(p+2, ntresplen); + PLONG(p+4, ntoff); + p += 8; + + /* DomainNameFields */ + PSHORT(p, domlen); + PSHORT(p+2, domlen); + PLONG(p+4, domoff); + p += 8; + + /* UserNameFields */ + PSHORT(p, usrlen); + PSHORT(p+2, usrlen); + PLONG(p+4, usroff); + p += 8; + + /* WorkstationFields (empty) */ + PSHORT(p, 0); + PSHORT(p+2, 0); + PLONG(p+4, lmoff); + p += 8; + + /* EncryptedRandomSessionKeyFields */ + PSHORT(p, esklen); + PSHORT(p+2, esklen); + PLONG(p+4, eskoff); + p += 8; + + /* NegotiateFlags: NfESS echoed from challenge; NfKeyExch signals EncryptedRandomSessionKey */ + PLONG(p, NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch); p += 4; + + /* Version (8 zero bytes, offset 64; NTLMSSP_NEGOTIATE_VERSION not advertised) */ + memset(p, 0, 8); p += 8; + + /* + * MIC (16 zero bytes, offset 72; caller fills via HMAC_MD5 for NTLMv2). + * Must be zeroed here so the caller can compute HMAC over the full message + * before writing the real value into buf+72. + */ + memset(p, 0, 16); p += 16; + + /* payload */ + memmove(p, dom16, domlen); p += domlen; /* DomainName */ + memmove(p, usr16, usrlen); p += usrlen; /* UserName */ + if(lmresp != nil) + memmove(p, lmresp, lmlen); /* LmChallengeResponse */ + else + memset(p, 0, lmlen); /* fallback: all zeros */ + p += lmlen; + memmove(p, ntresp, ntresplen); p += ntresplen; /* NtChallengeResponse */ + if(eskresp != nil){ + memmove(p, eskresp, MD5dlen); p += MD5dlen; /* EncryptedRandomSessionKey */ + } + + return p - buf; +} + +/* + * Write Phase A TSRequest (NTLM Negotiate + CredSSP v5 clientNonce) to fd. + */ +int +writetsreqnonce(int fd, uchar *tok, int toklen, uchar *nonce, int noncelen) +{ +uchar buf[4096]; +int n; + +n = mktsreqA(buf, sizeof buf, tok, toklen, nonce, noncelen); +if(n < 0) +return -1; +if(write(fd, buf, n) != n){ +werrstr("NLA: write TSRequest (phase A): %r"); +return -1; +} +return 0; +} + +/* + * Write Phase E TSRequest (pubKeyAuth + authInfo) to fd. + */ +int +writetsreqdone(int fd, uchar *pubkey, int pubkeylen, uchar *auth, int authlen) +{ +uchar buf[8192]; +int n; + +n = mktsreqE(buf, sizeof buf, pubkey, pubkeylen, auth, authlen); +if(n < 0) +return -1; +if(write(fd, buf, n) != n){ +werrstr("NLA: write TSRequest (phase E): %r"); +return -1; +} +return 0; +} + +/* + * Complete the CredSSP v5 handshake (Phases D and E). + * Phase D: read the server's TSRequest containing pubKeyAuth. + * Phase E: send TSRequest with client pubKeyAuth + encrypted TSCredentials. + * + * fd - TLS file descriptor + * cert - server's TLS certificate DER (for pubKeyAuth channel binding) + * certlen - length of cert + * cnonce - 32-byte client nonce sent in Phase A (CredSSP v5) + * dom - Windows domain (for TSCredentials) + * user - username (for TSCredentials) + * pass - plaintext password (for TSCredentials delegation) + * sesskey - NTLMv2 ExportedSessionKey computed during Phase C + */ +int +nlafinish(int fd, uchar *cert, int certlen, uchar *cnonce, + char *dom, char *user, char *pass, uchar sesskey[MD5dlen]) +{ +uchar tsreqbuf[4096]; +uchar signkey[MD5dlen], sealkey[MD5dlen]; +uchar creds[2048], sealcreds[2048+16]; +uchar pubkeyauth[SHA2_256dlen]; +uchar *spki; +int n, spkilen, pubkeyauthlen; + +/* Phase D: read server's pubKeyAuth TSRequest */ +fprint(2, "nla: reading Phase D (server pubKeyAuth)\n"); +n = readtsreq(fd, tsreqbuf, sizeof tsreqbuf); +if(n < 0) +return -1; +fprint(2, "nla: Phase D received (%d bytes)\n", n); + +/* Derive SignKey and SealKey from ExportedSessionKey (computed during Phase C) */ +fprint(2, "nla: ExportedSessionKey (for sign/seal):"); +for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", sesskey[n]); +fprint(2, "\n"); +ntlmkeys(sesskey, signkey, sealkey); +fprint(2, "nla: signKey:"); +for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", signkey[n]); +fprint(2, "\n"); + +/* Extract SubjectPublicKeyInfo from server's TLS certificate */ +fprint(2, "nla: extracting server SubjectPublicKeyInfo (certlen=%d)\n", certlen); +spki = nil; spkilen = 0; +if(cert != nil && certlen > 0) +spki = certspki(cert, certlen, &spkilen); +if(spki == nil || spkilen <= 0){ +werrstr("NLA: cannot extract server public key from TLS certificate"); +return -1; +} +fprint(2, "nla: SPKI (%d bytes):", spkilen); +for(n = 0; n < spkilen; n++) fprint(2, " %02ux", spki[n]); +fprint(2, "\n"); +fprint(2, "nla: clientNonce:"); +for(n = 0; n < 32; n++) fprint(2, " %02ux", cnonce[n]); +fprint(2, "\n"); + +/* + * CredSSP v5 Phase E pubKeyAuth (MS-CSSP §3.1.5.1.1.1): + * ClientServerHashKey = HMAC_SHA256(sesskey, "CredSSP Client-To-Server Binding Hash\0") + * pubKeyAuth = HMAC_SHA256(ClientServerHashKey, cnonce || SPKI) + */ +pubkeyauthlen = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, sesskey, cnonce, spki, spkilen); +if(pubkeyauthlen < 0) +return -1; +fprint(2, "nla: pubKeyAuth:"); +for(n = 0; n < pubkeyauthlen; n++) fprint(2, " %02ux", pubkeyauth[n]); +fprint(2, "\n"); + +n = mktscreds(creds, sizeof creds, dom, user, pass); +if(n < 0) +return -1; +fprint(2, "nla: TSCredentials encoded (%d bytes)\n", n); + +/* authInfo uses seqno=0 in CredSSP v5 (pubKeyAuth is HMAC, not EncryptMessage) */ +n = ntlmseal(sealcreds, sizeof sealcreds, signkey, sealkey, 0, creds, n); +if(n < 0) +return -1; +fprint(2, "nla: authInfo sealed (%d bytes)\n", n); + +/* Phase E: send pubKeyAuth + authInfo (encrypted TSCredentials) */ +fprint(2, "nla: sending Phase E (pubKeyAuth + authInfo)\n"); +n = writetsreqdone(fd, pubkeyauth, pubkeyauthlen, sealcreds, n); +fprint(2, "nla: Phase E sent (result=%d)\n", n); +return n; +} diff --git a/nla_test.c b/nla_test.c new file mode 100644 index 0000000..44e5430 --- /dev/null +++ b/nla_test.c @@ -0,0 +1,242 @@ +#include "nla.c" + +int nlatests(void); + +/* + * testmkntnego: NTLM Negotiate (Type 1) message must be 32 bytes with + * correct signature, message type, and negotiate flags. + */ +static int +testmkntnego(void) +{ + uchar buf[64]; + int n; + + n = mkntnego(buf, sizeof buf); + if(n != 32) + sysfatal("testmkntnego: want 32 bytes, got %d", n); + if(memcmp(buf, "NTLMSSP\0", 8) != 0) + sysfatal("testmkntnego: bad signature"); + if(GLONG(buf+8) != 1) + sysfatal("testmkntnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+12) != (NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch)) + sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", + NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch, (ulong)GLONG(buf+12)); + return 0; +} + +/* + * testmkntnegosmall: buffer too small must return an error. + */ +static int +testmkntnegosmall(void) +{ + uchar buf[16]; + int n; + + n = mkntnego(buf, sizeof buf); + if(n >= 0) + sysfatal("testmkntnegosmall: expected error, got %d", n); + return 0; +} + +/* + * testgetntchal: extract the 8-byte challenge from a minimal but valid + * NTLM Challenge (Type 2) message. + */ +static int +testgetntchal(void) +{ + uchar msg[48]; + uchar challenge[8]; + uchar wantchal[8] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + int n; + + memset(msg, 0, sizeof msg); + memmove(msg, "NTLMSSP\0", 8); + PLONG(msg+8, 2); /* MessageType = 2 */ + memmove(msg+24, wantchal, 8); /* ServerChallenge */ + + n = getntchal(challenge, msg, sizeof msg); + if(n < 0) + sysfatal("testgetntchal: unexpected error"); + if(memcmp(challenge, wantchal, 8) != 0) + sysfatal("testgetntchal: challenge mismatch"); + return 0; +} + +/* + * testgetntchalshort: message shorter than 32 bytes must return an error. + */ +static int +testgetntchalshort(void) +{ + uchar msg[10]; + uchar challenge[8]; + int n; + + memset(msg, 0, sizeof msg); + n = getntchal(challenge, msg, sizeof msg); + if(n >= 0) + sysfatal("testgetntchalshort: expected error, got %d", n); + return 0; +} + +/* + * testgetntchalbadsig: wrong signature must return an error. + */ +static int +testgetntchalbadsig(void) +{ + uchar msg[48]; + uchar challenge[8]; + int n; + + memset(msg, 0, sizeof msg); + memmove(msg, "BADMSSSP", 8); + PLONG(msg+8, 2); + + n = getntchal(challenge, msg, sizeof msg); + if(n >= 0) + sysfatal("testgetntchalbadsig: expected error, got %d", n); + return 0; +} + +/* + * testgetntchalbadtype: MessageType != 2 must return an error. + */ +static int +testgetntchalbadtype(void) +{ + uchar msg[48]; + uchar challenge[8]; + int n; + + memset(msg, 0, sizeof msg); + memmove(msg, "NTLMSSP\0", 8); + PLONG(msg+8, 1); /* Type 1, not 2 */ + + n = getntchal(challenge, msg, sizeof msg); + if(n >= 0) + sysfatal("testgetntchalbadtype: expected error, got %d", n); + return 0; +} + +/* + * testmktsreqhdr: verify the exact DER encoding of a TSRequest wrapping a + * single-byte NTLM token (0xAA). + * + * 30 10 SEQUENCE(16) + * a0 03 02 01 05 [0] version=5 (CredSSPVer) + * a1 09 [1] negoTokens(9) + * 30 07 SEQUENCE OF(7) + * 30 05 SEQUENCE(5) + * a0 03 [0](3) + * 04 01 AA OCTET STRING{AA} + */ +static int +testmktsreqhdr(void) +{ + uchar token[] = {0xAA}; + uchar want[] = { + 0x30, 0x10, + 0xa0, 0x03, 0x02, 0x01, CredSSPVer, + 0xa1, 0x09, + 0x30, 0x07, + 0x30, 0x05, + 0xa0, 0x03, + 0x04, 0x01, 0xAA, + }; + uchar buf[64]; + int n; + + n = mktsreq(buf, sizeof buf, token, sizeof token); + if(n != (int)sizeof want) + sysfatal("testmktsreqhdr: len: want %d, got %d", (int)sizeof want, n); + if(memcmp(buf, want, sizeof want) != 0) + sysfatal("testmktsreqhdr: bytes mismatch"); + return 0; +} + +/* + * testmktsreqround: round-trip encode/decode must recover the original token. + */ +static int +testmktsreqround(void) +{ + uchar token[8] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22}; + uchar buf[256]; + uchar *outp; + int n, outlen; + + n = mktsreq(buf, sizeof buf, token, sizeof token); + if(n < 0) + sysfatal("testmktsreqround: mktsreq failed"); + outp = gettsreq(buf, n, &outlen); + if(outp == nil) + sysfatal("testmktsreqround: gettsreq failed"); + if(outlen != (int)sizeof token) + sysfatal("testmktsreqround: outlen: want %d, got %d", + (int)sizeof token, outlen); + if(memcmp(outp, token, sizeof token) != 0) + sysfatal("testmktsreqround: token mismatch"); + return 0; +} + +/* + * testmktsreqsmallbuf: buffer too small must return an error. + */ +static int +testmktsreqsmallbuf(void) +{ + uchar token[8] = {0}; + uchar buf[4]; + int n; + + n = mktsreq(buf, sizeof buf, token, sizeof token); + if(n >= 0) + sysfatal("testmktsreqsmallbuf: expected error, got %d", n); + return 0; +} + +/* + * testmkntauth: NTLM Authenticate (Type 3) message must have correct + * signature, message type, and negotiate flags at the expected offsets. + */ +static int +testmkntauth(void) +{ + uchar ntresp[NTRespLen]; + uchar buf[640]; + int n; + + memset(ntresp, 0x55, NTRespLen); + n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, NTRespLen, nil, nil); + if(n < 0) + sysfatal("testmkntauth: unexpected error"); + if(n < 88) + sysfatal("testmkntauth: message too short (%d)", n); + if(memcmp(buf, "NTLMSSP\0", 8) != 0) + sysfatal("testmkntauth: bad signature"); + if(GLONG(buf+8) != 3) + sysfatal("testmkntauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+60) != (NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch)) + sysfatal("testmkntauth: bad NegotiateFlags"); + return 0; +} + +int +nlatests(void) +{ + testmkntnego(); + testmkntnegosmall(); + testgetntchal(); + testgetntchalshort(); + testgetntchalbadsig(); + testgetntchalbadtype(); + testmktsreqhdr(); + testmktsreqround(); + testmktsreqsmallbuf(); + testmkntauth(); + return 0; +} diff --git a/rd-thread.c b/rd-thread.c index a32648f..fbfe1d3 100644 --- a/rd-thread.c +++ b/rd-thread.c @@ -31,7 +31,7 @@ static void snarfthread(void*); static void usage(void) { - fprint(2, "usage: rd [-0A] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-n term] [-s shell] [net!]server[!port]\n"); + fprint(2, "usage: rd [-0AN] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-n term] [-s shell] [net!]server[!port]\n"); threadexitsall("usage"); } @@ -39,20 +39,23 @@ void threadmain(int argc, char *argv[]) { int doauth; - char *server, *addr, *keyspec; + char *server, *addr; UserPasswd *creds; Rdp* c; c = &conn; - keyspec = ""; + c->keyspec = ""; doauth = 1; ARGBEGIN{ case 'A': doauth = 0; break; + case 'N': + c->nla = 1; + break; case 'k': - keyspec = EARGF(usage()); + c->keyspec = EARGF(usage()); break; case 'T': c->label = strdup(EARGF(usage())); @@ -87,8 +90,8 @@ threadmain(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - if(doauth){ - creds = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=rdp %s", keyspec); + if(doauth && !c->nla){ + creds = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=rdp %s", c->keyspec); if(creds == nil) fprint(2, "factotum: %r\n"); else { diff --git a/rd.c b/rd.c index eab3acd..136474c 100644 --- a/rd.c +++ b/rd.c @@ -19,7 +19,7 @@ void sendmouse(Rdp* c, Mouse m); static void usage(void) { - fprint(2, "usage: rd [-0A] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-s shell] [net!]server[!port]\n"); + fprint(2, "usage: rd [-0AN] [-T title] [-a depth] [-c wdir] [-d dom] [-k keyspec] [-s shell] [net!]server[!port]\n"); exits("usage"); } @@ -142,20 +142,23 @@ void main(int argc, char *argv[]) { int doauth; - char *server, *addr, *keyspec; + char *server, *addr; UserPasswd *creds; Rdp* c; c = &conn; - keyspec = ""; + c->keyspec = ""; doauth = 1; ARGBEGIN{ case 'A': doauth = 0; break; + case 'N': + c->nla = 1; + break; case 'k': - keyspec = EARGF(usage()); + c->keyspec = EARGF(usage()); break; case 'T': c->label = strdup(EARGF(usage())); @@ -183,6 +186,7 @@ main(int argc, char *argv[]) usage(); server = argv[0]; + c->server = server; c->local = getenv("sysname"); c->user = getenv("user"); @@ -190,8 +194,8 @@ main(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - if(doauth){ - creds = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", keyspec); + if(doauth && !c->nla){ + creds = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); if(creds == nil) fprint(2, "factotum: %r\n"); else { diff --git a/rpc.c b/rpc.c index 56b8b63..14515e1 100644 --- a/rpc.c +++ b/rpc.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include "dat.h" #include "fns.h" @@ -21,7 +23,9 @@ x224handshake(Rdp* c) Msg t, r; t.type = Xconnect; - t.negproto = ProtoTLS; + /* advertise HYBRID_EX (ProtoUAUTH) alongside HYBRID so the server + * may send the Early User Authorization Result PDU ([MS-RDPBCGR] 2.2.1.14) */ + t.negproto = c->nla ? (ProtoCSSP | ProtoUAUTH) : ProtoTLS; if(writemsg(c, &t) <= 0) return -1; if(readmsg(c, &r) <= 0) @@ -30,18 +34,277 @@ x224handshake(Rdp* c) werrstr("X.224: protocol botch"); return -1; } - if((r.negproto&ProtoTLS) == 0){ - werrstr("server refused STARTTLS"); - return -1; + if(c->nla){ + /* server may select HYBRID (ProtoCSSP) or HYBRID_EX (ProtoUAUTH) */ + if((r.negproto & (ProtoCSSP | ProtoUAUTH)) == 0){ + werrstr("server refused CredSSP"); + return -1; + } + }else{ + if((r.negproto&ProtoTLS) == 0){ + werrstr("server refused STARTTLS"); + return -1; + } } c->sproto = r.negproto; if(starttls(c) < 0) return -1; + if(c->nla && nlahandshake(c) < 0) + return -1; + + /* [MS-RDPBCGR] 2.2.1.14: Early User Authorization Result PDU — + * server sends a 4-byte authorizationResult immediately after the + * CredSSP handshake if (and only if) client advertised HYBRID_EX */ + if(c->nla && (c->sproto & ProtoUAUTH)){ + uchar authbuf[4]; + ulong authresult; + if(readn(c->fd, authbuf, 4) != 4){ + werrstr("NLA: read Early User Authorization Result: %r"); + return -1; + } + authresult = GLONG(authbuf); + fprint(2, "nla: Early User Authorization Result: %08lux\n", authresult); + if(authresult != 0){ + werrstr("NLA: server denied access (authorizationResult=%08lux)", authresult); + return -1; + } + } return 0; } +int +nlahandshake(Rdp *c) +{ + uchar ntnego[64], ntauth[2048]; + uchar challenge[8], tsreqbuf[4096]; + uchar cchal[8]; /* NTLMv2 client challenge */ + uchar lmv2resp[24]; /* NTLMv2 LmChallengeResponse */ + uchar ntv2resp[NTv2RespMax]; /* NTLMv2 NtChallengeResponse */ + uchar ntresp[64]; /* factotum mschap NTLMv1 fallback */ + uchar sesskey[MD5dlen]; /* NTLMv2 SessionBaseKey (= KeyExchangeKey) */ + uchar exportedsk[MD5dlen]; /* random ExportedSessionKey (used for sign/seal) */ + uchar eskresp[MD5dlen]; /* EncryptedRandomSessionKey = RC4K(sesskey, exportedsk) */ + RC4state rc4st; + uchar cnonce[32]; /* CredSSP v5 client nonce */ + char user[256], domfromchal[256], pass[256], *dom; + uchar *ntp, *ti; + int n, ntlen, ntv2len, nresp, tilen, i, tlen, toff, nnego; + UserPasswd *up; + + ntv2len = 0; + memset(sesskey, 0, sizeof sesskey); + memset(exportedsk, 0, sizeof exportedsk); + + /* Phase A: NTLM Negotiate (CredSSP v5, with clientNonce) */ + fprint(2, "nla: sending Phase A (NTLM Negotiate)\n"); + n = mkntnego(ntnego, sizeof ntnego); + if(n < 0) + return -1; + nnego = n; /* save for MIC computation */ + genrandom(cnonce, sizeof cnonce); + if(writetsreqnonce(c->fd, ntnego, n, cnonce, sizeof cnonce) < 0) + return -1; + fprint(2, "nla: Phase A sent (%d byte token)\n", n); + + /* Phase B: NTLM Challenge. + * In PROTOCOL_HYBRID_EX mode the server may send a 4-byte Early User + * Authorization Result PDU here instead of a TSRequest, e.g. to deny + * access before the full CredSSP exchange completes. */ + fprint(2, "nla: reading Phase B (NTLM Challenge)\n"); + if(c->sproto & ProtoUAUTH){ + ulong euarp; + n = readtsreq_oreuarp(c->fd, tsreqbuf, sizeof tsreqbuf, &euarp); + if(n == 0){ + fprint(2, "nla: Early User Authorization Result at Phase B: %08lux\n", euarp); + werrstr("NLA: server denied access (EUARP authorizationResult=%08lux)", euarp); + return -1; + } + } else { + n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); + } + if(n < 0) + return -1; + ntp = gettsreq(tsreqbuf, n, &ntlen); + if(ntp == nil) + return -1; + if(getntchal(challenge, ntp, ntlen) < 0) + return -1; + + fprint(2, "nla: Phase B received (%d byte TSRequest, %d byte token)\n", n, ntlen); + /* Debug: dump the raw NTLM Challenge packet */ + fprint(2, "ntlm challenge (%d bytes):", ntlen); + for(i = 0; i < ntlen; i++) + fprint(2, " %02ux", ntp[i]); + fprint(2, "\n"); + + /* Extract TargetInfo (needed for NTLMv2 blob and timestamp) */ + ti = getntargetinfo(ntp, ntlen, &tilen); + if(ti == nil) + tilen = 0; + + /* Use TargetName from Challenge as domain if none was specified */ + dom = c->windom; + if(*dom == '\0' && ntlen >= 20){ + tlen = GSHORT(ntp+12); + toff = GLONG(ntp+16); + if(tlen > 0 && toff >= 0 && tlen <= ntlen - toff){ + n = fromutf16(domfromchal, sizeof(domfromchal)-1, ntp+toff, tlen); + domfromchal[n] = '\0'; + dom = domfromchal; + } + } + + /* + * Get password for NT response and credential delegation. + * Try proto=pass first, then the -p flag. + * If a password is available we compute the NT response directly + * (DESL(MD4(UNICODE(pass)), chal)) which avoids needing a separate + * proto=mschap key in factotum. If no password, fall back to + * auth_respond with proto=mschap. + */ + fprint(2, "nla: retrieving password (keyspec=%s)\n", c->keyspec); + user[0] = '\0'; + pass[0] = '\0'; + up = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); + if(up != nil){ + if(up->user != nil) + snprint(user, sizeof user, "%s", up->user); + if(up->passwd != nil) + snprint(pass, sizeof pass, "%s", up->passwd); + free(up); + } + if(pass[0] == '\0' && c->passwd != nil && c->passwd[0] != '\0'){ + snprint(pass, sizeof pass, "%s", c->passwd); + if(user[0] == '\0' && c->user != nil && c->user[0] != '\0') + snprint(user, sizeof user, "%s", c->user); + } + + if(pass[0] != '\0'){ + /* Compute NTLMv2 NT and LM responses from password (MS-NLMP §3.3.2) */ + fprint(2, "nla: computing NTLMv2 response from password (user=%s, dom=%s)\n", user, dom); + genrandom(cchal, sizeof cchal); + { + /* + * Build SPN "TERMSRV/" for MsvAvTargetName. + * Strip any "net!" prefix and "!port" suffix from c->server + * (Plan 9 dial address format is "net!host!port"). + */ + char spnbuf[280]; + char *h, *bang; + h = (c->server != nil) ? c->server : ""; + if((bang = strchr(h, '!')) != nil) + h = bang + 1; /* skip "tcp!" or other network prefix */ + if((bang = strchr(h, '!')) != nil) + snprint(spnbuf, sizeof spnbuf, "TERMSRV/%.*s", (int)(bang - h), h); + else + snprint(spnbuf, sizeof spnbuf, "TERMSRV/%s", h); + fprint(2, "nla: SPN for MsvAvTargetName: %s\n", spnbuf); + ntv2len = ntv2frompasswd(pass, user, dom, + challenge, cchal, ti, tilen, + ntv2resp, sizeof ntv2resp, lmv2resp, sesskey, + c->tlscert, c->tlscertlen, spnbuf); + } + if(ntv2len < 0) + return -1; + fprint(2, "nla: SessionBaseKey (KeyExchangeKey):"); + for(i = 0; i < MD5dlen; i++) fprint(2, " %02ux", sesskey[i]); + fprint(2, "\n"); + /* + * Generate a random ExportedSessionKey and compute EncryptedRandomSessionKey + * = RC4K(SessionBaseKey, ExportedSessionKey) per MS-NLMP §3.1.5.1.2.3. + * The ExportedSessionKey is used for sign/seal key derivation and CredSSP hashes. + */ + genrandom(exportedsk, MD5dlen); + memmove(eskresp, exportedsk, MD5dlen); + setupRC4state(&rc4st, sesskey, MD5dlen); + rc4(&rc4st, eskresp, MD5dlen); + fprint(2, "nla: ExportedSessionKey (random):"); + for(i = 0; i < MD5dlen; i++) fprint(2, " %02ux", exportedsk[i]); + fprint(2, "\n"); + fprint(2, "nla: EncryptedRandomSessionKey:"); + for(i = 0; i < MD5dlen; i++) fprint(2, " %02ux", eskresp[i]); + fprint(2, "\n"); + fprint(2, "nla: ntnego (%d bytes):", nnego); + for(i = 0; i < nnego; i++) fprint(2, " %02ux", ntnego[i]); + fprint(2, "\n"); + }else{ + /* Fall back to factotum mschap (NTLMv1; credential delegation will fail) */ + fprint(2, "nla: calling factotum mschap (keyspec=%s, dom=%s)\n", c->keyspec, dom); + nresp = auth_respond(challenge, 8, + user, sizeof(user)-1, + ntresp, sizeof(ntresp), + auth_getkey, + "proto=mschap role=client service=rdp %s", c->keyspec); + if(nresp < 0){ + werrstr("factotum mschap: %r"); + return -1; + } + if(nresp < 2*24){ /* sizeof(MSchapreply) = LMresp[24] + NTresp[24] */ + werrstr("factotum mschap: response too short (%d)", nresp); + return -1; + } + fprint(2, "nla: factotum returned user=%s nresp=%d\n", user, nresp); + } + + /* Propagate user name if not yet set on the connection */ + if(user[0] != '\0' && c->user[0] == '\0'){ + c->user = strdup(user); + if(c->user == nil) + sysfatal("strdup: %r"); + } + + /* Phase C: NTLM Authenticate */ + fprint(2, "nla: sending Phase C (NTLM Authenticate, user=%s, dom=%s)\n", c->user, dom); + if(pass[0] != '\0'){ + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntv2resp, ntv2len, lmv2resp, eskresp); + if(n < 0) + return -1; + /* + * Compute and fill the MIC field at bytes 72–87 of AUTHENTICATE_MESSAGE. + * Required by MS-NLMP §3.1.5.1.2.3 when the Challenge TargetInfo contains + * MsvAvTimestamp (AvId=7), which Windows servers always include. + * MIC = HMAC_MD5(ExportedSessionKey, + * NTLM_Negotiate || NTLM_Challenge || NTLM_Authenticate) + * ntauth[72..87] was zeroed by mkntauth, so HMAC is computed over the + * message as it will appear on the wire (MIC field = 0 during computation). + * ExportedSessionKey is the random key (not the SessionBaseKey). + */ + { + DigestState *mds; + mds = hmac_md5(ntnego, nnego, exportedsk, MD5dlen, nil, nil); + mds = hmac_md5(ntp, ntlen, exportedsk, MD5dlen, nil, mds); + hmac_md5(ntauth, n, exportedsk, MD5dlen, ntauth+72, mds); + } + fprint(2, "nla: MIC:"); + for(i = 0; i < 16; i++) fprint(2, " %02ux", ntauth[72+i]); + fprint(2, "\n"); + fprint(2, "nla: ntauth (%d bytes):", n); + for(i = 0; i < n; i++) fprint(2, " %02ux", ntauth[i]); + fprint(2, "\n"); + } else { + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, 24, nil, nil); + if(n < 0) + return -1; + } + if(writetsreq(c->fd, ntauth, n) < 0) + return -1; + fprint(2, "nla: Phase C sent (%d byte token)\n", n); + + if(pass[0] == '\0'){ + werrstr("NLA: no password for credential delegation; " + "add 'proto=pass service=rdp' key to factotum or use -p"); + return -1; + } + fprint(2, "nla: password obtained (%d chars), calling nlafinish\n", (int)strlen(pass)); + + /* Phases D and E: read server pubKeyAuth, send TSCredentials */ + n = nlafinish(c->fd, c->tlscert, c->tlscertlen, cnonce, dom, c->user, pass, exportedsk); + memset(pass, 0, sizeof pass); + return n; +} + int x224hangup(Rdp* c) { @@ -264,12 +527,20 @@ sendclientinfo(Rdp* c) t.type = Dclientinfo; t.mcsuid = c->mcsuid; - t.dom = c->windom; - t.user = c->user; - t.pass = c->passwd; + if(c->nla){ + /* server already knows credentials from CredSSP exchange */ + t.dom = ""; + t.user = ""; + t.pass = ""; + t.dologin = 0; + }else{ + t.dom = c->windom; + t.user = c->user; + t.pass = c->passwd; + t.dologin = (strlen(c->user) > 0); + } t.rshell = c->shell; t.rwd = c->rwd; - t.dologin = (strlen(c->user) > 0); if(writemsg(c, &t) <= 0) sysfatal("sendclientinfo: %r"); diff --git a/tls.c b/tls.c index 21fd7df..a336817 100644 --- a/tls.c +++ b/tls.c @@ -48,6 +48,13 @@ starttls(Rdp* r) close(sfd); return -1; } + if(c.cert != nil && c.certlen > 0){ + r->tlscert = malloc(c.certlen); + if(r->tlscert != nil){ + memmove(r->tlscert, c.cert, c.certlen); + r->tlscertlen = c.certlen; + } + } /* BUG: free c.cert? */ close(r->fd); diff --git a/tls9f.c b/tls9f.c index 14461dc..28e2827 100644 --- a/tls9f.c +++ b/tls9f.c @@ -48,6 +48,13 @@ starttls(Rdp* r) close(sfd); return -1; } + if(c.cert != nil && c.certlen > 0){ + r->tlscert = malloc(c.certlen); + if(r->tlscert != nil){ + memmove(r->tlscert, c.cert, c.certlen); + r->tlscertlen = c.certlen; + } + } /* BUG: free c.cert? */ close(r->fd); diff --git a/tlsp9p.c b/tlsp9p.c index 7ccd3d7..28024f7 100644 --- a/tlsp9p.c +++ b/tlsp9p.c @@ -30,6 +30,13 @@ starttls(Rdp* r) close(sfd); return -1; } + if(c.cert != nil && c.certlen > 0){ + r->tlscert = malloc(c.certlen); + if(r->tlscert != nil){ + memmove(r->tlscert, c.cert, c.certlen); + r->tlscertlen = c.certlen; + } + } /* BUG: free c.cert? */ close(r->fd);