From 14d356bf0b35e762df11e30a7efa838eef375cb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:49:15 +0000 Subject: [PATCH 01/45] nla: add CredSSP/NTLM authentication using auth_respond Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/a74b69ad-c93f-4941-b700-c8d1ee0a5f0b Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- dat.h | 2 + fns.h | 3 + mkfile | 1 + nla.c | 479 ++++++++++++++++++++++++++++++++++++++++++++++++++++ rd-thread.c | 8 +- rd.c | 8 +- rpc.c | 17 +- 7 files changed, 510 insertions(+), 8 deletions(-) create mode 100644 nla.c diff --git a/dat.h b/dat.h index c13b812..1484923 100644 --- a/dat.h +++ b/dat.h @@ -50,6 +50,8 @@ struct Rdp char *passwd; /* password for auto logon (sic) */ char *shell; /* remote shell override */ char *rwd; /* remote working directory */ + int nla; /* use NLA (CredSSP/NTLM) authentication */ + char *keyspec; /* factotum key spec */ int xsz; /* rfb dimensions */ int ysz; /* rfb dimensions */ int depth; /* rfb color depth */ diff --git a/fns.h b/fns.h index 6b3840d..675be79 100644 --- a/fns.h +++ b/fns.h @@ -39,6 +39,9 @@ int istpkt(uchar*,uchar*); int tptype(uchar*,uchar*); uchar* tpdat(uchar*,uchar*); +/* nla.c */ +int nlahandshake(Rdp*); + /* rd.c */ void atexitkiller(void); void atexitkill(int pid); diff --git a/mkfile b/mkfile index 5de4bab..dc31f87 100644 --- a/mkfile +++ b/mkfile @@ -21,6 +21,7 @@ OFILES=\ mpas.$O\ mppc.$O\ msg.$O\ + nla.$O\ rd.$O\ rpc.$O\ utf16.$O\ diff --git a/nla.c b/nla.c new file mode 100644 index 0000000..9098b83 --- /dev/null +++ b/nla.c @@ -0,0 +1,479 @@ +/* + * 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. + * The NT response is computed by factotum via auth_respond(2) + * with proto=mschap, so the plaintext password never leaves factotum. + * + * Exchange: + * Client → Server: TSRequest { negoTokens = [NTLM Negotiate] } + * Server → Client: TSRequest { negoTokens = [NTLM Challenge] } + * Client → Server: TSRequest { negoTokens = [NTLM Authenticate] } + */ +#include +#include +#include +#include "dat.h" +#include "fns.h" + +enum +{ + /* NTLM NegotiateFlags (subset used here) */ + NfUnicode = 0x00000001, /* NTLMSSP_NEGOTIATE_UNICODE */ + NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ + NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ + NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ + + NTLMFlags = NfUnicode | NfReqTarget | NfNTLM | NfAlwaysSign, + + /* NTLM response size (NTLMv1) */ + NTRespLen = 24, + + /* CredSSP version advertised in TSRequest */ + CredSSPVer = 5, +}; + +/* + * ASN.1 DER helpers + */ + +static int +szder(int n) +{ + if(n < 0x80) + return 1; + if(n < 0x100) + return 2; + return 3; +} + +static uchar* +putder(uchar *p, int n) +{ + if(n < 0x80){ + *p++ = n; + }else if(n < 0x100){ + *p++ = 0x81; + *p++ = n; + }else{ + *p++ = 0x82; + *p++ = n >> 8; + *p++ = n; + } + return p; +} + +/* parse one TLV; returns pointer past the tag+length (i.e. to the value) */ +static uchar* +getdertlv(uchar *p, uchar *ep, int *tag, int *len) +{ + if(p+2 > ep){ + werrstr("NLA: DER short"); + return nil; + } + *tag = *p++; + if(*p < 0x80){ + *len = *p++; + }else if(*p == 0x81){ + p++; + if(p >= ep){ werrstr("NLA: DER short"); return nil; } + *len = *p++; + }else if(*p == 0x82){ + p++; + if(p+2 > ep){ werrstr("NLA: DER short"); return nil; } + *len = (p[0]<<8)|p[1]; + p += 2; + }else{ + werrstr("NLA: unsupported DER length form %02x", p[-1]); + return nil; + } + return p; +} + +/* + * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=ntlm}] } + * + * ASN.1: + * TSRequest ::= SEQUENCE { + * version [0] INTEGER, + * negoTokens [1] SEQUENCE OF SEQUENCE { [0] OCTET STRING } OPTIONAL, + * ... + * } + */ +static int +mktsreq(uchar *buf, int nbuf, uchar *ntlm, int ntlmlen) +{ + int octetsz, a0toksz, itemsz, datasz, a1sz, bodysz, total; + uchar *p; + + /* OCTET STRING wrapping the NTLM token */ + octetsz = 1 + szder(ntlmlen) + ntlmlen; + /* [0] { octet } = negoToken field inside NegoDataItem */ + a0toksz = 1 + szder(octetsz) + octetsz; + /* SEQUENCE { a0tok } = NegoDataItem */ + itemsz = 1 + szder(a0toksz) + a0toksz; + /* SEQUENCE OF { item } = NegoData */ + datasz = 1 + szder(itemsz) + itemsz; + /* [1] { seqdata } = negoTokens field */ + a1sz = 1 + szder(datasz) + datasz; + /* [0] { INTEGER CredSSPVer } = version field; always 5 bytes: a0 03 02 01 vv */ + bodysz = 5 + a1sz; + total = 1 + szder(bodysz) + bodysz; + + if(total > nbuf){ + werrstr("mktsreq: buffer too small (%d < %d)", nbuf, total); + return -1; + } + + p = buf; + /* TSRequest SEQUENCE */ + *p++ = 0x30; p = putder(p, bodysz); + /* version [0] EXPLICIT INTEGER CredSSPVer */ + *p++ = 0xa0; *p++ = 0x03; + *p++ = 0x02; *p++ = 0x01; *p++ = CredSSPVer; + /* negoTokens [1] EXPLICIT NegoData */ + *p++ = 0xa1; p = putder(p, datasz); + /* NegoData SEQUENCE OF */ + *p++ = 0x30; p = putder(p, itemsz); + /* NegoDataItem SEQUENCE */ + *p++ = 0x30; p = putder(p, a0toksz); + /* negoToken [0] EXPLICIT OCTET STRING */ + *p++ = 0xa0; p = putder(p, octetsz); + *p++ = 0x04; p = putder(p, ntlmlen); + memmove(p, ntlm, ntlmlen); + p += ntlmlen; + + return p - buf; +} + +/* + * Parse TSRequest and extract the NTLM token from negoTokens[0]. + * Sets *ntlmp to point into buf (not a copy) and *ntlmlenp to its length. + */ +static int +gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) +{ + uchar *p, *ep, *q; + int tag, len; + + p = buf; + ep = buf + n; + + /* TSRequest SEQUENCE */ + q = getdertlv(p, ep, &tag, &len); + if(q == nil || tag != 0x30) + goto bad; + p = q; + ep = p + len; + + /* walk SEQUENCE body looking for [1] negoTokens */ + while(p < ep){ + q = getdertlv(p, ep, &tag, &len); + if(q == nil) + goto bad; + if(tag == 0xa1){ + /* [1] NegoData SEQUENCE OF */ + p = q; + ep = p + len; + q = getdertlv(p, ep, &tag, &len); + if(q == nil || tag != 0x30) + goto bad; + p = q; + ep = p + len; + /* NegoDataItem SEQUENCE */ + q = getdertlv(p, ep, &tag, &len); + if(q == nil || tag != 0x30) + goto bad; + p = q; + ep = p + len; + /* negoToken [0] */ + q = getdertlv(p, ep, &tag, &len); + if(q == nil || tag != 0xa0) + goto bad; + p = q; + ep = p + len; + /* OCTET STRING */ + q = getdertlv(p, ep, &tag, &len); + if(q == nil || tag != 0x04) + goto bad; + *ntlmp = q; + *ntlmlenp = len; + return 0; + } + p = q + len; + } +bad: + werrstr("NLA: TSRequest parse error"); + return -1; +} + +/* + * Send a TSRequest wrapping the given NTLM token over the TLS fd. + */ +static int +sendtsreq(int fd, uchar *ntlm, int ntlmlen) +{ + uchar buf[4096]; + int n; + + n = mktsreq(buf, sizeof buf, ntlm, ntlmlen); + 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. + */ +static int +recvtsreq(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] != 0x30){ + werrstr("NLA: TSRequest not a SEQUENCE (got 0x%02x)", hdr[0]); + return -1; + } + if(hdr[1] < 0x80){ + bodylen = hdr[1]; + hlen = 2; + }else if(hdr[1] == 0x81){ + if(readn(fd, hdr+2, 1) != 1){ + werrstr("NLA: read TSRequest length: %r"); + return -1; + } + bodylen = hdr[2]; + hlen = 3; + }else if(hdr[1] == 0x82){ + 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; +} + +/* + * Build NTLM Negotiate message (Type 1). + * This is a minimal negotiate with no domain or workstation names. + */ +static int +mkntlm1(uchar *buf, int nbuf) +{ + uchar *p; + + if(nbuf < 32){ + werrstr("mkntlm1: buffer too small"); + return -1; + } + p = buf; + memmove(p, "NTLMSSP\0", 8); p += 8; + PLONG(p, 1); p += 4; /* MessageType */ + PLONG(p, NTLMFlags); 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. + */ +static int +getntlm2(uchar *buf, int n, uchar challenge[8]) +{ + 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; +} + +/* + * Build NTLM Authenticate message (Type 3). + * + * ntresp must be NTRespLen (24) bytes: the NT response from auth_respond. + * LmChallengeResponse is set to zeros (acceptable for NTLMv1). + * + * Fixed header layout (64 bytes, no Version or MIC fields): + * 0 Signature "NTLMSSP\0" (8) + * 8 MessageType = 3 (4) + * 12 LmChallengeResponseFields (8) + * 20 NtChallengeResponseFields (8) + * 28 DomainNameFields (8) + * 36 UserNameFields (8) + * 44 WorkstationFields (8) + * 52 EncryptedRandomSessionKeyFields (8) + * 60 NegotiateFlags (4) + * + * Payload (at offset 64): DomainName | UserName | LmResponse | NtResponse + */ +static int +mkntlm3(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) +{ + uchar dom16[512], usr16[512]; + int domlen, usrlen; + int domoff, usroff, lmoff, ntoff; + int lmlen, total; + uchar *p; + + domlen = toutf16(dom16, sizeof dom16, domain, strlen(domain)); + usrlen = toutf16(usr16, sizeof usr16, user, strlen(user)); + + lmlen = NTRespLen; /* zeros for LM response */ + domoff = 64; + usroff = domoff + domlen; + lmoff = usroff + usrlen; + ntoff = lmoff + lmlen; + total = ntoff + NTRespLen; + + if(total > nbuf){ + werrstr("mkntlm3: 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); p += 2; + PSHORT(p, lmlen); p += 2; + PLONG(p, lmoff); p += 4; + + /* NtChallengeResponseFields */ + PSHORT(p, NTRespLen); p += 2; + PSHORT(p, NTRespLen); p += 2; + PLONG(p, ntoff); p += 4; + + /* DomainNameFields */ + PSHORT(p, domlen); p += 2; + PSHORT(p, domlen); p += 2; + PLONG(p, domoff); p += 4; + + /* UserNameFields */ + PSHORT(p, usrlen); p += 2; + PSHORT(p, usrlen); p += 2; + PLONG(p, usroff); p += 4; + + /* WorkstationFields (empty) */ + PSHORT(p, 0); p += 2; + PSHORT(p, 0); p += 2; + PLONG(p, lmoff); p += 4; /* offset points to lm area; length is 0 */ + + /* EncryptedRandomSessionKeyFields (empty) */ + PSHORT(p, 0); p += 2; + PSHORT(p, 0); p += 2; + PLONG(p, ntoff+NTRespLen); p += 4; + + /* NegotiateFlags */ + PLONG(p, NTLMFlags); p += 4; + + /* payload */ + memmove(p, dom16, domlen); p += domlen; /* DomainName */ + memmove(p, usr16, usrlen); p += usrlen; /* UserName */ + memset(p, 0, lmlen); p += lmlen; /* LmChallengeResponse */ + memmove(p, ntresp, NTRespLen); p += NTRespLen; /* NtChallengeResponse */ + + return p - buf; +} + +/* + * nlahandshake performs the CredSSP/NTLM authentication exchange after TLS. + * + * Phase A: send NTLM Negotiate wrapped in TSRequest. + * Phase B: receive server's NTLM Challenge in TSRequest; call auth_respond + * (proto=mschap) to get the NT response from factotum. + * Phase C: send NTLM Authenticate with the NT response wrapped in TSRequest. + */ +int +nlahandshake(Rdp *c) +{ + uchar ntlm1[64], tsreqbuf[4096], ntlm3[640]; + uchar challenge[8], ntresp[64]; + char user[256]; + uchar *ntlmp; + int n, ntlmlen, nresp; + + /* Phase A: NTLM Negotiate */ + n = mkntlm1(ntlm1, sizeof ntlm1); + if(n < 0) + return -1; + if(sendtsreq(c->fd, ntlm1, n) < 0) + return -1; + + /* Phase B: NTLM Challenge */ + n = recvtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); + if(n < 0) + return -1; + if(gettsreq(tsreqbuf, n, &ntlmp, &ntlmlen) < 0) + return -1; + if(getntlm2(ntlmp, ntlmlen, challenge) < 0) + return -1; + + /* Ask factotum to compute the NT response for this challenge */ + user[0] = '\0'; + nresp = auth_respond(challenge, 8, + user, sizeof(user)-1, + ntresp, sizeof(ntresp), + auth_getkey, + "proto=mschap service=rdp %s", c->keyspec); + if(nresp < 0){ + werrstr("factotum mschap: %r"); + return -1; + } + if(nresp < NTRespLen){ + werrstr("factotum mschap: response too short (%d)", nresp); + return -1; + } + + /* Use the user name returned by factotum if we don't have one */ + if(user[0] != '\0' && c->user[0] == '\0') + c->user = strdup(user); + + /* Phase C: NTLM Authenticate */ + n = mkntlm3(ntlm3, sizeof ntlm3, c->user, c->windom, ntresp); + if(n < 0) + return -1; + if(sendtsreq(c->fd, ntlm3, n) < 0) + return -1; + + return 0; +} diff --git a/rd-thread.c b/rd-thread.c index a32648f..dfd8a14 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"); } @@ -51,6 +51,9 @@ threadmain(int argc, char *argv[]) case 'A': doauth = 0; break; + case 'N': + c->nla = 1; + break; case 'k': keyspec = EARGF(usage()); break; @@ -87,7 +90,8 @@ threadmain(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - if(doauth){ + c->keyspec = keyspec; + if(doauth && !c->nla){ creds = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=rdp %s", keyspec); if(creds == nil) fprint(2, "factotum: %r\n"); diff --git a/rd.c b/rd.c index eab3acd..c4de36c 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"); } @@ -154,6 +154,9 @@ main(int argc, char *argv[]) case 'A': doauth = 0; break; + case 'N': + c->nla = 1; + break; case 'k': keyspec = EARGF(usage()); break; @@ -190,7 +193,8 @@ main(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - if(doauth){ + c->keyspec = keyspec; + if(doauth && !c->nla){ creds = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", keyspec); if(creds == nil) fprint(2, "factotum: %r\n"); diff --git a/rpc.c b/rpc.c index 56b8b63..d0d141c 100644 --- a/rpc.c +++ b/rpc.c @@ -21,7 +21,7 @@ x224handshake(Rdp* c) Msg t, r; t.type = Xconnect; - t.negproto = ProtoTLS; + t.negproto = c->nla ? ProtoCSSP : ProtoTLS; if(writemsg(c, &t) <= 0) return -1; if(readmsg(c, &r) <= 0) @@ -30,14 +30,23 @@ x224handshake(Rdp* c) werrstr("X.224: protocol botch"); return -1; } - if((r.negproto&ProtoTLS) == 0){ - werrstr("server refused STARTTLS"); - return -1; + if(c->nla){ + if((r.negproto&ProtoCSSP) == 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; return 0; } From 3e6d1bd0082e942902648c991925748b9a93f0a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:50:25 +0000 Subject: [PATCH 02/45] nla: fix nil keyspec guard and strdup error check Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/a74b69ad-c93f-4941-b700-c8d1ee0a5f0b Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/nla.c b/nla.c index 9098b83..74b54eb 100644 --- a/nla.c +++ b/nla.c @@ -454,7 +454,7 @@ nlahandshake(Rdp *c) user, sizeof(user)-1, ntresp, sizeof(ntresp), auth_getkey, - "proto=mschap service=rdp %s", c->keyspec); + "proto=mschap service=rdp %s", c->keyspec != nil ? c->keyspec : ""); if(nresp < 0){ werrstr("factotum mschap: %r"); return -1; @@ -465,8 +465,13 @@ nlahandshake(Rdp *c) } /* Use the user name returned by factotum if we don't have one */ - if(user[0] != '\0' && c->user[0] == '\0') - c->user = strdup(user); + if(user[0] != '\0' && c->user[0] == '\0'){ + char *u; + u = strdup(user); + if(u == nil) + sysfatal("strdup: %r"); + c->user = u; + } /* Phase C: NTLM Authenticate */ n = mkntlm3(ntlm3, sizeof ntlm3, c->user, c->windom, ntresp); From 326e4ff5e32c836bf992c3ab5381e262b784cb1e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:05:45 +0000 Subject: [PATCH 03/45] nla: rename functions, use c->keyspec, suppress credentials for NLA, add tests Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/cb00a60a-2ef0-41c5-bbea-04959593dc38 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- efs_test.c | 2 + mkfile | 3 +- nla.c | 42 ++++----- nla_test.c | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++++ rd-thread.c | 9 +- rd.c | 9 +- rpc.c | 16 +++- 7 files changed, 286 insertions(+), 36 deletions(-) create mode 100644 nla_test.c 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/mkfile b/mkfile index dc31f87..3cc394a 100644 --- a/mkfile +++ b/mkfile @@ -32,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/nla.c b/nla.c index 74b54eb..3da3605 100644 --- a/nla.c +++ b/nla.c @@ -39,7 +39,7 @@ enum */ static int -szder(int n) +sizeder(int n) { if(n < 0x80) return 1; @@ -108,18 +108,18 @@ mktsreq(uchar *buf, int nbuf, uchar *ntlm, int ntlmlen) uchar *p; /* OCTET STRING wrapping the NTLM token */ - octetsz = 1 + szder(ntlmlen) + ntlmlen; + octetsz = 1 + sizeder(ntlmlen) + ntlmlen; /* [0] { octet } = negoToken field inside NegoDataItem */ - a0toksz = 1 + szder(octetsz) + octetsz; + a0toksz = 1 + sizeder(octetsz) + octetsz; /* SEQUENCE { a0tok } = NegoDataItem */ - itemsz = 1 + szder(a0toksz) + a0toksz; + itemsz = 1 + sizeder(a0toksz) + a0toksz; /* SEQUENCE OF { item } = NegoData */ - datasz = 1 + szder(itemsz) + itemsz; + datasz = 1 + sizeder(itemsz) + itemsz; /* [1] { seqdata } = negoTokens field */ - a1sz = 1 + szder(datasz) + datasz; + a1sz = 1 + sizeder(datasz) + datasz; /* [0] { INTEGER CredSSPVer } = version field; always 5 bytes: a0 03 02 01 vv */ bodysz = 5 + a1sz; - total = 1 + szder(bodysz) + bodysz; + total = 1 + sizeder(bodysz) + bodysz; if(total > nbuf){ werrstr("mktsreq: buffer too small (%d < %d)", nbuf, total); @@ -212,7 +212,7 @@ gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) * Send a TSRequest wrapping the given NTLM token over the TLS fd. */ static int -sendtsreq(int fd, uchar *ntlm, int ntlmlen) +writetsreq(int fd, uchar *ntlm, int ntlmlen) { uchar buf[4096]; int n; @@ -232,7 +232,7 @@ sendtsreq(int fd, uchar *ntlm, int ntlmlen) * Returns the total number of bytes read, or -1 on error. */ static int -recvtsreq(int fd, uchar *buf, int nbuf) +readtsreq(int fd, uchar *buf, int nbuf) { uchar hdr[4]; int hlen, bodylen, total, n; @@ -287,12 +287,12 @@ recvtsreq(int fd, uchar *buf, int nbuf) * This is a minimal negotiate with no domain or workstation names. */ static int -mkntlm1(uchar *buf, int nbuf) +mkntlmnego(uchar *buf, int nbuf) { uchar *p; if(nbuf < 32){ - werrstr("mkntlm1: buffer too small"); + werrstr("mkntlmnego: buffer too small"); return -1; } p = buf; @@ -308,7 +308,7 @@ mkntlm1(uchar *buf, int nbuf) * Extract the 8-byte server challenge from an NTLM Challenge (Type 2) message. */ static int -getntlm2(uchar *buf, int n, uchar challenge[8]) +getntlmchal(uchar *buf, int n, uchar challenge[8]) { if(n < 32){ werrstr("NTLM Challenge: too short (%d)", n); @@ -346,7 +346,7 @@ getntlm2(uchar *buf, int n, uchar challenge[8]) * Payload (at offset 64): DomainName | UserName | LmResponse | NtResponse */ static int -mkntlm3(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) +mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) { uchar dom16[512], usr16[512]; int domlen, usrlen; @@ -365,7 +365,7 @@ mkntlm3(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) total = ntoff + NTRespLen; if(total > nbuf){ - werrstr("mkntlm3: buffer too small (%d < %d)", nbuf, total); + werrstr("mkntlmauth: buffer too small (%d < %d)", nbuf, total); return -1; } @@ -426,26 +426,26 @@ mkntlm3(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) int nlahandshake(Rdp *c) { - uchar ntlm1[64], tsreqbuf[4096], ntlm3[640]; + uchar ntlmnego[64], tsreqbuf[4096], ntlmauth[640]; uchar challenge[8], ntresp[64]; char user[256]; uchar *ntlmp; int n, ntlmlen, nresp; /* Phase A: NTLM Negotiate */ - n = mkntlm1(ntlm1, sizeof ntlm1); + n = mkntlmnego(ntlmnego, sizeof ntlmnego); if(n < 0) return -1; - if(sendtsreq(c->fd, ntlm1, n) < 0) + if(writetsreq(c->fd, ntlmnego, n) < 0) return -1; /* Phase B: NTLM Challenge */ - n = recvtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); + n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); if(n < 0) return -1; if(gettsreq(tsreqbuf, n, &ntlmp, &ntlmlen) < 0) return -1; - if(getntlm2(ntlmp, ntlmlen, challenge) < 0) + if(getntlmchal(ntlmp, ntlmlen, challenge) < 0) return -1; /* Ask factotum to compute the NT response for this challenge */ @@ -474,10 +474,10 @@ nlahandshake(Rdp *c) } /* Phase C: NTLM Authenticate */ - n = mkntlm3(ntlm3, sizeof ntlm3, c->user, c->windom, ntresp); + n = mkntlmauth(ntlmauth, sizeof ntlmauth, c->user, c->windom, ntresp); if(n < 0) return -1; - if(sendtsreq(c->fd, ntlm3, n) < 0) + if(writetsreq(c->fd, ntlmauth, n) < 0) return -1; return 0; diff --git a/nla_test.c b/nla_test.c new file mode 100644 index 0000000..327322e --- /dev/null +++ b/nla_test.c @@ -0,0 +1,241 @@ +#include "nla.c" + +int nlatests(void); + +/* + * testmkntlmnego: NTLM Negotiate (Type 1) message must be 32 bytes with + * correct signature, message type, and negotiate flags. + */ +static int +testmkntlmnego(void) +{ + uchar buf[64]; + int n; + + n = mkntlmnego(buf, sizeof buf); + if(n != 32) + sysfatal("testmkntlmnego: want 32 bytes, got %d", n); + if(memcmp(buf, "NTLMSSP\0", 8) != 0) + sysfatal("testmkntlmnego: bad signature"); + if(GLONG(buf+8) != 1) + sysfatal("testmkntlmnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+12) != NTLMFlags) + sysfatal("testmkntlmnego: want NTLMFlags=%ux, got %lux", + NTLMFlags, (ulong)GLONG(buf+12)); + return 0; +} + +/* + * testmkntlmnegosmall: buffer too small must return an error. + */ +static int +testmkntlmnegosmall(void) +{ + uchar buf[16]; + int n; + + n = mkntlmnego(buf, sizeof buf); + if(n >= 0) + sysfatal("testmkntlmnegosmall: expected error, got %d", n); + return 0; +} + +/* + * testgetntlmchal: extract the 8-byte challenge from a minimal but valid + * NTLM Challenge (Type 2) message. + */ +static int +testgetntlmchal(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 = getntlmchal(msg, sizeof msg, challenge); + if(n < 0) + sysfatal("testgetntlmchal: unexpected error"); + if(memcmp(challenge, wantchal, 8) != 0) + sysfatal("testgetntlmchal: challenge mismatch"); + return 0; +} + +/* + * testgetntlmchalshort: message shorter than 32 bytes must return an error. + */ +static int +testgetntlmchalshort(void) +{ + uchar msg[10]; + uchar challenge[8]; + int n; + + memset(msg, 0, sizeof msg); + n = getntlmchal(msg, sizeof msg, challenge); + if(n >= 0) + sysfatal("testgetntlmchalshort: expected error, got %d", n); + return 0; +} + +/* + * testgetntlmchalbadsig: wrong signature must return an error. + */ +static int +testgetntlmchalbadsig(void) +{ + uchar msg[48]; + uchar challenge[8]; + int n; + + memset(msg, 0, sizeof msg); + memmove(msg, "BADMSSSP", 8); + PLONG(msg+8, 2); + + n = getntlmchal(msg, sizeof msg, challenge); + if(n >= 0) + sysfatal("testgetntlmchalbadsig: expected error, got %d", n); + return 0; +} + +/* + * testgetntlmchalbadtype: MessageType != 2 must return an error. + */ +static int +testgetntlmchalbadtype(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 = getntlmchal(msg, sizeof msg, challenge); + if(n >= 0) + sysfatal("testgetntlmchalbadtype: 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 + * 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, 0x05, + 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"); + if(gettsreq(buf, n, &outp, &outlen) < 0) + 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; +} + +/* + * testmkntlmauth: NTLM Authenticate (Type 3) message must have correct + * signature, message type, and negotiate flags at the expected offsets. + */ +static int +testmkntlmauth(void) +{ + uchar ntresp[NTRespLen]; + uchar buf[640]; + int n; + + memset(ntresp, 0x55, NTRespLen); + n = mkntlmauth(buf, sizeof buf, "joe", "CORP", ntresp); + if(n < 0) + sysfatal("testmkntlmauth: unexpected error"); + if(n < 64) + sysfatal("testmkntlmauth: message too short (%d)", n); + if(memcmp(buf, "NTLMSSP\0", 8) != 0) + sysfatal("testmkntlmauth: bad signature"); + if(GLONG(buf+8) != 3) + sysfatal("testmkntlmauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+60) != NTLMFlags) + sysfatal("testmkntlmauth: bad NegotiateFlags"); + return 0; +} + +int +nlatests(void) +{ + testmkntlmnego(); + testmkntlmnegosmall(); + testgetntlmchal(); + testgetntlmchalshort(); + testgetntlmchalbadsig(); + testgetntlmchalbadtype(); + testmktsreqhdr(); + testmktsreqround(); + testmktsreqsmallbuf(); + testmkntlmauth(); + return 0; +} diff --git a/rd-thread.c b/rd-thread.c index dfd8a14..fbfe1d3 100644 --- a/rd-thread.c +++ b/rd-thread.c @@ -39,12 +39,12 @@ 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{ @@ -55,7 +55,7 @@ threadmain(int argc, char *argv[]) c->nla = 1; break; case 'k': - keyspec = EARGF(usage()); + c->keyspec = EARGF(usage()); break; case 'T': c->label = strdup(EARGF(usage())); @@ -90,9 +90,8 @@ threadmain(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - c->keyspec = keyspec; if(doauth && !c->nla){ - creds = auth_getuserpasswd(auth_getkey, "proto=pass role=client service=rdp %s", keyspec); + 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 c4de36c..dc6a204 100644 --- a/rd.c +++ b/rd.c @@ -142,12 +142,12 @@ 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{ @@ -158,7 +158,7 @@ main(int argc, char *argv[]) c->nla = 1; break; case 'k': - keyspec = EARGF(usage()); + c->keyspec = EARGF(usage()); break; case 'T': c->label = strdup(EARGF(usage())); @@ -193,9 +193,8 @@ main(int argc, char *argv[]) sysfatal("set $sysname\n"); if(c->user == nil) sysfatal("set $user"); - c->keyspec = keyspec; if(doauth && !c->nla){ - creds = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", keyspec); + 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 d0d141c..769af17 100644 --- a/rpc.c +++ b/rpc.c @@ -273,12 +273,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"); From d8369fa5429cde596d61550640c2b7bc47760f8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:07:01 +0000 Subject: [PATCH 04/45] nla: remove redundant nil check on c->keyspec Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/cb00a60a-2ef0-41c5-bbea-04959593dc38 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nla.c b/nla.c index 3da3605..afc9125 100644 --- a/nla.c +++ b/nla.c @@ -454,7 +454,7 @@ nlahandshake(Rdp *c) user, sizeof(user)-1, ntresp, sizeof(ntresp), auth_getkey, - "proto=mschap service=rdp %s", c->keyspec != nil ? c->keyspec : ""); + "proto=mschap service=rdp %s", c->keyspec); if(nresp < 0){ werrstr("factotum mschap: %r"); return -1; From 373eddf643823c1799a22123c440a668df4ebdb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:26:31 +0000 Subject: [PATCH 05/45] nla: guard auth.h and nlahandshake with NLATESTONLY to fix plan9port test link Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/7ba12010-cf5a-429b-aadb-39b98025c95e Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 4 ++++ nla_test.c | 1 + 2 files changed, 5 insertions(+) diff --git a/nla.c b/nla.c index afc9125..7f82041 100644 --- a/nla.c +++ b/nla.c @@ -13,7 +13,9 @@ */ #include #include +#ifndef NLATESTONLY #include +#endif #include "dat.h" #include "fns.h" @@ -423,6 +425,7 @@ mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLe * (proto=mschap) to get the NT response from factotum. * Phase C: send NTLM Authenticate with the NT response wrapped in TSRequest. */ +#ifndef NLATESTONLY int nlahandshake(Rdp *c) { @@ -482,3 +485,4 @@ nlahandshake(Rdp *c) return 0; } +#endif /* NLATESTONLY */ diff --git a/nla_test.c b/nla_test.c index 327322e..4fc324e 100644 --- a/nla_test.c +++ b/nla_test.c @@ -1,3 +1,4 @@ +#define NLATESTONLY #include "nla.c" int nlatests(void); From d2dda4a0edf485c794ea79011f6a623214fc4421 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:38:32 +0000 Subject: [PATCH 06/45] nla: move nlahandshake to rpc.c, remove ifdef guards Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/9756222e-9c44-40ad-a024-917391821207 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 8 +++++ nla.c | 85 ++++-------------------------------------------------- nla_test.c | 1 - rpc.c | 69 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 80 deletions(-) diff --git a/fns.h b/fns.h index 675be79..642174f 100644 --- a/fns.h +++ b/fns.h @@ -40,6 +40,14 @@ int tptype(uchar*,uchar*); uchar* tpdat(uchar*,uchar*); /* nla.c */ +int mkntlmnego(uchar*, int); +int getntlmchal(uchar*, int, uchar[8]); +int mkntlmauth(uchar*, int, char*, char*, uchar*); +int writetsreq(int, uchar*, int); +int readtsreq(int, uchar*, int); +int gettsreq(uchar*, int, uchar**, int*); + +/* rpc.c */ int nlahandshake(Rdp*); /* rd.c */ diff --git a/nla.c b/nla.c index 7f82041..2fbcbfa 100644 --- a/nla.c +++ b/nla.c @@ -13,9 +13,6 @@ */ #include #include -#ifndef NLATESTONLY -#include -#endif #include "dat.h" #include "fns.h" @@ -153,7 +150,7 @@ mktsreq(uchar *buf, int nbuf, uchar *ntlm, int ntlmlen) * Parse TSRequest and extract the NTLM token from negoTokens[0]. * Sets *ntlmp to point into buf (not a copy) and *ntlmlenp to its length. */ -static int +int gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) { uchar *p, *ep, *q; @@ -213,7 +210,7 @@ gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) /* * Send a TSRequest wrapping the given NTLM token over the TLS fd. */ -static int +int writetsreq(int fd, uchar *ntlm, int ntlmlen) { uchar buf[4096]; @@ -233,7 +230,7 @@ writetsreq(int fd, uchar *ntlm, int ntlmlen) * Read one raw TSRequest DER blob from fd. * Returns the total number of bytes read, or -1 on error. */ -static int +int readtsreq(int fd, uchar *buf, int nbuf) { uchar hdr[4]; @@ -288,7 +285,7 @@ readtsreq(int fd, uchar *buf, int nbuf) * Build NTLM Negotiate message (Type 1). * This is a minimal negotiate with no domain or workstation names. */ -static int +int mkntlmnego(uchar *buf, int nbuf) { uchar *p; @@ -309,7 +306,7 @@ mkntlmnego(uchar *buf, int nbuf) /* * Extract the 8-byte server challenge from an NTLM Challenge (Type 2) message. */ -static int +int getntlmchal(uchar *buf, int n, uchar challenge[8]) { if(n < 32){ @@ -347,7 +344,7 @@ getntlmchal(uchar *buf, int n, uchar challenge[8]) * * Payload (at offset 64): DomainName | UserName | LmResponse | NtResponse */ -static int +int mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) { uchar dom16[512], usr16[512]; @@ -416,73 +413,3 @@ mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLe return p - buf; } - -/* - * nlahandshake performs the CredSSP/NTLM authentication exchange after TLS. - * - * Phase A: send NTLM Negotiate wrapped in TSRequest. - * Phase B: receive server's NTLM Challenge in TSRequest; call auth_respond - * (proto=mschap) to get the NT response from factotum. - * Phase C: send NTLM Authenticate with the NT response wrapped in TSRequest. - */ -#ifndef NLATESTONLY -int -nlahandshake(Rdp *c) -{ - uchar ntlmnego[64], tsreqbuf[4096], ntlmauth[640]; - uchar challenge[8], ntresp[64]; - char user[256]; - uchar *ntlmp; - int n, ntlmlen, nresp; - - /* Phase A: NTLM Negotiate */ - n = mkntlmnego(ntlmnego, sizeof ntlmnego); - if(n < 0) - return -1; - if(writetsreq(c->fd, ntlmnego, n) < 0) - return -1; - - /* Phase B: NTLM Challenge */ - n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); - if(n < 0) - return -1; - if(gettsreq(tsreqbuf, n, &ntlmp, &ntlmlen) < 0) - return -1; - if(getntlmchal(ntlmp, ntlmlen, challenge) < 0) - return -1; - - /* Ask factotum to compute the NT response for this challenge */ - user[0] = '\0'; - nresp = auth_respond(challenge, 8, - user, sizeof(user)-1, - ntresp, sizeof(ntresp), - auth_getkey, - "proto=mschap service=rdp %s", c->keyspec); - if(nresp < 0){ - werrstr("factotum mschap: %r"); - return -1; - } - if(nresp < NTRespLen){ - werrstr("factotum mschap: response too short (%d)", nresp); - return -1; - } - - /* Use the user name returned by factotum if we don't have one */ - if(user[0] != '\0' && c->user[0] == '\0'){ - char *u; - u = strdup(user); - if(u == nil) - sysfatal("strdup: %r"); - c->user = u; - } - - /* Phase C: NTLM Authenticate */ - n = mkntlmauth(ntlmauth, sizeof ntlmauth, c->user, c->windom, ntresp); - if(n < 0) - return -1; - if(writetsreq(c->fd, ntlmauth, n) < 0) - return -1; - - return 0; -} -#endif /* NLATESTONLY */ diff --git a/nla_test.c b/nla_test.c index 4fc324e..327322e 100644 --- a/nla_test.c +++ b/nla_test.c @@ -1,4 +1,3 @@ -#define NLATESTONLY #include "nla.c" int nlatests(void); diff --git a/rpc.c b/rpc.c index 769af17..dd955d3 100644 --- a/rpc.c +++ b/rpc.c @@ -1,5 +1,6 @@ #include #include +#include #include "dat.h" #include "fns.h" @@ -51,6 +52,74 @@ x224handshake(Rdp* c) return 0; } +/* + * nlahandshake performs the CredSSP/NTLM authentication exchange after TLS. + * + * Phase A: send NTLM Negotiate wrapped in TSRequest. + * Phase B: receive server's NTLM Challenge in TSRequest; call auth_respond + * (proto=mschap) to get the NT response from factotum. + * Phase C: send NTLM Authenticate with the NT response wrapped in TSRequest. + */ +int +nlahandshake(Rdp *c) +{ + uchar ntlmnego[64], tsreqbuf[4096], ntlmauth[640]; + uchar challenge[8], ntresp[64]; + char user[256]; + uchar *ntlmp; + int n, ntlmlen, nresp; + + /* Phase A: NTLM Negotiate */ + n = mkntlmnego(ntlmnego, sizeof ntlmnego); + if(n < 0) + return -1; + if(writetsreq(c->fd, ntlmnego, n) < 0) + return -1; + + /* Phase B: NTLM Challenge */ + n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); + if(n < 0) + return -1; + if(gettsreq(tsreqbuf, n, &ntlmp, &ntlmlen) < 0) + return -1; + if(getntlmchal(ntlmp, ntlmlen, challenge) < 0) + return -1; + + /* Ask factotum to compute the NT response for this challenge */ + user[0] = '\0'; + nresp = auth_respond(challenge, 8, + user, sizeof(user)-1, + ntresp, sizeof(ntresp), + auth_getkey, + "proto=mschap service=rdp %s", c->keyspec); + if(nresp < 0){ + werrstr("factotum mschap: %r"); + return -1; + } + if(nresp < 24){ /* NTRespLen */ + werrstr("factotum mschap: response too short (%d)", nresp); + return -1; + } + + /* Use the user name returned by factotum if we don't have one */ + if(user[0] != '\0' && c->user[0] == '\0'){ + char *u; + u = strdup(user); + if(u == nil) + sysfatal("strdup: %r"); + c->user = u; + } + + /* Phase C: NTLM Authenticate */ + n = mkntlmauth(ntlmauth, sizeof ntlmauth, c->user, c->windom, ntresp); + if(n < 0) + return -1; + if(writetsreq(c->fd, ntlmauth, n) < 0) + return -1; + + return 0; +} + int x224hangup(Rdp* c) { From fc49a22fa54be3bada74b8fca622e25defc0fb8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 4 Apr 2026 23:39:28 +0000 Subject: [PATCH 07/45] rpc: simplify strdup assignment in nlahandshake Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/9756222e-9c44-40ad-a024-917391821207 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rpc.c b/rpc.c index dd955d3..a4f78b3 100644 --- a/rpc.c +++ b/rpc.c @@ -103,11 +103,9 @@ nlahandshake(Rdp *c) /* Use the user name returned by factotum if we don't have one */ if(user[0] != '\0' && c->user[0] == '\0'){ - char *u; - u = strdup(user); - if(u == nil) + c->user = strdup(user); + if(c->user == nil) sysfatal("strdup: %r"); - c->user = u; } /* Phase C: NTLM Authenticate */ From aeeca0bafdc658ffbd2e6f0d9d60053d7acddc2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:23:06 +0000 Subject: [PATCH 08/45] =?UTF-8?q?address=20review:=20rename=20ntlm?= =?UTF-8?q?=E2=86=92nt,=20use=20gbtag/gblen,=20simplify=20signatures,=20fi?= =?UTF-8?q?x=20grouping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/1d660bed-e1bc-4fc9-abb3-97e572d18ec7 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- dat.h | 2 +- fns.h | 8 +-- mkfile | 3 +- nla.c | 157 +++++++++++++++++------------------------------------ nla_test.c | 99 ++++++++++++++++----------------- rpc.c | 33 +++++------ 6 files changed, 118 insertions(+), 184 deletions(-) diff --git a/dat.h b/dat.h index 1484923..f8d3dd8 100644 --- a/dat.h +++ b/dat.h @@ -50,8 +50,8 @@ struct Rdp char *passwd; /* password for auto logon (sic) */ char *shell; /* remote shell override */ char *rwd; /* remote working directory */ - int nla; /* use NLA (CredSSP/NTLM) authentication */ char *keyspec; /* factotum key spec */ + int nla; /* use NLA (CredSSP/NTLM) authentication */ int xsz; /* rfb dimensions */ int ysz; /* rfb dimensions */ int depth; /* rfb color depth */ diff --git a/fns.h b/fns.h index 642174f..76c6332 100644 --- a/fns.h +++ b/fns.h @@ -40,12 +40,12 @@ int tptype(uchar*,uchar*); uchar* tpdat(uchar*,uchar*); /* nla.c */ -int mkntlmnego(uchar*, int); -int getntlmchal(uchar*, int, uchar[8]); -int mkntlmauth(uchar*, int, char*, char*, uchar*); +int mkntnego(uchar*, int); +int getntchal(uchar[8], uchar*, int); +int mkntauth(uchar*, int, char*, char*, uchar*); int writetsreq(int, uchar*, int); int readtsreq(int, uchar*, int); -int gettsreq(uchar*, int, uchar**, int*); +uchar* gettsreq(uchar*, int, int*); /* rpc.c */ int nlahandshake(Rdp*); diff --git a/mkfile b/mkfile index 3cc394a..eddf2d1 100644 --- a/mkfile +++ b/mkfile @@ -32,7 +32,6 @@ OFILES=\ THREADOFILES=${OFILES:rd.$O=rd-thread.$O} CLEANFILES=$O.thread $O.test -TESTHFILES=audio.c mppc.c rle.c egdi.c nla.c TESTOFILES=\ efs_test.$O errs.$O efs.$O utf16.$O \ aud_test.$O \ @@ -55,6 +54,6 @@ runtest:V: $O.test $O.thread: $THREADOFILES $LIB $LD $LDFLAGS -o $target $prereq -$TESTOFILES: $TESTHFILES +$TESTOFILES: audio.c mppc.c rle.c egdi.c nla.c $O.test: $TESTOFILES $LIB $LD $LDFLAGS -o $target $prereq diff --git a/nla.c b/nla.c index 2fbcbfa..419861d 100644 --- a/nla.c +++ b/nla.c @@ -7,9 +7,9 @@ * with proto=mschap, so the plaintext password never leaves factotum. * * Exchange: - * Client → Server: TSRequest { negoTokens = [NTLM Negotiate] } - * Server → Client: TSRequest { negoTokens = [NTLM Challenge] } - * Client → Server: TSRequest { negoTokens = [NTLM Authenticate] } + * Client → Server: TSRequest { negoTokens = [NT Negotiate] } + * Server → Client: TSRequest { negoTokens = [NT Challenge] } + * Client → Server: TSRequest { negoTokens = [NT Authenticate] } */ #include #include @@ -18,25 +18,21 @@ enum { - /* NTLM NegotiateFlags (subset used here) */ + /* NT NegotiateFlags (subset used here) */ NfUnicode = 0x00000001, /* NTLMSSP_NEGOTIATE_UNICODE */ NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ - NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ + NfNT = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ - NTLMFlags = NfUnicode | NfReqTarget | NfNTLM | NfAlwaysSign, + NTFlags = NfUnicode | NfReqTarget | NfNT | NfAlwaysSign, - /* NTLM response size (NTLMv1) */ + /* NT response size (NTLMv1) */ NTRespLen = 24, /* CredSSP version advertised in TSRequest */ CredSSPVer = 5, }; -/* - * ASN.1 DER helpers - */ - static int sizeder(int n) { @@ -63,35 +59,8 @@ putder(uchar *p, int n) return p; } -/* parse one TLV; returns pointer past the tag+length (i.e. to the value) */ -static uchar* -getdertlv(uchar *p, uchar *ep, int *tag, int *len) -{ - if(p+2 > ep){ - werrstr("NLA: DER short"); - return nil; - } - *tag = *p++; - if(*p < 0x80){ - *len = *p++; - }else if(*p == 0x81){ - p++; - if(p >= ep){ werrstr("NLA: DER short"); return nil; } - *len = *p++; - }else if(*p == 0x82){ - p++; - if(p+2 > ep){ werrstr("NLA: DER short"); return nil; } - *len = (p[0]<<8)|p[1]; - p += 2; - }else{ - werrstr("NLA: unsupported DER length form %02x", p[-1]); - return nil; - } - return p; -} - /* - * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=ntlm}] } + * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=nt}] } * * ASN.1: * TSRequest ::= SEQUENCE { @@ -101,13 +70,13 @@ getdertlv(uchar *p, uchar *ep, int *tag, int *len) * } */ static int -mktsreq(uchar *buf, int nbuf, uchar *ntlm, int ntlmlen) +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(ntlmlen) + ntlmlen; + /* OCTET STRING wrapping the NT token */ + octetsz = 1 + sizeder(toklen) + toklen; /* [0] { octet } = negoToken field inside NegoDataItem */ a0toksz = 1 + sizeder(octetsz) + octetsz; /* SEQUENCE { a0tok } = NegoDataItem */ @@ -139,19 +108,19 @@ mktsreq(uchar *buf, int nbuf, uchar *ntlm, int ntlmlen) *p++ = 0x30; p = putder(p, a0toksz); /* negoToken [0] EXPLICIT OCTET STRING */ *p++ = 0xa0; p = putder(p, octetsz); - *p++ = 0x04; p = putder(p, ntlmlen); - memmove(p, ntlm, ntlmlen); - p += ntlmlen; + *p++ = 0x04; p = putder(p, toklen); + memmove(p, tok, toklen); + p += toklen; return p - buf; } /* - * Parse TSRequest and extract the NTLM token from negoTokens[0]. - * Sets *ntlmp to point into buf (not a copy) and *ntlmlenp to its length. + * Parse TSRequest and return a pointer to the NT token in negoTokens[0]. + * Writes the token length to *ntlenp. Returns nil on error. */ -int -gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) +uchar* +gettsreq(uchar *buf, int n, int *ntlenp) { uchar *p, *ep, *q; int tag, len; @@ -160,63 +129,54 @@ gettsreq(uchar *buf, int n, uchar **ntlmp, int *ntlmlenp) ep = buf + n; /* TSRequest SEQUENCE */ - q = getdertlv(p, ep, &tag, &len); - if(q == nil || tag != 0x30) + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) goto bad; - p = q; ep = p + len; /* walk SEQUENCE body looking for [1] negoTokens */ while(p < ep){ - q = getdertlv(p, ep, &tag, &len); - if(q == nil) + if((q = gbtag(p, ep, &tag)) == nil + || (q = gblen(q, ep, &len)) == nil) goto bad; - if(tag == 0xa1){ - /* [1] NegoData SEQUENCE OF */ + if(tag == 1){ p = q; ep = p + len; - q = getdertlv(p, ep, &tag, &len); - if(q == nil || tag != 0x30) + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) goto bad; - p = q; ep = p + len; - /* NegoDataItem SEQUENCE */ - q = getdertlv(p, ep, &tag, &len); - if(q == nil || tag != 0x30) + if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + || (p = gblen(p, ep, &len)) == nil) goto bad; - p = q; ep = p + len; - /* negoToken [0] */ - q = getdertlv(p, ep, &tag, &len); - if(q == nil || tag != 0xa0) + if((p = gbtag(p, ep, &tag)) == nil || tag != 0 + || (p = gblen(p, ep, &len)) == nil) goto bad; - p = q; ep = p + len; - /* OCTET STRING */ - q = getdertlv(p, ep, &tag, &len); - if(q == nil || tag != 0x04) + if((p = gbtag(p, ep, &tag)) == nil || tag != TagOctetString + || (p = gblen(p, ep, &len)) == nil) goto bad; - *ntlmp = q; - *ntlmlenp = len; - return 0; + *ntlenp = len; + return p; } p = q + len; } bad: werrstr("NLA: TSRequest parse error"); - return -1; + return nil; } /* - * Send a TSRequest wrapping the given NTLM token over the TLS fd. + * Send a TSRequest wrapping the given NT token over the TLS fd. */ int -writetsreq(int fd, uchar *ntlm, int ntlmlen) +writetsreq(int fd, uchar *tok, int toklen) { uchar buf[4096]; int n; - n = mktsreq(buf, sizeof buf, ntlm, ntlmlen); + n = mktsreq(buf, sizeof buf, tok, toklen); if(n < 0) return -1; if(write(fd, buf, n) != n){ @@ -282,70 +242,51 @@ readtsreq(int fd, uchar *buf, int nbuf) } /* - * Build NTLM Negotiate message (Type 1). + * Build NT Negotiate message (Type 1). * This is a minimal negotiate with no domain or workstation names. */ int -mkntlmnego(uchar *buf, int nbuf) +mkntnego(uchar *buf, int nbuf) { uchar *p; if(nbuf < 32){ - werrstr("mkntlmnego: buffer too small"); + werrstr("mkntnego: buffer too small"); return -1; } p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NTLMFlags); p += 4; /* NegotiateFlags */ + PLONG(p, NTFlags); 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. + * Extract the 8-byte server challenge from an NT Challenge (Type 2) message. */ int -getntlmchal(uchar *buf, int n, uchar challenge[8]) +getntchal(uchar challenge[8], uchar *buf, int n) { if(n < 32){ - werrstr("NTLM Challenge: too short (%d)", n); + werrstr("NT Challenge: too short (%d)", n); return -1; } if(memcmp(buf, "NTLMSSP\0", 8) != 0){ - werrstr("NTLM Challenge: bad signature"); + werrstr("NT Challenge: bad signature"); return -1; } if(GLONG(buf+8) != 2){ - werrstr("NTLM Challenge: bad MessageType (%ld)", (long)GLONG(buf+8)); + werrstr("NT Challenge: bad MessageType (%ld)", (long)GLONG(buf+8)); return -1; } memmove(challenge, buf+24, 8); return 0; } -/* - * Build NTLM Authenticate message (Type 3). - * - * ntresp must be NTRespLen (24) bytes: the NT response from auth_respond. - * LmChallengeResponse is set to zeros (acceptable for NTLMv1). - * - * Fixed header layout (64 bytes, no Version or MIC fields): - * 0 Signature "NTLMSSP\0" (8) - * 8 MessageType = 3 (4) - * 12 LmChallengeResponseFields (8) - * 20 NtChallengeResponseFields (8) - * 28 DomainNameFields (8) - * 36 UserNameFields (8) - * 44 WorkstationFields (8) - * 52 EncryptedRandomSessionKeyFields (8) - * 60 NegotiateFlags (4) - * - * Payload (at offset 64): DomainName | UserName | LmResponse | NtResponse - */ int -mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) +mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) { uchar dom16[512], usr16[512]; int domlen, usrlen; @@ -364,7 +305,7 @@ mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLe total = ntoff + NTRespLen; if(total > nbuf){ - werrstr("mkntlmauth: buffer too small (%d < %d)", nbuf, total); + werrstr("mkntauth: buffer too small (%d < %d)", nbuf, total); return -1; } @@ -403,7 +344,7 @@ mkntlmauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLe PLONG(p, ntoff+NTRespLen); p += 4; /* NegotiateFlags */ - PLONG(p, NTLMFlags); p += 4; + PLONG(p, NTFlags); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ diff --git a/nla_test.c b/nla_test.c index 327322e..5968508 100644 --- a/nla_test.c +++ b/nla_test.c @@ -3,49 +3,49 @@ int nlatests(void); /* - * testmkntlmnego: NTLM Negotiate (Type 1) message must be 32 bytes with + * testmkntnego: NT Negotiate (Type 1) message must be 32 bytes with * correct signature, message type, and negotiate flags. */ static int -testmkntlmnego(void) +testmkntnego(void) { uchar buf[64]; int n; - n = mkntlmnego(buf, sizeof buf); + n = mkntnego(buf, sizeof buf); if(n != 32) - sysfatal("testmkntlmnego: want 32 bytes, got %d", n); + sysfatal("testmkntnego: want 32 bytes, got %d", n); if(memcmp(buf, "NTLMSSP\0", 8) != 0) - sysfatal("testmkntlmnego: bad signature"); + sysfatal("testmkntnego: bad signature"); if(GLONG(buf+8) != 1) - sysfatal("testmkntlmnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+12) != NTLMFlags) - sysfatal("testmkntlmnego: want NTLMFlags=%ux, got %lux", - NTLMFlags, (ulong)GLONG(buf+12)); + sysfatal("testmkntnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+12) != NTFlags) + sysfatal("testmkntnego: want NTFlags=%ux, got %lux", + NTFlags, (ulong)GLONG(buf+12)); return 0; } /* - * testmkntlmnegosmall: buffer too small must return an error. + * testmkntnegosmall: buffer too small must return an error. */ static int -testmkntlmnegosmall(void) +testmkntnegosmall(void) { uchar buf[16]; int n; - n = mkntlmnego(buf, sizeof buf); + n = mkntnego(buf, sizeof buf); if(n >= 0) - sysfatal("testmkntlmnegosmall: expected error, got %d", n); + sysfatal("testmkntnegosmall: expected error, got %d", n); return 0; } /* - * testgetntlmchal: extract the 8-byte challenge from a minimal but valid - * NTLM Challenge (Type 2) message. + * testgetntchal: extract the 8-byte challenge from a minimal but valid + * NT Challenge (Type 2) message. */ static int -testgetntlmchal(void) +testgetntchal(void) { uchar msg[48]; uchar challenge[8]; @@ -57,36 +57,36 @@ testgetntlmchal(void) PLONG(msg+8, 2); /* MessageType = 2 */ memmove(msg+24, wantchal, 8); /* ServerChallenge */ - n = getntlmchal(msg, sizeof msg, challenge); + n = getntchal(challenge, msg, sizeof msg); if(n < 0) - sysfatal("testgetntlmchal: unexpected error"); + sysfatal("testgetntchal: unexpected error"); if(memcmp(challenge, wantchal, 8) != 0) - sysfatal("testgetntlmchal: challenge mismatch"); + sysfatal("testgetntchal: challenge mismatch"); return 0; } /* - * testgetntlmchalshort: message shorter than 32 bytes must return an error. + * testgetntchalshort: message shorter than 32 bytes must return an error. */ static int -testgetntlmchalshort(void) +testgetntchalshort(void) { uchar msg[10]; uchar challenge[8]; int n; memset(msg, 0, sizeof msg); - n = getntlmchal(msg, sizeof msg, challenge); + n = getntchal(challenge, msg, sizeof msg); if(n >= 0) - sysfatal("testgetntlmchalshort: expected error, got %d", n); + sysfatal("testgetntchalshort: expected error, got %d", n); return 0; } /* - * testgetntlmchalbadsig: wrong signature must return an error. + * testgetntchalbadsig: wrong signature must return an error. */ static int -testgetntlmchalbadsig(void) +testgetntchalbadsig(void) { uchar msg[48]; uchar challenge[8]; @@ -96,17 +96,17 @@ testgetntlmchalbadsig(void) memmove(msg, "BADMSSSP", 8); PLONG(msg+8, 2); - n = getntlmchal(msg, sizeof msg, challenge); + n = getntchal(challenge, msg, sizeof msg); if(n >= 0) - sysfatal("testgetntlmchalbadsig: expected error, got %d", n); + sysfatal("testgetntchalbadsig: expected error, got %d", n); return 0; } /* - * testgetntlmchalbadtype: MessageType != 2 must return an error. + * testgetntchalbadtype: MessageType != 2 must return an error. */ static int -testgetntlmchalbadtype(void) +testgetntchalbadtype(void) { uchar msg[48]; uchar challenge[8]; @@ -116,15 +116,15 @@ testgetntlmchalbadtype(void) memmove(msg, "NTLMSSP\0", 8); PLONG(msg+8, 1); /* Type 1, not 2 */ - n = getntlmchal(msg, sizeof msg, challenge); + n = getntchal(challenge, msg, sizeof msg); if(n >= 0) - sysfatal("testgetntlmchalbadtype: expected error, got %d", n); + 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). + * single-byte NT token (0xAA). * * 30 10 SEQUENCE(16) * a0 03 02 01 05 [0] version=5 @@ -172,7 +172,8 @@ testmktsreqround(void) n = mktsreq(buf, sizeof buf, token, sizeof token); if(n < 0) sysfatal("testmktsreqround: mktsreq failed"); - if(gettsreq(buf, n, &outp, &outlen) < 0) + outp = gettsreq(buf, n, &outlen); + if(outp == nil) sysfatal("testmktsreqround: gettsreq failed"); if(outlen != (int)sizeof token) sysfatal("testmktsreqround: outlen: want %d, got %d", @@ -199,43 +200,43 @@ testmktsreqsmallbuf(void) } /* - * testmkntlmauth: NTLM Authenticate (Type 3) message must have correct + * testmkntauth: NT Authenticate (Type 3) message must have correct * signature, message type, and negotiate flags at the expected offsets. */ static int -testmkntlmauth(void) +testmkntauth(void) { uchar ntresp[NTRespLen]; uchar buf[640]; int n; memset(ntresp, 0x55, NTRespLen); - n = mkntlmauth(buf, sizeof buf, "joe", "CORP", ntresp); + n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp); if(n < 0) - sysfatal("testmkntlmauth: unexpected error"); + sysfatal("testmkntauth: unexpected error"); if(n < 64) - sysfatal("testmkntlmauth: message too short (%d)", n); + sysfatal("testmkntauth: message too short (%d)", n); if(memcmp(buf, "NTLMSSP\0", 8) != 0) - sysfatal("testmkntlmauth: bad signature"); + sysfatal("testmkntauth: bad signature"); if(GLONG(buf+8) != 3) - sysfatal("testmkntlmauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+60) != NTLMFlags) - sysfatal("testmkntlmauth: bad NegotiateFlags"); + sysfatal("testmkntauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); + if(GLONG(buf+60) != NTFlags) + sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } int nlatests(void) { - testmkntlmnego(); - testmkntlmnegosmall(); - testgetntlmchal(); - testgetntlmchalshort(); - testgetntlmchalbadsig(); - testgetntlmchalbadtype(); + testmkntnego(); + testmkntnegosmall(); + testgetntchal(); + testgetntchalshort(); + testgetntchalbadsig(); + testgetntchalbadtype(); testmktsreqhdr(); testmktsreqround(); testmktsreqsmallbuf(); - testmkntlmauth(); + testmkntauth(); return 0; } diff --git a/rpc.c b/rpc.c index a4f78b3..cf93ac1 100644 --- a/rpc.c +++ b/rpc.c @@ -52,37 +52,30 @@ x224handshake(Rdp* c) return 0; } -/* - * nlahandshake performs the CredSSP/NTLM authentication exchange after TLS. - * - * Phase A: send NTLM Negotiate wrapped in TSRequest. - * Phase B: receive server's NTLM Challenge in TSRequest; call auth_respond - * (proto=mschap) to get the NT response from factotum. - * Phase C: send NTLM Authenticate with the NT response wrapped in TSRequest. - */ int nlahandshake(Rdp *c) { - uchar ntlmnego[64], tsreqbuf[4096], ntlmauth[640]; + uchar ntnego[64], tsreqbuf[4096], ntauth[640]; uchar challenge[8], ntresp[64]; char user[256]; - uchar *ntlmp; - int n, ntlmlen, nresp; + uchar *ntp; + int n, ntlen, nresp; - /* Phase A: NTLM Negotiate */ - n = mkntlmnego(ntlmnego, sizeof ntlmnego); + /* Phase A: NT Negotiate */ + n = mkntnego(ntnego, sizeof ntnego); if(n < 0) return -1; - if(writetsreq(c->fd, ntlmnego, n) < 0) + if(writetsreq(c->fd, ntnego, n) < 0) return -1; - /* Phase B: NTLM Challenge */ + /* Phase B: NT Challenge */ n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); if(n < 0) return -1; - if(gettsreq(tsreqbuf, n, &ntlmp, &ntlmlen) < 0) + ntp = gettsreq(tsreqbuf, n, &ntlen); + if(ntp == nil) return -1; - if(getntlmchal(ntlmp, ntlmlen, challenge) < 0) + if(getntchal(challenge, ntp, ntlen) < 0) return -1; /* Ask factotum to compute the NT response for this challenge */ @@ -108,11 +101,11 @@ nlahandshake(Rdp *c) sysfatal("strdup: %r"); } - /* Phase C: NTLM Authenticate */ - n = mkntlmauth(ntlmauth, sizeof ntlmauth, c->user, c->windom, ntresp); + /* Phase C: NT Authenticate */ + n = mkntauth(ntauth, sizeof ntauth, c->user, c->windom, ntresp); if(n < 0) return -1; - if(writetsreq(c->fd, ntlmauth, n) < 0) + if(writetsreq(c->fd, ntauth, n) < 0) return -1; return 0; From 8433166f6bb9c9e643c5533af911787a10e6be89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:24:36 +0000 Subject: [PATCH 09/45] nla: add named constants for CredSSP context-specific tags Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/1d660bed-e1bc-4fc9-abb3-97e572d18ec7 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/nla.c b/nla.c index 419861d..476e45c 100644 --- a/nla.c +++ b/nla.c @@ -29,6 +29,10 @@ enum /* NT response size (NTLMv1) */ NTRespLen = 24, + /* CredSSP TSRequest context-specific field tags (gbtag returns 5-bit tag number) */ + TSSnegoTokens = 1, /* TSRequest [1] negoTokens field */ + TSSnegoToken = 0, /* NegoDataItem [0] negoToken field */ + /* CredSSP version advertised in TSRequest */ CredSSPVer = 5, }; @@ -139,7 +143,7 @@ gettsreq(uchar *buf, int n, int *ntlenp) if((q = gbtag(p, ep, &tag)) == nil || (q = gblen(q, ep, &len)) == nil) goto bad; - if(tag == 1){ + if(tag == TSSnegoTokens){ p = q; ep = p + len; if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq @@ -150,7 +154,7 @@ gettsreq(uchar *buf, int n, int *ntlenp) || (p = gblen(p, ep, &len)) == nil) goto bad; ep = p + len; - if((p = gbtag(p, ep, &tag)) == nil || tag != 0 + if((p = gbtag(p, ep, &tag)) == nil || tag != TSSnegoToken || (p = gblen(p, ep, &len)) == nil) goto bad; ep = p + len; From 6b45d4a61f97a9e87ed959f903003f200a5f8a5c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:42:32 +0000 Subject: [PATCH 10/45] nla: fix build failures: NTLM in constants/comments, restore TESTHFILES, fix gettsreq Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/546c4471-ad4e-4fc2-bc82-8982e94453e9 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- mkfile | 3 ++- nla.c | 53 ++++++++++++++++++++++++++++------------------------- nla_test.c | 16 ++++++++-------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/mkfile b/mkfile index eddf2d1..3cc394a 100644 --- a/mkfile +++ b/mkfile @@ -32,6 +32,7 @@ OFILES=\ THREADOFILES=${OFILES:rd.$O=rd-thread.$O} CLEANFILES=$O.thread $O.test +TESTHFILES=audio.c mppc.c rle.c egdi.c nla.c TESTOFILES=\ efs_test.$O errs.$O efs.$O utf16.$O \ aud_test.$O \ @@ -54,6 +55,6 @@ runtest:V: $O.test $O.thread: $THREADOFILES $LIB $LD $LDFLAGS -o $target $prereq -$TESTOFILES: audio.c mppc.c rle.c egdi.c nla.c +$TESTOFILES: $TESTHFILES $O.test: $TESTOFILES $LIB $LD $LDFLAGS -o $target $prereq diff --git a/nla.c b/nla.c index 476e45c..4dbadaa 100644 --- a/nla.c +++ b/nla.c @@ -7,9 +7,9 @@ * with proto=mschap, so the plaintext password never leaves factotum. * * Exchange: - * Client → Server: TSRequest { negoTokens = [NT Negotiate] } - * Server → Client: TSRequest { negoTokens = [NT Challenge] } - * Client → Server: TSRequest { negoTokens = [NT Authenticate] } + * Client → Server: TSRequest { negoTokens = [NTLM Negotiate] } + * Server → Client: TSRequest { negoTokens = [NTLM Challenge] } + * Client → Server: TSRequest { negoTokens = [NTLM Authenticate] } */ #include #include @@ -18,17 +18,21 @@ enum { - /* NT NegotiateFlags (subset used here) */ + /* NTLM NegotiateFlags (subset used here) */ NfUnicode = 0x00000001, /* NTLMSSP_NEGOTIATE_UNICODE */ NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ - NfNT = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ + NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ - NTFlags = NfUnicode | NfReqTarget | NfNT | NfAlwaysSign, + NTLMFlags = NfUnicode | NfReqTarget | NfNTLM | NfAlwaysSign, - /* NT response size (NTLMv1) */ + /* NTLM response size (NTLMv1) */ NTRespLen = 24, + /* ASN.1 Universal tags (BER/DER) */ + TagOStr = 4, /* OCTET STRING */ + TagSeq = 16, /* SEQUENCE / SEQUENCE OF */ + /* CredSSP TSRequest context-specific field tags (gbtag returns 5-bit tag number) */ TSSnegoTokens = 1, /* TSRequest [1] negoTokens field */ TSSnegoToken = 0, /* NegoDataItem [0] negoToken field */ @@ -64,7 +68,7 @@ putder(uchar *p, int n) } /* - * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=nt}] } + * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=ntlm}] } * * ASN.1: * TSRequest ::= SEQUENCE { @@ -79,7 +83,7 @@ mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) int octetsz, a0toksz, itemsz, datasz, a1sz, bodysz, total; uchar *p; - /* OCTET STRING wrapping the NT token */ + /* OCTET STRING wrapping the NTLM token */ octetsz = 1 + sizeder(toklen) + toklen; /* [0] { octet } = negoToken field inside NegoDataItem */ a0toksz = 1 + sizeder(octetsz) + octetsz; @@ -120,7 +124,7 @@ mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) } /* - * Parse TSRequest and return a pointer to the NT token in negoTokens[0]. + * Parse TSRequest and return a pointer to the NTLM token in negoTokens[0]. * Writes the token length to *ntlenp. Returns nil on error. */ uchar* @@ -144,21 +148,20 @@ gettsreq(uchar *buf, int n, int *ntlenp) || (q = gblen(q, ep, &len)) == nil) goto bad; if(tag == TSSnegoTokens){ - p = q; - ep = p + len; - if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq + /* NegoData SEQUENCE OF */ + if((p = gbtag(q, ep, &tag)) == nil || tag != TagSeq || (p = gblen(p, ep, &len)) == nil) goto bad; - ep = p + len; + /* NegoDataItem SEQUENCE */ if((p = gbtag(p, ep, &tag)) == nil || tag != TagSeq || (p = gblen(p, ep, &len)) == nil) goto bad; - ep = p + len; + /* negoToken [0] */ if((p = gbtag(p, ep, &tag)) == nil || tag != TSSnegoToken || (p = gblen(p, ep, &len)) == nil) goto bad; - ep = p + len; - if((p = gbtag(p, ep, &tag)) == nil || tag != TagOctetString + /* OCTET STRING */ + if((p = gbtag(p, ep, &tag)) == nil || tag != TagOStr || (p = gblen(p, ep, &len)) == nil) goto bad; *ntlenp = len; @@ -172,7 +175,7 @@ gettsreq(uchar *buf, int n, int *ntlenp) } /* - * Send a TSRequest wrapping the given NT token over the TLS fd. + * Send a TSRequest wrapping the given NTLM token over the TLS fd. */ int writetsreq(int fd, uchar *tok, int toklen) @@ -246,7 +249,7 @@ readtsreq(int fd, uchar *buf, int nbuf) } /* - * Build NT Negotiate message (Type 1). + * Build NTLM Negotiate message (Type 1). * This is a minimal negotiate with no domain or workstation names. */ int @@ -261,28 +264,28 @@ mkntnego(uchar *buf, int nbuf) p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NTFlags); p += 4; /* NegotiateFlags */ + PLONG(p, NTLMFlags); 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 NT Challenge (Type 2) message. + * 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("NT Challenge: too short (%d)", n); + werrstr("NTLM Challenge: too short (%d)", n); return -1; } if(memcmp(buf, "NTLMSSP\0", 8) != 0){ - werrstr("NT Challenge: bad signature"); + werrstr("NTLM Challenge: bad signature"); return -1; } if(GLONG(buf+8) != 2){ - werrstr("NT Challenge: bad MessageType (%ld)", (long)GLONG(buf+8)); + werrstr("NTLM Challenge: bad MessageType (%ld)", (long)GLONG(buf+8)); return -1; } memmove(challenge, buf+24, 8); @@ -348,7 +351,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] PLONG(p, ntoff+NTRespLen); p += 4; /* NegotiateFlags */ - PLONG(p, NTFlags); p += 4; + PLONG(p, NTLMFlags); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ diff --git a/nla_test.c b/nla_test.c index 5968508..306ec69 100644 --- a/nla_test.c +++ b/nla_test.c @@ -3,7 +3,7 @@ int nlatests(void); /* - * testmkntnego: NT Negotiate (Type 1) message must be 32 bytes with + * testmkntnego: NTLM Negotiate (Type 1) message must be 32 bytes with * correct signature, message type, and negotiate flags. */ static int @@ -19,9 +19,9 @@ testmkntnego(void) sysfatal("testmkntnego: bad signature"); if(GLONG(buf+8) != 1) sysfatal("testmkntnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+12) != NTFlags) - sysfatal("testmkntnego: want NTFlags=%ux, got %lux", - NTFlags, (ulong)GLONG(buf+12)); + if(GLONG(buf+12) != NTLMFlags) + sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", + NTLMFlags, (ulong)GLONG(buf+12)); return 0; } @@ -42,7 +42,7 @@ testmkntnegosmall(void) /* * testgetntchal: extract the 8-byte challenge from a minimal but valid - * NT Challenge (Type 2) message. + * NTLM Challenge (Type 2) message. */ static int testgetntchal(void) @@ -124,7 +124,7 @@ testgetntchalbadtype(void) /* * testmktsreqhdr: verify the exact DER encoding of a TSRequest wrapping a - * single-byte NT token (0xAA). + * single-byte NTLM token (0xAA). * * 30 10 SEQUENCE(16) * a0 03 02 01 05 [0] version=5 @@ -200,7 +200,7 @@ testmktsreqsmallbuf(void) } /* - * testmkntauth: NT Authenticate (Type 3) message must have correct + * testmkntauth: NTLM Authenticate (Type 3) message must have correct * signature, message type, and negotiate flags at the expected offsets. */ static int @@ -220,7 +220,7 @@ testmkntauth(void) sysfatal("testmkntauth: bad signature"); if(GLONG(buf+8) != 3) sysfatal("testmkntauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+60) != NTFlags) + if(GLONG(buf+60) != NTLMFlags) sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } From 8425559af365b1c546b86978b7c5d159475d548b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:59:35 +0000 Subject: [PATCH 11/45] nla: replace magic DER tag bytes with named constants in writetsreq/readtsreq Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/eda203d1-3275-4df8-bfa2-cb5ef1be5de0 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/nla.c b/nla.c index 4dbadaa..47f6ceb 100644 --- a/nla.c +++ b/nla.c @@ -30,6 +30,7 @@ enum NTRespLen = 24, /* ASN.1 Universal tags (BER/DER) */ + TagInt = 2, /* INTEGER */ TagOStr = 4, /* OCTET STRING */ TagSeq = 16, /* SEQUENCE / SEQUENCE OF */ @@ -104,19 +105,19 @@ mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) p = buf; /* TSRequest SEQUENCE */ - *p++ = 0x30; p = putder(p, bodysz); + *p++ = 0x20|TagSeq; p = putder(p, bodysz); /* version [0] EXPLICIT INTEGER CredSSPVer */ - *p++ = 0xa0; *p++ = 0x03; - *p++ = 0x02; *p++ = 0x01; *p++ = CredSSPVer; + *p++ = 0xa0|TSSnegoToken; *p++ = 0x03; + *p++ = TagInt; *p++ = 0x01; *p++ = CredSSPVer; /* negoTokens [1] EXPLICIT NegoData */ - *p++ = 0xa1; p = putder(p, datasz); + *p++ = 0xa0|TSSnegoTokens; p = putder(p, datasz); /* NegoData SEQUENCE OF */ - *p++ = 0x30; p = putder(p, itemsz); + *p++ = 0x20|TagSeq; p = putder(p, itemsz); /* NegoDataItem SEQUENCE */ - *p++ = 0x30; p = putder(p, a0toksz); + *p++ = 0x20|TagSeq; p = putder(p, a0toksz); /* negoToken [0] EXPLICIT OCTET STRING */ - *p++ = 0xa0; p = putder(p, octetsz); - *p++ = 0x04; p = putder(p, toklen); + *p++ = 0xa0|TSSnegoToken; p = putder(p, octetsz); + *p++ = TagOStr; p = putder(p, toklen); memmove(p, tok, toklen); p += toklen; @@ -209,7 +210,7 @@ readtsreq(int fd, uchar *buf, int nbuf) werrstr("NLA: read TSRequest header: %r"); return -1; } - if(hdr[0] != 0x30){ + if(hdr[0] != (0x20|TagSeq)){ werrstr("NLA: TSRequest not a SEQUENCE (got 0x%02x)", hdr[0]); return -1; } From 33cf896c44450fecb9dac7f3cd875c974ce0a126 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:06:58 +0000 Subject: [PATCH 12/45] =?UTF-8?q?nla:=20rename=20TagOStr=E2=86=92TagOctetS?= =?UTF-8?q?tring,=20name=20BER=20magic=20numbers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/35aac9ff-67f6-4e4e-9f35-3b4708ec4397 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/nla.c b/nla.c index 47f6ceb..6bc70ac 100644 --- a/nla.c +++ b/nla.c @@ -31,9 +31,18 @@ enum /* ASN.1 Universal tags (BER/DER) */ TagInt = 2, /* INTEGER */ - TagOStr = 4, /* OCTET STRING */ + 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 */ @@ -45,7 +54,7 @@ enum static int sizeder(int n) { - if(n < 0x80) + if(n < BerShortMax) return 1; if(n < 0x100) return 2; @@ -55,13 +64,13 @@ sizeder(int n) static uchar* putder(uchar *p, int n) { - if(n < 0x80){ + if(n < BerShortMax){ *p++ = n; }else if(n < 0x100){ - *p++ = 0x81; + *p++ = BerLen1; *p++ = n; }else{ - *p++ = 0x82; + *p++ = BerLen2; *p++ = n >> 8; *p++ = n; } @@ -105,19 +114,19 @@ mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) p = buf; /* TSRequest SEQUENCE */ - *p++ = 0x20|TagSeq; p = putder(p, bodysz); + *p++ = BerConstructed|TagSeq; p = putder(p, bodysz); /* version [0] EXPLICIT INTEGER CredSSPVer */ - *p++ = 0xa0|TSSnegoToken; *p++ = 0x03; + *p++ = BerContext|TSSnegoToken; *p++ = 0x03; *p++ = TagInt; *p++ = 0x01; *p++ = CredSSPVer; /* negoTokens [1] EXPLICIT NegoData */ - *p++ = 0xa0|TSSnegoTokens; p = putder(p, datasz); + *p++ = BerContext|TSSnegoTokens; p = putder(p, datasz); /* NegoData SEQUENCE OF */ - *p++ = 0x20|TagSeq; p = putder(p, itemsz); + *p++ = BerConstructed|TagSeq; p = putder(p, itemsz); /* NegoDataItem SEQUENCE */ - *p++ = 0x20|TagSeq; p = putder(p, a0toksz); + *p++ = BerConstructed|TagSeq; p = putder(p, a0toksz); /* negoToken [0] EXPLICIT OCTET STRING */ - *p++ = 0xa0|TSSnegoToken; p = putder(p, octetsz); - *p++ = TagOStr; p = putder(p, toklen); + *p++ = BerContext|TSSnegoToken; p = putder(p, octetsz); + *p++ = TagOctetString; p = putder(p, toklen); memmove(p, tok, toklen); p += toklen; @@ -162,7 +171,7 @@ gettsreq(uchar *buf, int n, int *ntlenp) || (p = gblen(p, ep, &len)) == nil) goto bad; /* OCTET STRING */ - if((p = gbtag(p, ep, &tag)) == nil || tag != TagOStr + if((p = gbtag(p, ep, &tag)) == nil || tag != TagOctetString || (p = gblen(p, ep, &len)) == nil) goto bad; *ntlenp = len; @@ -210,21 +219,21 @@ readtsreq(int fd, uchar *buf, int nbuf) werrstr("NLA: read TSRequest header: %r"); return -1; } - if(hdr[0] != (0x20|TagSeq)){ + if(hdr[0] != (BerConstructed|TagSeq)){ werrstr("NLA: TSRequest not a SEQUENCE (got 0x%02x)", hdr[0]); return -1; } - if(hdr[1] < 0x80){ + if(hdr[1] < BerShortMax){ bodylen = hdr[1]; hlen = 2; - }else if(hdr[1] == 0x81){ + }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] == 0x82){ + }else if(hdr[1] == BerLen2){ if(readn(fd, hdr+2, 2) != 2){ werrstr("NLA: read TSRequest length: %r"); return -1; From 9be022e110f677b31749b6e0330f2f16807a29c6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:22:04 +0000 Subject: [PATCH 13/45] nla: add len comment, use putder for int length, consolidate p+= in mkntauth Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/9854f701-636d-4920-ba11-0b4f5dba0518 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/nla.c b/nla.c index 6bc70ac..3096fc8 100644 --- a/nla.c +++ b/nla.c @@ -116,8 +116,8 @@ mktsreq(uchar *buf, int nbuf, uchar *tok, int toklen) /* TSRequest SEQUENCE */ *p++ = BerConstructed|TagSeq; p = putder(p, bodysz); /* version [0] EXPLICIT INTEGER CredSSPVer */ - *p++ = BerContext|TSSnegoToken; *p++ = 0x03; - *p++ = TagInt; *p++ = 0x01; *p++ = 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 */ @@ -331,34 +331,22 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] PLONG(p, 3); p += 4; /* MessageType */ /* LmChallengeResponseFields */ - PSHORT(p, lmlen); p += 2; - PSHORT(p, lmlen); p += 2; - PLONG(p, lmoff); p += 4; + PSHORT(p, lmlen); PSHORT(p+2, lmlen); PLONG(p+4, lmoff); p += 8; /* NtChallengeResponseFields */ - PSHORT(p, NTRespLen); p += 2; - PSHORT(p, NTRespLen); p += 2; - PLONG(p, ntoff); p += 4; + PSHORT(p, NTRespLen); PSHORT(p+2, NTRespLen); PLONG(p+4, ntoff); p += 8; /* DomainNameFields */ - PSHORT(p, domlen); p += 2; - PSHORT(p, domlen); p += 2; - PLONG(p, domoff); p += 4; + PSHORT(p, domlen); PSHORT(p+2, domlen); PLONG(p+4, domoff); p += 8; /* UserNameFields */ - PSHORT(p, usrlen); p += 2; - PSHORT(p, usrlen); p += 2; - PLONG(p, usroff); p += 4; + PSHORT(p, usrlen); PSHORT(p+2, usrlen); PLONG(p+4, usroff); p += 8; /* WorkstationFields (empty) */ - PSHORT(p, 0); p += 2; - PSHORT(p, 0); p += 2; - PLONG(p, lmoff); p += 4; /* offset points to lm area; length is 0 */ + PSHORT(p, 0); PSHORT(p+2, 0); PLONG(p+4, lmoff); p += 8; /* offset points to lm area; length is 0 */ /* EncryptedRandomSessionKeyFields (empty) */ - PSHORT(p, 0); p += 2; - PSHORT(p, 0); p += 2; - PLONG(p, ntoff+NTRespLen); p += 4; + PSHORT(p, 0); PSHORT(p+2, 0); PLONG(p+4, ntoff+NTRespLen); p += 8; /* NegotiateFlags */ PLONG(p, NTLMFlags); p += 4; From 44d30c03c7429c3c28104d6b992e4e0532a18b8b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:26:28 +0000 Subject: [PATCH 14/45] nla: unfold mkntauth field groups to one statement per line Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/f8cdbd92-49c8-4805-a2b3-0679ad8fc952 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/nla.c b/nla.c index 3096fc8..d63388e 100644 --- a/nla.c +++ b/nla.c @@ -331,22 +331,40 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] PLONG(p, 3); p += 4; /* MessageType */ /* LmChallengeResponseFields */ - PSHORT(p, lmlen); PSHORT(p+2, lmlen); PLONG(p+4, lmoff); p += 8; + 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; + 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; + 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; + 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; /* offset points to lm area; length is 0 */ + PSHORT(p, 0); + PSHORT(p+2, 0); + PLONG(p+4, lmoff); /* offset points to lm area; length is 0 */ + p += 8; /* EncryptedRandomSessionKeyFields (empty) */ - PSHORT(p, 0); PSHORT(p+2, 0); PLONG(p+4, ntoff+NTRespLen); p += 8; + PSHORT(p, 0); + PSHORT(p+2, 0); + PLONG(p+4, ntoff+NTRespLen); + p += 8; /* NegotiateFlags */ PLONG(p, NTLMFlags); p += 4; From 8cfbbea611a8174351272e7acb931fe8da6be819 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:33:24 +0000 Subject: [PATCH 15/45] rpc: fix phase comments to say NTLM Negotiate/Challenge/Authenticate Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/494b37f2-b597-4fe5-818e-4714999afe08 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rpc.c b/rpc.c index cf93ac1..3aec602 100644 --- a/rpc.c +++ b/rpc.c @@ -61,14 +61,14 @@ nlahandshake(Rdp *c) uchar *ntp; int n, ntlen, nresp; - /* Phase A: NT Negotiate */ + /* Phase A: NTLM Negotiate */ n = mkntnego(ntnego, sizeof ntnego); if(n < 0) return -1; if(writetsreq(c->fd, ntnego, n) < 0) return -1; - /* Phase B: NT Challenge */ + /* Phase B: NTLM Challenge */ n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); if(n < 0) return -1; @@ -101,7 +101,7 @@ nlahandshake(Rdp *c) sysfatal("strdup: %r"); } - /* Phase C: NT Authenticate */ + /* Phase C: NTLM Authenticate */ n = mkntauth(ntauth, sizeof ntauth, c->user, c->windom, ntresp); if(n < 0) return -1; From db0e50f5620c37f174b5b77e806a910337f419d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:41:21 +0000 Subject: [PATCH 16/45] README: document NLA in Options, High-level architecture, Repository layout Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/fb445df0-54c0-4d91-825a-1794c7f2c9ae Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- README.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) 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:** From 4cd0c93851517758f7ca218cd6a52edb1ea6d596 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:30:26 +0000 Subject: [PATCH 17/45] nla: add NTLMv1 Extended Session Security (ESS) support Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/edcc64e8-4b81-439b-b54f-d499378effb4 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 2 +- nla.c | 13 +++++++++---- nla_test.c | 2 +- rpc.c | 28 +++++++++++++++++++++++++--- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/fns.h b/fns.h index 76c6332..4b2ede1 100644 --- a/fns.h +++ b/fns.h @@ -42,7 +42,7 @@ uchar* tpdat(uchar*,uchar*); /* nla.c */ int mkntnego(uchar*, int); int getntchal(uchar[8], uchar*, int); -int mkntauth(uchar*, int, char*, char*, uchar*); +int mkntauth(uchar*, int, char*, char*, uchar*, uchar*); int writetsreq(int, uchar*, int); int readtsreq(int, uchar*, int); uchar* gettsreq(uchar*, int, int*); diff --git a/nla.c b/nla.c index d63388e..a44da76 100644 --- a/nla.c +++ b/nla.c @@ -23,6 +23,7 @@ enum NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ + NfESS = 0x00080000, /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ NTLMFlags = NfUnicode | NfReqTarget | NfNTLM | NfAlwaysSign, @@ -303,7 +304,7 @@ getntchal(uchar challenge[8], uchar *buf, int n) } int -mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen]) +mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen], uchar *lmresp) { uchar dom16[512], usr16[512]; int domlen, usrlen; @@ -314,7 +315,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] domlen = toutf16(dom16, sizeof dom16, domain, strlen(domain)); usrlen = toutf16(usr16, sizeof usr16, user, strlen(user)); - lmlen = NTRespLen; /* zeros for LM response */ + lmlen = NTRespLen; /* LmChallengeResponse length (24 bytes) */ domoff = 64; usroff = domoff + domlen; lmoff = usroff + usrlen; @@ -367,12 +368,16 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] p += 8; /* NegotiateFlags */ - PLONG(p, NTLMFlags); p += 4; + PLONG(p, NTLMFlags | (lmresp != nil ? NfESS : 0)); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ memmove(p, usr16, usrlen); p += usrlen; /* UserName */ - memset(p, 0, lmlen); p += lmlen; /* LmChallengeResponse */ + if(lmresp != nil) + memmove(p, lmresp, lmlen); /* ESS: client_nonce || zeros */ + else + memset(p, 0, lmlen); /* NTLMv1: all zeros */ + p += lmlen; memmove(p, ntresp, NTRespLen); p += NTRespLen; /* NtChallengeResponse */ return p - buf; diff --git a/nla_test.c b/nla_test.c index 306ec69..5801d21 100644 --- a/nla_test.c +++ b/nla_test.c @@ -211,7 +211,7 @@ testmkntauth(void) int n; memset(ntresp, 0x55, NTRespLen); - n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp); + n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, nil); if(n < 0) sysfatal("testmkntauth: unexpected error"); if(n < 64) diff --git a/rpc.c b/rpc.c index 3aec602..df08082 100644 --- a/rpc.c +++ b/rpc.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "dat.h" #include "fns.h" @@ -56,10 +57,13 @@ int nlahandshake(Rdp *c) { uchar ntnego[64], tsreqbuf[4096], ntauth[640]; - uchar challenge[8], ntresp[64]; + uchar challenge[8], chal[8], ntresp[64]; + uchar cnonce[8], tmp[16], md5out[MD5dlen]; + uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ char user[256]; uchar *ntp; int n, ntlen, nresp; + long srvflags; /* Phase A: NTLM Negotiate */ n = mkntnego(ntnego, sizeof ntnego); @@ -78,9 +82,27 @@ nlahandshake(Rdp *c) if(getntchal(challenge, ntp, ntlen) < 0) return -1; + /* Check if server requested Extended Session Security (ESS/NTLMv1-ESS) */ + srvflags = (ntlen >= 24) ? (long)GLONG(ntp+20) : 0; + lmrespptr = nil; + memmove(chal, challenge, 8); + if(srvflags & 0x00080000){ /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ + /* generate random 8-byte client nonce */ + genrandom(cnonce, sizeof cnonce); + /* ESS challenge = MD5(server_challenge || client_nonce)[0..7] */ + memmove(tmp, challenge, 8); + memmove(tmp+8, cnonce, 8); + md5(tmp, 16, md5out, nil); + memmove(chal, md5out, 8); + /* LM response = client_nonce (8 bytes) + zeros (16 bytes) */ + memmove(lmresp, cnonce, 8); + memset(lmresp+8, 0, 16); + lmrespptr = lmresp; + } + /* Ask factotum to compute the NT response for this challenge */ user[0] = '\0'; - nresp = auth_respond(challenge, 8, + nresp = auth_respond(chal, 8, user, sizeof(user)-1, ntresp, sizeof(ntresp), auth_getkey, @@ -102,7 +124,7 @@ nlahandshake(Rdp *c) } /* Phase C: NTLM Authenticate */ - n = mkntauth(ntauth, sizeof ntauth, c->user, c->windom, ntresp); + n = mkntauth(ntauth, sizeof ntauth, c->user, c->windom, ntresp, lmrespptr); if(n < 0) return -1; if(writetsreq(c->fd, ntauth, n) < 0) From ebceb92537fff3b1654dc89e2c797838c1c15aec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:34:19 +0000 Subject: [PATCH 18/45] nla: role=client in keyspec template; inline NTLMFlags Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/6fc53881-343c-4561-9758-5c0f8b9da7df Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 6 ++---- nla_test.c | 6 +++--- rpc.c | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/nla.c b/nla.c index a44da76..a043521 100644 --- a/nla.c +++ b/nla.c @@ -25,8 +25,6 @@ enum NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ NfESS = 0x00080000, /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ - NTLMFlags = NfUnicode | NfReqTarget | NfNTLM | NfAlwaysSign, - /* NTLM response size (NTLMv1) */ NTRespLen = 24, @@ -275,7 +273,7 @@ mkntnego(uchar *buf, int nbuf) p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NTLMFlags); p += 4; /* NegotiateFlags */ + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* NegotiateFlags */ memset(p, 0, 8); p += 8; /* DomainNameFields (empty) */ memset(p, 0, 8); p += 8; /* WorkstationFields (empty) */ return p - buf; @@ -368,7 +366,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] p += 8; /* NegotiateFlags */ - PLONG(p, NTLMFlags | (lmresp != nil ? NfESS : 0)); p += 4; + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ diff --git a/nla_test.c b/nla_test.c index 5801d21..5160bb9 100644 --- a/nla_test.c +++ b/nla_test.c @@ -19,9 +19,9 @@ testmkntnego(void) sysfatal("testmkntnego: bad signature"); if(GLONG(buf+8) != 1) sysfatal("testmkntnego: want MessageType=1, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+12) != NTLMFlags) + if(GLONG(buf+12) != (NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign)) sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", - NTLMFlags, (ulong)GLONG(buf+12)); + NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign, (ulong)GLONG(buf+12)); return 0; } @@ -220,7 +220,7 @@ testmkntauth(void) sysfatal("testmkntauth: bad signature"); if(GLONG(buf+8) != 3) sysfatal("testmkntauth: want MessageType=3, got %ld", (long)GLONG(buf+8)); - if(GLONG(buf+60) != NTLMFlags) + if(GLONG(buf+60) != (NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign)) sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } diff --git a/rpc.c b/rpc.c index df08082..b942531 100644 --- a/rpc.c +++ b/rpc.c @@ -106,7 +106,7 @@ nlahandshake(Rdp *c) user, sizeof(user)-1, ntresp, sizeof(ntresp), auth_getkey, - "proto=mschap service=rdp %s", c->keyspec); + "proto=mschap role=client service=rdp %s", c->keyspec); if(nresp < 0){ werrstr("factotum mschap: %r"); return -1; From 6bbba4bed9d9f8b00b6a76d29220858a0f822115 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:45:08 +0000 Subject: [PATCH 19/45] nla: add hex dump of NTLM Challenge packet for debugging Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/2b113228-78bd-416c-9e53-e692b6d5bfce Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rpc.c b/rpc.c index b942531..ce5daf7 100644 --- a/rpc.c +++ b/rpc.c @@ -62,7 +62,7 @@ nlahandshake(Rdp *c) uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ char user[256]; uchar *ntp; - int n, ntlen, nresp; + int n, ntlen, nresp, i; long srvflags; /* Phase A: NTLM Negotiate */ @@ -82,6 +82,12 @@ nlahandshake(Rdp *c) if(getntchal(challenge, ntp, ntlen) < 0) return -1; + /* 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"); + /* Check if server requested Extended Session Security (ESS/NTLMv1-ESS) */ srvflags = (ntlen >= 24) ? (long)GLONG(ntp+20) : 0; lmrespptr = nil; From cc190f933241858c703d8c2ee92ca5bc9580d486 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:07:05 +0000 Subject: [PATCH 20/45] nla: extract TargetName from NTLM Challenge and use as DomainName in Authenticate Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/b9f5058e-07c9-403f-88a8-cdd03ece6afb Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/rpc.c b/rpc.c index ce5daf7..f005c03 100644 --- a/rpc.c +++ b/rpc.c @@ -60,9 +60,9 @@ nlahandshake(Rdp *c) uchar challenge[8], chal[8], ntresp[64]; uchar cnonce[8], tmp[16], md5out[MD5dlen]; uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ - char user[256]; + char user[256], domfromchal[256], *dom; uchar *ntp; - int n, ntlen, nresp, i; + int n, ntlen, nresp, i, tlen, toff; long srvflags; /* Phase A: NTLM Negotiate */ @@ -106,6 +106,18 @@ nlahandshake(Rdp *c) lmrespptr = lmresp; } + /* 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; + } + } + /* Ask factotum to compute the NT response for this challenge */ user[0] = '\0'; nresp = auth_respond(chal, 8, @@ -130,7 +142,7 @@ nlahandshake(Rdp *c) } /* Phase C: NTLM Authenticate */ - n = mkntauth(ntauth, sizeof ntauth, c->user, c->windom, ntresp, lmrespptr); + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp, lmrespptr); if(n < 0) return -1; if(writetsreq(c->fd, ntauth, n) < 0) From daecabcfab96209ed41a2da6179a8a17f1a36c4d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 01:19:35 +0000 Subject: [PATCH 21/45] =?UTF-8?q?nla:=20fix=20NtChallengeResponse=20?= =?UTF-8?q?=E2=80=93=20use=20NTresp[24]=20from=20factotum=20MSchapreply,?= =?UTF-8?q?=20not=20LMresp[0]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/b5ac9ac7-b177-4f03-a1ea-c8c774e562bb Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 4 ++-- rpc.c | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/nla.c b/nla.c index a043521..46862eb 100644 --- a/nla.c +++ b/nla.c @@ -372,9 +372,9 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] memmove(p, dom16, domlen); p += domlen; /* DomainName */ memmove(p, usr16, usrlen); p += usrlen; /* UserName */ if(lmresp != nil) - memmove(p, lmresp, lmlen); /* ESS: client_nonce || zeros */ + memmove(p, lmresp, lmlen); /* ESS: cnonce||zeros; NTLMv1: factotum LMresp */ else - memset(p, 0, lmlen); /* NTLMv1: all zeros */ + memset(p, 0, lmlen); /* fallback: all zeros */ p += lmlen; memmove(p, ntresp, NTRespLen); p += NTRespLen; /* NtChallengeResponse */ diff --git a/rpc.c b/rpc.c index f005c03..5b4129b 100644 --- a/rpc.c +++ b/rpc.c @@ -129,7 +129,7 @@ nlahandshake(Rdp *c) werrstr("factotum mschap: %r"); return -1; } - if(nresp < 24){ /* NTRespLen */ + if(nresp < 2*24){ /* sizeof(MSchapreply) = LMresp[24] + NTresp[24] */ werrstr("factotum mschap: response too short (%d)", nresp); return -1; } @@ -142,7 +142,11 @@ nlahandshake(Rdp *c) } /* Phase C: NTLM Authenticate */ - n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp, lmrespptr); + /* + * factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24]. + * For ESS, use the pre-built lmresp (cnonce+zeros); otherwise use factotum's LMresp. + */ + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, lmrespptr != nil ? lmrespptr : ntresp); if(n < 0) return -1; if(writetsreq(c->fd, ntauth, n) < 0) From 0ce1b2827beff81bc6b5e32a9d6431c7f25442a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:05:18 +0000 Subject: [PATCH 22/45] nla: implement CredSSP credential delegation (Phases D and E) Store TLS server certificate in Rdp (tls*.c, dat.h); add to nla.c: certspki, ntsesskey, ntlmkeys, ntlmseal, mktspasswdcreds, mktscreds, mkpubkeyauth, mktsreqA, mktsreqE, writetsreqnonce, writetsreqdone, nlafinish. Update nlahandshake (rpc.c) to generate clientNonce, send Phase A with clientNonce, retrieve password via auth_getuserpasswd (proto=pass, fallback to -p), and call nlafinish for Phases D and E. Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/7b31e7cb-7398-4cb1-9430-b6f8dfac3d75 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- dat.h | 2 + fns.h | 3 + nla.c | 463 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- rpc.c | 45 ++++-- tls.c | 7 + tls9f.c | 7 + tlsp9p.c | 7 + 7 files changed, 517 insertions(+), 17 deletions(-) diff --git a/dat.h b/dat.h index f8d3dd8..b113d8c 100644 --- a/dat.h +++ b/dat.h @@ -52,6 +52,8 @@ struct Rdp char *rwd; /* remote working directory */ char *keyspec; /* factotum key spec */ int nla; /* use NLA (CredSSP/NTLM) authentication */ + 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/fns.h b/fns.h index 4b2ede1..11a9ba2 100644 --- a/fns.h +++ b/fns.h @@ -44,6 +44,9 @@ int mkntnego(uchar*, int); int getntchal(uchar[8], uchar*, int); int mkntauth(uchar*, int, char*, char*, uchar*, uchar*); 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*); int readtsreq(int, uchar*, int); uchar* gettsreq(uchar*, int, int*); diff --git a/nla.c b/nla.c index 46862eb..c83643e 100644 --- a/nla.c +++ b/nla.c @@ -2,17 +2,22 @@ * 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. - * The NT response is computed by factotum via auth_respond(2) - * with proto=mschap, so the plaintext password never leaves factotum. + * (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 { negoTokens = [NTLM Negotiate] } - * Server → Client: TSRequest { negoTokens = [NTLM Challenge] } - * Client → Server: TSRequest { negoTokens = [NTLM Authenticate] } + * 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" @@ -45,6 +50,9 @@ enum /* 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 */ CredSSPVer = 5, @@ -77,7 +85,351 @@ putder(uchar *p, int n) } /* - * Encode TSRequest { version=CredSSPVer, negoTokens=[{negoToken=ntlm}] } + * 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); +} + +/* + * 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 */ + rc4init(&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 { @@ -380,3 +732,100 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] 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 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 + * clnonce - 32-byte client nonce (sent in Phase A, per CredSSP v5) + * dom - Windows domain (for TSCredentials) + * user - username (for TSCredentials) + * pass - plaintext password (session key derivation + TSCredentials) + */ +int +nlafinish(int fd, uchar *cert, int certlen, uchar *clnonce, + char *dom, char *user, char *pass) +{ +uchar tsreqbuf[4096]; +uchar sesskey[MD5dlen], signkey[MD5dlen], sealkey[MD5dlen]; +uchar creds[2048], sealcreds[2048+16]; +uchar pubkeyauth[SHA2_256dlen]; +uchar *spki; +int n, spkilen; + +/* Phase D: read server's pubKeyAuth TSRequest */ +n = readtsreq(fd, tsreqbuf, sizeof tsreqbuf); +if(n < 0) +return -1; + +/* Derive NTLMv1 ExportedSessionKey, SignKey, SealKey */ +ntsesskey(pass, sesskey); +ntlmkeys(sesskey, signkey, sealkey); + +/* Extract SubjectPublicKeyInfo from server's TLS certificate */ +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; +} + +n = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, +sesskey, clnonce, spki, spkilen); +if(n < 0) +return -1; + +n = mktscreds(creds, sizeof creds, dom, user, pass); +if(n < 0) +return -1; + +n = ntlmseal(sealcreds, sizeof sealcreds, signkey, sealkey, 0, creds, n); +if(n < 0) +return -1; + +/* Phase E: send pubKeyAuth + authInfo (encrypted TSCredentials) */ +return writetsreqdone(fd, pubkeyauth, sizeof pubkeyauth, sealcreds, n); +} diff --git a/rpc.c b/rpc.c index 5b4129b..1a97dd2 100644 --- a/rpc.c +++ b/rpc.c @@ -56,20 +56,25 @@ x224handshake(Rdp* c) int nlahandshake(Rdp *c) { - uchar ntnego[64], tsreqbuf[4096], ntauth[640]; - uchar challenge[8], chal[8], ntresp[64]; - uchar cnonce[8], tmp[16], md5out[MD5dlen]; + uchar ntnego[64], ntauth[640]; + uchar challenge[8], chal[8], ntresp[64], tsreqbuf[4096]; + uchar esscnonce[8], tmp[16], md5out[MD5dlen]; uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ - char user[256], domfromchal[256], *dom; + uchar clnonce[32]; /* CredSSP v5 client nonce */ + char user[256], domfromchal[256], pass[256], *dom; uchar *ntp; int n, ntlen, nresp, i, tlen, toff; long srvflags; + UserPasswd *up; - /* Phase A: NTLM Negotiate */ + /* Generate CredSSP v5 client nonce (32 bytes random) */ + genrandom(clnonce, sizeof clnonce); + + /* Phase A: NTLM Negotiate (includes clientNonce for CredSSP v5) */ n = mkntnego(ntnego, sizeof ntnego); if(n < 0) return -1; - if(writetsreq(c->fd, ntnego, n) < 0) + if(writetsreqnonce(c->fd, ntnego, n, clnonce, sizeof clnonce) < 0) return -1; /* Phase B: NTLM Challenge */ @@ -94,14 +99,14 @@ nlahandshake(Rdp *c) memmove(chal, challenge, 8); if(srvflags & 0x00080000){ /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ /* generate random 8-byte client nonce */ - genrandom(cnonce, sizeof cnonce); + genrandom(esscnonce, sizeof esscnonce); /* ESS challenge = MD5(server_challenge || client_nonce)[0..7] */ memmove(tmp, challenge, 8); - memmove(tmp+8, cnonce, 8); + memmove(tmp+8, esscnonce, 8); md5(tmp, 16, md5out, nil); memmove(chal, md5out, 8); /* LM response = client_nonce (8 bytes) + zeros (16 bytes) */ - memmove(lmresp, cnonce, 8); + memmove(lmresp, esscnonce, 8); memset(lmresp+8, 0, 16); lmrespptr = lmresp; } @@ -152,7 +157,27 @@ nlahandshake(Rdp *c) if(writetsreq(c->fd, ntauth, n) < 0) return -1; - return 0; + /* Get password for session key derivation and TSCredentials. + * Try proto=pass first (preferred), then fall back to -p value. */ + pass[0] = '\0'; + up = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); + if(up != nil){ + 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(pass[0] == '\0'){ + werrstr("NLA: no password for credential delegation; " + "add 'proto=pass service=rdp' key to factotum or use -p"); + return -1; + } + + /* Phases D and E: read server pubKeyAuth, send TSCredentials */ + n = nlafinish(c->fd, c->tlscert, c->tlscertlen, clnonce, dom, c->user, pass); + memset(pass, 0, sizeof pass); + return n; } int 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); From 265865a77f478ae9cd6fba00481f4e16939a01c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:17:16 +0000 Subject: [PATCH 23/45] =?UTF-8?q?nla:=20fix=20build=20failures=20=E2=80=93?= =?UTF-8?q?=20restore=20missing=20/*,=20setupRC4state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/14036d0d-77e1-40ee-afd0-29d1d2d0c930 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nla.c b/nla.c index c83643e..6052d9e 100644 --- a/nla.c +++ b/nla.c @@ -201,7 +201,7 @@ ntlmseal(uchar *out, int nout, uchar *signkey, uchar *sealkey, return -1; } /* Encrypt message using SealKey; RC4 state advances */ - rc4init(&h, sealkey, MD5dlen); + setupRC4state(&h, sealkey, MD5dlen); memmove(out+16, msg, nmsg); rc4(&h, out+16, nmsg); @@ -429,8 +429,7 @@ mktsreqE(uchar *buf, int nbuf, uchar *pubkey, int pubkeylen, uchar *auth, int au return p - buf; } - - * +/* * ASN.1: * TSRequest ::= SEQUENCE { * version [0] INTEGER, From f64bdb5fe68cbe8415b627dc8793501a57b7173d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:29:41 +0000 Subject: [PATCH 24/45] =?UTF-8?q?nla:=20add=20debug=20prints=20to=20trace?= =?UTF-8?q?=20hang=20in=20CredSSP=20Phases=20C=E2=80=93E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/76ec15aa-882e-4c5b-8e86-ecb77be6974b Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 13 ++++++++++++- rpc.c | 9 +++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/nla.c b/nla.c index 6052d9e..aa15238 100644 --- a/nla.c +++ b/nla.c @@ -795,15 +795,19 @@ uchar *spki; int n, spkilen; /* 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 NTLMv1 ExportedSessionKey, SignKey, SealKey */ +fprint(2, "nla: deriving session/sign/seal keys\n"); ntsesskey(pass, sesskey); ntlmkeys(sesskey, signkey, sealkey); /* 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); @@ -811,20 +815,27 @@ if(spki == nil || spkilen <= 0){ werrstr("NLA: cannot extract server public key from TLS certificate"); return -1; } +fprint(2, "nla: SPKI extracted (%d bytes)\n", spkilen); n = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, sesskey, clnonce, spki, spkilen); if(n < 0) return -1; +fprint(2, "nla: pubKeyAuth computed (%d bytes)\n", n); n = mktscreds(creds, sizeof creds, dom, user, pass); if(n < 0) return -1; +fprint(2, "nla: TSCredentials encoded (%d bytes)\n", n); 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) */ -return writetsreqdone(fd, pubkeyauth, sizeof pubkeyauth, sealcreds, n); +fprint(2, "nla: sending Phase E (pubKeyAuth + authInfo)\n"); +n = writetsreqdone(fd, pubkeyauth, sizeof pubkeyauth, sealcreds, n); +fprint(2, "nla: Phase E sent (result=%d)\n", n); +return n; } diff --git a/rpc.c b/rpc.c index 1a97dd2..63a3c06 100644 --- a/rpc.c +++ b/rpc.c @@ -71,13 +71,16 @@ nlahandshake(Rdp *c) genrandom(clnonce, sizeof clnonce); /* Phase A: NTLM Negotiate (includes clientNonce for CredSSP v5) */ + fprint(2, "nla: sending Phase A (NTLM Negotiate)\n"); n = mkntnego(ntnego, sizeof ntnego); if(n < 0) return -1; if(writetsreqnonce(c->fd, ntnego, n, clnonce, sizeof clnonce) < 0) return -1; + fprint(2, "nla: Phase A sent (%d byte token)\n", n); /* Phase B: NTLM Challenge */ + fprint(2, "nla: reading Phase B (NTLM Challenge)\n"); n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); if(n < 0) return -1; @@ -87,6 +90,7 @@ nlahandshake(Rdp *c) 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++) @@ -151,14 +155,18 @@ nlahandshake(Rdp *c) * factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24]. * For ESS, use the pre-built lmresp (cnonce+zeros); otherwise use factotum's LMresp. */ + fprint(2, "nla: calling factotum mschap (user=%s, dom=%s)\n", c->user, dom); + fprint(2, "nla: sending Phase C (NTLM Authenticate)\n"); n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, lmrespptr != nil ? lmrespptr : ntresp); 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); /* Get password for session key derivation and TSCredentials. * Try proto=pass first (preferred), then fall back to -p value. */ + fprint(2, "nla: retrieving password for credential delegation\n"); pass[0] = '\0'; up = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); if(up != nil){ @@ -173,6 +181,7 @@ nlahandshake(Rdp *c) "add 'proto=pass service=rdp' key to factotum or use -p"); return -1; } + fprint(2, "nla: password obtained, calling nlafinish\n"); /* Phases D and E: read server pubKeyAuth, send TSCredentials */ n = nlafinish(c->fd, c->tlscert, c->tlscertlen, clnonce, dom, c->user, pass); From 416a63147b0e47bd413e834f24905d880f2334c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 02:36:32 +0000 Subject: [PATCH 25/45] =?UTF-8?q?nla:=20fix=20debug=20print=20ordering=20?= =?UTF-8?q?=E2=80=93=20move=20prints=20before=20blocking=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/44e62834-7b5c-4be3-9e32-2d116ee48035 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/rpc.c b/rpc.c index 63a3c06..9ef70e6 100644 --- a/rpc.c +++ b/rpc.c @@ -128,6 +128,7 @@ nlahandshake(Rdp *c) } /* Ask factotum to compute the NT response for this challenge */ + fprint(2, "nla: calling factotum mschap (keyspec=%s, dom=%s)\n", c->keyspec, dom); user[0] = '\0'; nresp = auth_respond(chal, 8, user, sizeof(user)-1, @@ -142,6 +143,7 @@ nlahandshake(Rdp *c) werrstr("factotum mschap: response too short (%d)", nresp); return -1; } + fprint(2, "nla: factotum returned user=%s nresp=%d\n", user, nresp); /* Use the user name returned by factotum if we don't have one */ if(user[0] != '\0' && c->user[0] == '\0'){ @@ -155,8 +157,7 @@ nlahandshake(Rdp *c) * factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24]. * For ESS, use the pre-built lmresp (cnonce+zeros); otherwise use factotum's LMresp. */ - fprint(2, "nla: calling factotum mschap (user=%s, dom=%s)\n", c->user, dom); - fprint(2, "nla: sending Phase C (NTLM Authenticate)\n"); + fprint(2, "nla: sending Phase C (NTLM Authenticate, user=%s, dom=%s)\n", c->user, dom); n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, lmrespptr != nil ? lmrespptr : ntresp); if(n < 0) return -1; @@ -166,7 +167,7 @@ nlahandshake(Rdp *c) /* Get password for session key derivation and TSCredentials. * Try proto=pass first (preferred), then fall back to -p value. */ - fprint(2, "nla: retrieving password for credential delegation\n"); + fprint(2, "nla: retrieving password (keyspec=%s)\n", c->keyspec); pass[0] = '\0'; up = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); if(up != nil){ @@ -181,7 +182,7 @@ nlahandshake(Rdp *c) "add 'proto=pass service=rdp' key to factotum or use -p"); return -1; } - fprint(2, "nla: password obtained, calling nlafinish\n"); + 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, clnonce, dom, c->user, pass); From 593fb44ce3dc8d540088c0eed2d660f49ff33700 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 03:09:55 +0000 Subject: [PATCH 26/45] nla: compute NT response directly from password; add ntrespfrompasswd using DES-ECB Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/39cc837c-4c9e-437f-900e-82f2c55bdb1c Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 1 + nla.c | 61 ++++++++++++++++++++++++++++++++++++++++ rpc.c | 90 ++++++++++++++++++++++++++++++++++++++--------------------- 3 files changed, 120 insertions(+), 32 deletions(-) diff --git a/fns.h b/fns.h index 11a9ba2..ae875c1 100644 --- a/fns.h +++ b/fns.h @@ -43,6 +43,7 @@ uchar* tpdat(uchar*,uchar*); int mkntnego(uchar*, int); int getntchal(uchar[8], uchar*, int); int mkntauth(uchar*, int, char*, char*, 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); diff --git a/nla.c b/nla.c index aa15238..fba114a 100644 --- a/nla.c +++ b/nla.c @@ -166,6 +166,67 @@ ntsesskey(char *pass, uchar sesskey[MD5dlen]) 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); + des_ecb_encrypt(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. diff --git a/rpc.c b/rpc.c index 9ef70e6..858b243 100644 --- a/rpc.c +++ b/rpc.c @@ -61,6 +61,7 @@ nlahandshake(Rdp *c) uchar esscnonce[8], tmp[16], md5out[MD5dlen]; uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ uchar clnonce[32]; /* CredSSP v5 client nonce */ + uchar ntresp_direct[24], *nt_for_auth, *lm_for_auth; char user[256], domfromchal[256], pass[256], *dom; uchar *ntp; int n, ntlen, nresp, i, tlen, toff; @@ -127,25 +128,66 @@ nlahandshake(Rdp *c) } } - /* Ask factotum to compute the NT response for this challenge */ - fprint(2, "nla: calling factotum mschap (keyspec=%s, dom=%s)\n", c->keyspec, dom); + /* + * 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'; - nresp = auth_respond(chal, 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; + 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(nresp < 2*24){ /* sizeof(MSchapreply) = LMresp[24] + NTresp[24] */ - werrstr("factotum mschap: response too short (%d)", nresp); - return -1; + 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); } - fprint(2, "nla: factotum returned user=%s nresp=%d\n", user, nresp); - /* Use the user name returned by factotum if we don't have one */ + if(pass[0] != '\0'){ + /* Compute NT response directly from password */ + fprint(2, "nla: computing NT response from password (user=%s, dom=%s)\n", user, dom); + ntrespfrompasswd(pass, chal, ntresp_direct); + nt_for_auth = ntresp_direct; + /* LM response: ESS uses cnonce+zeros; non-ESS uses zeros */ + if(lmrespptr != nil) + lm_for_auth = lmrespptr; + else{ + memset(lmresp, 0, sizeof lmresp); + lm_for_auth = lmresp; + } + }else{ + /* Fall back to factotum mschap */ + fprint(2, "nla: calling factotum mschap (keyspec=%s, dom=%s)\n", c->keyspec, dom); + nresp = auth_respond(chal, 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); + /* factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24] */ + nt_for_auth = ntresp + 24; + lm_for_auth = (lmrespptr != nil) ? lmrespptr : ntresp; + } + + /* 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) @@ -153,30 +195,14 @@ nlahandshake(Rdp *c) } /* Phase C: NTLM Authenticate */ - /* - * factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24]. - * For ESS, use the pre-built lmresp (cnonce+zeros); otherwise use factotum's LMresp. - */ fprint(2, "nla: sending Phase C (NTLM Authenticate, user=%s, dom=%s)\n", c->user, dom); - n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, lmrespptr != nil ? lmrespptr : ntresp); + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, nt_for_auth, lm_for_auth); 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); - /* Get password for session key derivation and TSCredentials. - * Try proto=pass first (preferred), then fall back to -p value. */ - fprint(2, "nla: retrieving password (keyspec=%s)\n", c->keyspec); - pass[0] = '\0'; - up = auth_getuserpasswd(auth_getkey, "proto=pass service=rdp %s", c->keyspec); - if(up != nil){ - 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(pass[0] == '\0'){ werrstr("NLA: no password for credential delegation; " "add 'proto=pass service=rdp' key to factotum or use -p"); From 9e58cc4bc988b037d819bc6444a3502473660ced Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:21:52 +0000 Subject: [PATCH 27/45] =?UTF-8?q?nla:=20fix=20CI=20failure=20=E2=80=94=20d?= =?UTF-8?q?es=5Fecb=5Fencrypt=20=E2=86=92=20desECBencrypt=20(Plan=209=20li?= =?UTF-8?q?bsec=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/872794bf-6810-4757-9b4e-b99709320dea Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nla.c b/nla.c index fba114a..391c093 100644 --- a/nla.c +++ b/nla.c @@ -196,7 +196,7 @@ ntlmdes(uchar *key7, uchar chal[8], uchar out[8]) des7to8(key7, key8); setupDESstate(&ds, key8, nil); memmove(out, chal, 8); - des_ecb_encrypt(out, 8, &ds); + desECBencrypt(out, 8, &ds); } /* From 575f4f0ea5a25422263e865c1db72464af39392a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:52:03 +0000 Subject: [PATCH 28/45] nla: downgrade to CredSSP v2 (no clientNonce), v2-compatible Phase E pubKeyAuth Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/f6820118-d147-4d26-8888-97e216ecd442 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 2 +- nla.c | 40 +++++++++++++++++++++++++++------------- rpc.c | 10 +++------- 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/fns.h b/fns.h index ae875c1..e1edd24 100644 --- a/fns.h +++ b/fns.h @@ -47,7 +47,7 @@ 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*); +int nlafinish(int, uchar*, int, char*, char*, char*); int readtsreq(int, uchar*, int); uchar* gettsreq(uchar*, int, int*); diff --git a/nla.c b/nla.c index 391c093..aa9a206 100644 --- a/nla.c +++ b/nla.c @@ -26,6 +26,8 @@ enum /* NTLM NegotiateFlags (subset used here) */ NfUnicode = 0x00000001, /* NTLMSSP_NEGOTIATE_UNICODE */ NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ + NfSign = 0x00000010, /* NTLMSSP_NEGOTIATE_SIGN */ + NfSeal = 0x00000020, /* NTLMSSP_NEGOTIATE_SEAL */ NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ NfESS = 0x00080000, /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ @@ -54,8 +56,8 @@ enum TSSpubKeyAuth = 3, /* TSRequest [3] pubKeyAuth field */ TSSclientNonce = 5, /* TSRequest [5] clientNonce field (version 5+) */ - /* CredSSP version advertised in TSRequest */ - CredSSPVer = 5, + /* CredSSP version advertised in TSRequest (v2 = minimum, max compatibility) */ + CredSSPVer = 2, }; static int @@ -685,7 +687,7 @@ mkntnego(uchar *buf, int nbuf) p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* NegotiateFlags */ + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign); p += 4; /* NegotiateFlags */ memset(p, 0, 8); p += 8; /* DomainNameFields (empty) */ memset(p, 0, 8); p += 8; /* WorkstationFields (empty) */ return p - buf; @@ -778,7 +780,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] p += 8; /* NegotiateFlags */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ @@ -845,15 +847,16 @@ return 0; * pass - plaintext password (session key derivation + TSCredentials) */ int -nlafinish(int fd, uchar *cert, int certlen, uchar *clnonce, +nlafinish(int fd, uchar *cert, int certlen, char *dom, char *user, char *pass) { uchar tsreqbuf[4096]; uchar sesskey[MD5dlen], signkey[MD5dlen], sealkey[MD5dlen]; uchar creds[2048], sealcreds[2048+16]; -uchar pubkeyauth[SHA2_256dlen]; +uchar pubkeyauth[2048+16]; /* NTLM EncryptMessage: 16-byte sig + SPKI */ +uchar spkibuf[2048]; uchar *spki; -int n, spkilen; +int n, spkilen, pubkeyauthlen; /* Phase D: read server's pubKeyAuth TSRequest */ fprint(2, "nla: reading Phase D (server pubKeyAuth)\n"); @@ -878,25 +881,36 @@ return -1; } fprint(2, "nla: SPKI extracted (%d bytes)\n", spkilen); -n = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, -sesskey, clnonce, spki, spkilen); -if(n < 0) +/* + * CredSSP v2 Phase E pubKeyAuth: + * increment the first byte of SPKI by 1 and encrypt with NTLM EncryptMessage (seqno=0). + * This proves to the server that we hold the same NTLM session key. + */ +if(spkilen > (int)sizeof spkibuf){ +werrstr("NLA: SPKI too large (%d)", spkilen); +return -1; +} +memmove(spkibuf, spki, spkilen); +spkibuf[0]++; /* increment first byte by 1 (MS-CSSP §3.1.5.1.1.1) */ +pubkeyauthlen = ntlmseal(pubkeyauth, sizeof pubkeyauth, + signkey, sealkey, 0, spkibuf, spkilen); +if(pubkeyauthlen < 0) return -1; -fprint(2, "nla: pubKeyAuth computed (%d bytes)\n", n); +fprint(2, "nla: pubKeyAuth computed (%d bytes)\n", pubkeyauthlen); n = mktscreds(creds, sizeof creds, dom, user, pass); if(n < 0) return -1; fprint(2, "nla: TSCredentials encoded (%d bytes)\n", n); -n = ntlmseal(sealcreds, sizeof sealcreds, signkey, sealkey, 0, creds, n); +n = ntlmseal(sealcreds, sizeof sealcreds, signkey, sealkey, 1, 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, sizeof pubkeyauth, sealcreds, n); +n = writetsreqdone(fd, pubkeyauth, pubkeyauthlen, sealcreds, n); fprint(2, "nla: Phase E sent (result=%d)\n", n); return n; } diff --git a/rpc.c b/rpc.c index 858b243..8d088b6 100644 --- a/rpc.c +++ b/rpc.c @@ -60,7 +60,6 @@ nlahandshake(Rdp *c) uchar challenge[8], chal[8], ntresp[64], tsreqbuf[4096]; uchar esscnonce[8], tmp[16], md5out[MD5dlen]; uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ - uchar clnonce[32]; /* CredSSP v5 client nonce */ uchar ntresp_direct[24], *nt_for_auth, *lm_for_auth; char user[256], domfromchal[256], pass[256], *dom; uchar *ntp; @@ -68,15 +67,12 @@ nlahandshake(Rdp *c) long srvflags; UserPasswd *up; - /* Generate CredSSP v5 client nonce (32 bytes random) */ - genrandom(clnonce, sizeof clnonce); - - /* Phase A: NTLM Negotiate (includes clientNonce for CredSSP v5) */ + /* Phase A: NTLM Negotiate (CredSSP v2, no clientNonce) */ fprint(2, "nla: sending Phase A (NTLM Negotiate)\n"); n = mkntnego(ntnego, sizeof ntnego); if(n < 0) return -1; - if(writetsreqnonce(c->fd, ntnego, n, clnonce, sizeof clnonce) < 0) + if(writetsreq(c->fd, ntnego, n) < 0) return -1; fprint(2, "nla: Phase A sent (%d byte token)\n", n); @@ -211,7 +207,7 @@ nlahandshake(Rdp *c) 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, clnonce, dom, c->user, pass); + n = nlafinish(c->fd, c->tlscert, c->tlscertlen, dom, c->user, pass); memset(pass, 0, sizeof pass); return n; } From dac5014397c47e5f36d6a05033f60eaf15ddcbd1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:42:56 +0000 Subject: [PATCH 29/45] nla: fix testmkntnego/testmkntauth flags; fix NfESS incorrectly set without ESS Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/a98be0c6-ab2e-4ac1-901a-f6c3bf3e069f Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla_test.c | 6 +++--- rpc.c | 12 ++++-------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/nla_test.c b/nla_test.c index 5160bb9..271585e 100644 --- a/nla_test.c +++ b/nla_test.c @@ -19,9 +19,9 @@ testmkntnego(void) 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|NfNTLM|NfAlwaysSign)) + if(GLONG(buf+12) != (NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign)) sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", - NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign, (ulong)GLONG(buf+12)); + NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign, (ulong)GLONG(buf+12)); return 0; } @@ -220,7 +220,7 @@ testmkntauth(void) 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|NfNTLM|NfAlwaysSign)) + if(GLONG(buf+60) != (NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign)) sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } diff --git a/rpc.c b/rpc.c index 8d088b6..21fcb2c 100644 --- a/rpc.c +++ b/rpc.c @@ -154,13 +154,8 @@ nlahandshake(Rdp *c) fprint(2, "nla: computing NT response from password (user=%s, dom=%s)\n", user, dom); ntrespfrompasswd(pass, chal, ntresp_direct); nt_for_auth = ntresp_direct; - /* LM response: ESS uses cnonce+zeros; non-ESS uses zeros */ - if(lmrespptr != nil) - lm_for_auth = lmrespptr; - else{ - memset(lmresp, 0, sizeof lmresp); - lm_for_auth = lmresp; - } + /* LM response: nil for non-ESS (no NfESS); ESS cnonce+zeros otherwise */ + lm_for_auth = lmrespptr; }else{ /* Fall back to factotum mschap */ fprint(2, "nla: calling factotum mschap (keyspec=%s, dom=%s)\n", c->keyspec, dom); @@ -180,7 +175,8 @@ nlahandshake(Rdp *c) fprint(2, "nla: factotum returned user=%s nresp=%d\n", user, nresp); /* factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24] */ nt_for_auth = ntresp + 24; - lm_for_auth = (lmrespptr != nil) ? lmrespptr : ntresp; + /* nil for non-ESS so NfESS is not set; ESS cnonce+zeros otherwise */ + lm_for_auth = lmrespptr; } /* Propagate user name if not yet set on the connection */ From 2cf18f25fa71b5e3df395f5c57ea2920c86f6725 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:03:36 +0000 Subject: [PATCH 30/45] nla: fix testmktsreqhdr expected version byte (CredSSPVer=2 not 5) Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/c65e7b77-6fb5-464b-9116-c1beae548a01 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla_test.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nla_test.c b/nla_test.c index 271585e..0e7e7e9 100644 --- a/nla_test.c +++ b/nla_test.c @@ -127,7 +127,7 @@ testgetntchalbadtype(void) * single-byte NTLM token (0xAA). * * 30 10 SEQUENCE(16) - * a0 03 02 01 05 [0] version=5 + * a0 03 02 01 02 [0] version=2 (CredSSPVer) * a1 09 [1] negoTokens(9) * 30 07 SEQUENCE OF(7) * 30 05 SEQUENCE(5) @@ -140,7 +140,7 @@ testmktsreqhdr(void) uchar token[] = {0xAA}; uchar want[] = { 0x30, 0x10, - 0xa0, 0x03, 0x02, 0x01, 0x05, + 0xa0, 0x03, 0x02, 0x01, CredSSPVer, 0xa1, 0x09, 0x30, 0x07, 0x30, 0x05, From 10b7e03740d27a3516cbd3970db8a6028ddaa020 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:16:40 +0000 Subject: [PATCH 31/45] nla: implement Early User Authorization Result PDU (PROTOCOL_HYBRID_EX) Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/58955cb2-2f9a-4aed-b282-db3da18fc678 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- msg_test.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ rpc.c | 25 +++++++++++++++++++++++-- 2 files changed, 67 insertions(+), 2 deletions(-) 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/rpc.c b/rpc.c index 21fcb2c..51a8336 100644 --- a/rpc.c +++ b/rpc.c @@ -23,7 +23,9 @@ x224handshake(Rdp* c) Msg t, r; t.type = Xconnect; - t.negproto = c->nla ? ProtoCSSP : 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) @@ -33,7 +35,8 @@ x224handshake(Rdp* c) return -1; } if(c->nla){ - if((r.negproto&ProtoCSSP) == 0){ + /* server may select HYBRID (ProtoCSSP) or HYBRID_EX (ProtoUAUTH) */ + if((r.negproto & (ProtoCSSP | ProtoUAUTH)) == 0){ werrstr("server refused CredSSP"); return -1; } @@ -50,6 +53,24 @@ x224handshake(Rdp* c) 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; } From 26c3a91fc0ee635497a529c53722d1e109a3f0e2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 19:49:11 +0000 Subject: [PATCH 32/45] nla: detect EUARP at Phase B in PROTOCOL_HYBRID_EX mode Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/f3815d96-286b-4323-9883-2f4e3f600712 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 1 + nla.c | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ rpc.c | 17 ++++++++++++-- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/fns.h b/fns.h index e1edd24..c877e1d 100644 --- a/fns.h +++ b/fns.h @@ -49,6 +49,7 @@ int writetsreqnonce(int, uchar*, int, uchar*, int); int writetsreqdone(int, uchar*, int, uchar*, int); int nlafinish(int, uchar*, int, char*, char*, char*); int readtsreq(int, uchar*, int); +int readtsreq_oreuarp(int, uchar*, int, ulong*); uchar* gettsreq(uchar*, int, int*); /* rpc.c */ diff --git a/nla.c b/nla.c index aa9a206..f2fb7ab 100644 --- a/nla.c +++ b/nla.c @@ -671,6 +671,77 @@ readtsreq(int fd, uchar *buf, int nbuf) 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. diff --git a/rpc.c b/rpc.c index 51a8336..58ae08f 100644 --- a/rpc.c +++ b/rpc.c @@ -97,9 +97,22 @@ nlahandshake(Rdp *c) return -1; fprint(2, "nla: Phase A sent (%d byte token)\n", n); - /* Phase B: NTLM Challenge */ + /* 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"); - n = readtsreq(c->fd, tsreqbuf, sizeof tsreqbuf); + 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); From acd19af5cf4a75775eec7fc6302a55ff05e9bc9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:41:56 +0000 Subject: [PATCH 33/45] nla: upgrade to CredSSP v5 (clientNonce + HMAC-SHA256 pubKeyAuth); fix flag defs Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/a71a69eb-c49f-4c88-887c-81bdcfaa1285 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 2 +- nla.c | 45 +++++++++++++++++++-------------------------- nla_test.c | 2 +- rpc.c | 8 +++++--- 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/fns.h b/fns.h index c877e1d..3193dae 100644 --- a/fns.h +++ b/fns.h @@ -47,7 +47,7 @@ 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, char*, char*, char*); +int nlafinish(int, uchar*, int, uchar*, char*, char*, char*); int readtsreq(int, uchar*, int); int readtsreq_oreuarp(int, uchar*, int, ulong*); uchar* gettsreq(uchar*, int, int*); diff --git a/nla.c b/nla.c index f2fb7ab..34277f3 100644 --- a/nla.c +++ b/nla.c @@ -24,13 +24,13 @@ enum { /* NTLM NegotiateFlags (subset used here) */ - NfUnicode = 0x00000001, /* NTLMSSP_NEGOTIATE_UNICODE */ - NfReqTarget = 0x00000004, /* NTLMSSP_REQUEST_TARGET */ - NfSign = 0x00000010, /* NTLMSSP_NEGOTIATE_SIGN */ - NfSeal = 0x00000020, /* NTLMSSP_NEGOTIATE_SEAL */ - NfNTLM = 0x00000200, /* NTLMSSP_NEGOTIATE_NTLM */ - NfAlwaysSign = 0x00008000, /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ - NfESS = 0x00080000, /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ + 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 */ /* NTLM response size (NTLMv1) */ NTRespLen = 24, @@ -56,8 +56,8 @@ enum TSSpubKeyAuth = 3, /* TSRequest [3] pubKeyAuth field */ TSSclientNonce = 5, /* TSRequest [5] clientNonce field (version 5+) */ - /* CredSSP version advertised in TSRequest (v2 = minimum, max compatibility) */ - CredSSPVer = 2, + /* CredSSP version advertised in TSRequest (v5 = required by Windows 10+) */ + CredSSPVer = 5, }; static int @@ -905,27 +905,26 @@ return 0; } /* - * Complete the CredSSP handshake (Phases D and E). + * 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 - * clnonce - 32-byte client nonce (sent in Phase A, per CredSSP v5) + * cnonce - 32-byte client nonce sent in Phase A (CredSSP v5) * dom - Windows domain (for TSCredentials) * user - username (for TSCredentials) * pass - plaintext password (session key derivation + TSCredentials) */ int -nlafinish(int fd, uchar *cert, int certlen, +nlafinish(int fd, uchar *cert, int certlen, uchar *cnonce, char *dom, char *user, char *pass) { uchar tsreqbuf[4096]; uchar sesskey[MD5dlen], signkey[MD5dlen], sealkey[MD5dlen]; uchar creds[2048], sealcreds[2048+16]; -uchar pubkeyauth[2048+16]; /* NTLM EncryptMessage: 16-byte sig + SPKI */ -uchar spkibuf[2048]; +uchar pubkeyauth[SHA2_256dlen]; uchar *spki; int n, spkilen, pubkeyauthlen; @@ -953,18 +952,11 @@ return -1; fprint(2, "nla: SPKI extracted (%d bytes)\n", spkilen); /* - * CredSSP v2 Phase E pubKeyAuth: - * increment the first byte of SPKI by 1 and encrypt with NTLM EncryptMessage (seqno=0). - * This proves to the server that we hold the same NTLM session key. + * 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) */ -if(spkilen > (int)sizeof spkibuf){ -werrstr("NLA: SPKI too large (%d)", spkilen); -return -1; -} -memmove(spkibuf, spki, spkilen); -spkibuf[0]++; /* increment first byte by 1 (MS-CSSP §3.1.5.1.1.1) */ -pubkeyauthlen = ntlmseal(pubkeyauth, sizeof pubkeyauth, - signkey, sealkey, 0, spkibuf, spkilen); +pubkeyauthlen = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, sesskey, cnonce, spki, spkilen); if(pubkeyauthlen < 0) return -1; fprint(2, "nla: pubKeyAuth computed (%d bytes)\n", pubkeyauthlen); @@ -974,7 +966,8 @@ if(n < 0) return -1; fprint(2, "nla: TSCredentials encoded (%d bytes)\n", n); -n = ntlmseal(sealcreds, sizeof sealcreds, signkey, sealkey, 1, creds, 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); diff --git a/nla_test.c b/nla_test.c index 0e7e7e9..96d4977 100644 --- a/nla_test.c +++ b/nla_test.c @@ -127,7 +127,7 @@ testgetntchalbadtype(void) * single-byte NTLM token (0xAA). * * 30 10 SEQUENCE(16) - * a0 03 02 01 02 [0] version=2 (CredSSPVer) + * a0 03 02 01 05 [0] version=5 (CredSSPVer) * a1 09 [1] negoTokens(9) * 30 07 SEQUENCE OF(7) * 30 05 SEQUENCE(5) diff --git a/rpc.c b/rpc.c index 58ae08f..cf7bbf8 100644 --- a/rpc.c +++ b/rpc.c @@ -82,18 +82,20 @@ nlahandshake(Rdp *c) uchar esscnonce[8], tmp[16], md5out[MD5dlen]; uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ uchar ntresp_direct[24], *nt_for_auth, *lm_for_auth; + uchar cnonce[32]; /* CredSSP v5 client nonce */ char user[256], domfromchal[256], pass[256], *dom; uchar *ntp; int n, ntlen, nresp, i, tlen, toff; long srvflags; UserPasswd *up; - /* Phase A: NTLM Negotiate (CredSSP v2, no clientNonce) */ + /* 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; - if(writetsreq(c->fd, ntnego, n) < 0) + 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); @@ -237,7 +239,7 @@ nlahandshake(Rdp *c) 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, dom, c->user, pass); + n = nlafinish(c->fd, c->tlscert, c->tlscertlen, cnonce, dom, c->user, pass); memset(pass, 0, sizeof pass); return n; } From 7e3a54167f700be5be624416b0484ded6709b277 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:20:21 +0000 Subject: [PATCH 34/45] nla: remove NfSign|NfSeal from NTLM Negotiate/Authenticate flags Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/d1ad494a-0b72-42a2-8a93-ac27c8f4c124 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 6 ++---- nla_test.c | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/nla.c b/nla.c index 34277f3..5fbfb9c 100644 --- a/nla.c +++ b/nla.c @@ -26,8 +26,6 @@ enum /* NTLM NegotiateFlags (subset used here) */ 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 */ @@ -758,7 +756,7 @@ mkntnego(uchar *buf, int nbuf) p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign); p += 4; /* NegotiateFlags */ + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* NegotiateFlags */ memset(p, 0, 8); p += 8; /* DomainNameFields (empty) */ memset(p, 0, 8); p += 8; /* WorkstationFields (empty) */ return p - buf; @@ -851,7 +849,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] p += 8; /* NegotiateFlags */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ diff --git a/nla_test.c b/nla_test.c index 96d4977..80820ce 100644 --- a/nla_test.c +++ b/nla_test.c @@ -19,9 +19,9 @@ testmkntnego(void) 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|NfNTLM|NfSign|NfSeal|NfAlwaysSign)) + if(GLONG(buf+12) != (NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign)) sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", - NfUnicode|NfReqTarget|NfNTLM|NfSign|NfSeal|NfAlwaysSign, (ulong)GLONG(buf+12)); + NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign, (ulong)GLONG(buf+12)); return 0; } @@ -220,7 +220,7 @@ testmkntauth(void) 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|NfNTLM|NfSign|NfSeal|NfAlwaysSign)) + if(GLONG(buf+60) != (NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign)) sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } From 8b4de13223ebd96daca22157ca912b32aeee89c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:04:23 +0000 Subject: [PATCH 35/45] nla: implement NTLMv2 authentication (required by Windows 10+) - Add getntargetinfo: extract TargetInfo AvPairs from NTLM Challenge - Add getavtimestamp: find MsvAvTimestamp in TargetInfo (for NTLMv2 blob) - Add ntv2frompasswd: compute NTLMv2 NT/LM responses and ExportedSessionKey (ResponseKeyNT = HMAC_MD5(NT_hash, UNICODE(uppercase(user)||domain)), NtProofStr = HMAC_MD5(ResponseKeyNT, svchal||blob), ExportedSessionKey = HMAC_MD5(ResponseKeyNT, NtProofStr)) - Modify mkntauth: accept variable-length NT response (NTLMv2 is >24 bytes); remove ESS flag conditional (NTLMv2 does not use NTLMSSP_NEGOTIATE_ESS) - Modify nlafinish: accept pre-computed ExportedSessionKey parameter instead of re-deriving from password (NTLMv2 key depends on NtProofStr) - Update nlahandshake: extract TargetInfo, generate 8-byte client challenge, call ntv2frompasswd, pass ExportedSessionKey to nlafinish; keep factotum mschap NTLMv1 fallback (24-byte response) for non-password auth paths - Update fns.h prototypes; update nla_test.c for new mkntauth signature Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/10669e98-e03f-4d33-8726-9303072cd21f Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 6 +- nla.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++++----- nla_test.c | 2 +- rpc.c | 71 +++++++++---------- 4 files changed, 216 insertions(+), 61 deletions(-) diff --git a/fns.h b/fns.h index 3193dae..e77374c 100644 --- a/fns.h +++ b/fns.h @@ -42,12 +42,14 @@ uchar* tpdat(uchar*,uchar*); /* nla.c */ int mkntnego(uchar*, int); int getntchal(uchar[8], uchar*, int); -int mkntauth(uchar*, int, char*, char*, uchar*, uchar*); +uchar* getntargetinfo(uchar*, int, int*); +int ntv2frompasswd(char*, char*, char*, uchar*, uchar*, uchar*, int, uchar*, int, uchar*, uchar*); +int mkntauth(uchar*, int, char*, char*, uchar*, int, 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*); +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*); diff --git a/nla.c b/nla.c index 5fbfb9c..af07fbd 100644 --- a/nla.c +++ b/nla.c @@ -30,8 +30,10 @@ enum NfAlwaysSign = (1<<15), /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ NfESS = (1<<19), /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ - /* NTLM response size (NTLMv1) */ - NTRespLen = 24, + /* NTLM response sizes */ + NTRespLen = 24, /* NTLMv1 NT/LM response length */ + MaxNTLMTargetInfo = 1024, /* maximum TargetInfo AvPairs length */ + NTv2RespMax = 16 + 32 + MaxNTLMTargetInfo, /* max NTLMv2 NtChallengeResponse */ /* ASN.1 Universal tags (BER/DER) */ TagInt = 2, /* INTEGER */ @@ -784,8 +786,168 @@ getntchal(uchar challenge[8], uchar *buf, int n) 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 NTLMv2 NT and LM challenge responses and the ExportedSessionKey. + * + * 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 ExportedSessionKey (16 bytes; used for CredSSP key derivation) + * + * NtChallengeResponse = NtProofStr[16] ‖ Blob[32+tilen] + * LmChallengeResponse = HMAC_MD5(ResponseKeyNT, svchal‖cchal) ‖ cchal (24 bytes) + * ExportedSessionKey = HMAC_MD5(ResponseKeyNT, NtProofStr) + * + * Returns the length of NtChallengeResponse written, or -1 on error. + */ int -mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen], uchar *lmresp) +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 nthash[MD4dlen], rkey[MD5dlen], ntproofstr[MD5dlen]; + uchar blob[32 + MaxNTLMTargetInfo]; + uchar unidata[1024], unipass[256], ts[8]; + DigestState *ds; + Rune r; + char *p; + int n, bloblen; + uchar *w; + + if(tilen > MaxNTLMTargetInfo){ + werrstr("ntv2frompasswd: TargetInfo too large (%d)", tilen); + return -1; + } + bloblen = 32 + tilen; + 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): + * [0] RespType = 0x01 + * [1] HiRespType = 0x01 + * [2-7] Reserved (6 zero bytes) + * [8-15] Timestamp (MsvAvTimestamp or zeros) + * [16-23] ClientChallenge + * [24-27] Reserved (4 zero bytes) + * [28..] TargetInfo (verbatim from Challenge) + * [28+tilen..31+tilen] Reserved (4 zero bytes) + */ + 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(ti != nil && tilen > 0) + memmove(blob + 28, ti, tilen); + + /* 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 dom16[512], usr16[512]; int domlen, usrlen; @@ -796,12 +958,12 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] domlen = toutf16(dom16, sizeof dom16, domain, strlen(domain)); usrlen = toutf16(usr16, sizeof usr16, user, strlen(user)); - lmlen = NTRespLen; /* LmChallengeResponse length (24 bytes) */ + lmlen = NTRespLen; /* LmChallengeResponse is always 24 bytes */ domoff = 64; usroff = domoff + domlen; lmoff = usroff + usrlen; ntoff = lmoff + lmlen; - total = ntoff + NTRespLen; + total = ntoff + ntresplen; if(total > nbuf){ werrstr("mkntauth: buffer too small (%d < %d)", nbuf, total); @@ -819,8 +981,8 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] p += 8; /* NtChallengeResponseFields */ - PSHORT(p, NTRespLen); - PSHORT(p+2, NTRespLen); + PSHORT(p, ntresplen); + PSHORT(p+2, ntresplen); PLONG(p+4, ntoff); p += 8; @@ -839,27 +1001,27 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar ntresp[NTRespLen] /* WorkstationFields (empty) */ PSHORT(p, 0); PSHORT(p+2, 0); - PLONG(p+4, lmoff); /* offset points to lm area; length is 0 */ + PLONG(p+4, lmoff); p += 8; /* EncryptedRandomSessionKeyFields (empty) */ PSHORT(p, 0); PSHORT(p+2, 0); - PLONG(p+4, ntoff+NTRespLen); + PLONG(p+4, ntoff+ntresplen); p += 8; /* NegotiateFlags */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign | (lmresp != nil ? NfESS : 0)); p += 4; + PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* payload */ memmove(p, dom16, domlen); p += domlen; /* DomainName */ memmove(p, usr16, usrlen); p += usrlen; /* UserName */ if(lmresp != nil) - memmove(p, lmresp, lmlen); /* ESS: cnonce||zeros; NTLMv1: factotum LMresp */ + memmove(p, lmresp, lmlen); /* LmChallengeResponse */ else memset(p, 0, lmlen); /* fallback: all zeros */ p += lmlen; - memmove(p, ntresp, NTRespLen); p += NTRespLen; /* NtChallengeResponse */ + memmove(p, ntresp, ntresplen); p += ntresplen; /* NtChallengeResponse */ return p - buf; } @@ -913,14 +1075,15 @@ return 0; * cnonce - 32-byte client nonce sent in Phase A (CredSSP v5) * dom - Windows domain (for TSCredentials) * user - username (for TSCredentials) - * pass - plaintext password (session key derivation + 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) + char *dom, char *user, char *pass, uchar sesskey[MD5dlen]) { uchar tsreqbuf[4096]; -uchar sesskey[MD5dlen], signkey[MD5dlen], sealkey[MD5dlen]; +uchar signkey[MD5dlen], sealkey[MD5dlen]; uchar creds[2048], sealcreds[2048+16]; uchar pubkeyauth[SHA2_256dlen]; uchar *spki; @@ -933,9 +1096,8 @@ if(n < 0) return -1; fprint(2, "nla: Phase D received (%d bytes)\n", n); -/* Derive NTLMv1 ExportedSessionKey, SignKey, SealKey */ -fprint(2, "nla: deriving session/sign/seal keys\n"); -ntsesskey(pass, sesskey); +/* Derive SignKey and SealKey from ExportedSessionKey (computed during Phase C) */ +fprint(2, "nla: deriving sign/seal keys from session key\n"); ntlmkeys(sesskey, signkey, sealkey); /* Extract SubjectPublicKeyInfo from server's TLS certificate */ diff --git a/nla_test.c b/nla_test.c index 80820ce..ad0054c 100644 --- a/nla_test.c +++ b/nla_test.c @@ -211,7 +211,7 @@ testmkntauth(void) int n; memset(ntresp, 0x55, NTRespLen); - n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, nil); + n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, NTRespLen, nil); if(n < 0) sysfatal("testmkntauth: unexpected error"); if(n < 64) diff --git a/rpc.c b/rpc.c index cf7bbf8..1acf3b7 100644 --- a/rpc.c +++ b/rpc.c @@ -77,16 +77,19 @@ x224handshake(Rdp* c) int nlahandshake(Rdp *c) { - uchar ntnego[64], ntauth[640]; - uchar challenge[8], chal[8], ntresp[64], tsreqbuf[4096]; - uchar esscnonce[8], tmp[16], md5out[MD5dlen]; - uchar lmresp[24], *lmrespptr; /* LmChallengeResponse is 24 bytes */ - uchar ntresp_direct[24], *nt_for_auth, *lm_for_auth; - uchar cnonce[32]; /* CredSSP v5 client nonce */ + uchar ntnego[64], ntauth[2048]; + uchar challenge[8], tsreqbuf[4096]; + uchar cchal[8]; /* NTLMv2 client challenge */ + uchar lmv2resp[24]; /* NTLMv2 LmChallengeResponse */ + uchar ntv2resp[16 + 32 + 1024]; /* NTLMv2 NtChallengeResponse */ + uchar ntresp[64]; /* factotum mschap NTLMv1 fallback */ + uchar sesskey[MD5dlen]; /* NTLMv2 ExportedSessionKey */ + uchar cnonce[32]; /* CredSSP v5 client nonce */ char user[256], domfromchal[256], pass[256], *dom; - uchar *ntp; - int n, ntlen, nresp, i, tlen, toff; - long srvflags; + uchar *ntp, *ti; + int n, ntlen, ntv2len, nresp, tilen, i, tlen, toff; + + ntv2len = 0; UserPasswd *up; /* Phase A: NTLM Negotiate (CredSSP v5, with clientNonce) */ @@ -130,23 +133,10 @@ nlahandshake(Rdp *c) fprint(2, " %02ux", ntp[i]); fprint(2, "\n"); - /* Check if server requested Extended Session Security (ESS/NTLMv1-ESS) */ - srvflags = (ntlen >= 24) ? (long)GLONG(ntp+20) : 0; - lmrespptr = nil; - memmove(chal, challenge, 8); - if(srvflags & 0x00080000){ /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ - /* generate random 8-byte client nonce */ - genrandom(esscnonce, sizeof esscnonce); - /* ESS challenge = MD5(server_challenge || client_nonce)[0..7] */ - memmove(tmp, challenge, 8); - memmove(tmp+8, esscnonce, 8); - md5(tmp, 16, md5out, nil); - memmove(chal, md5out, 8); - /* LM response = client_nonce (8 bytes) + zeros (16 bytes) */ - memmove(lmresp, esscnonce, 8); - memset(lmresp+8, 0, 16); - lmrespptr = lmresp; - } + /* 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; @@ -186,16 +176,18 @@ nlahandshake(Rdp *c) } if(pass[0] != '\0'){ - /* Compute NT response directly from password */ - fprint(2, "nla: computing NT response from password (user=%s, dom=%s)\n", user, dom); - ntrespfrompasswd(pass, chal, ntresp_direct); - nt_for_auth = ntresp_direct; - /* LM response: nil for non-ESS (no NfESS); ESS cnonce+zeros otherwise */ - lm_for_auth = lmrespptr; + /* 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); + ntv2len = ntv2frompasswd(pass, user, dom, + challenge, cchal, ti, tilen, + ntv2resp, sizeof ntv2resp, lmv2resp, sesskey); + if(ntv2len < 0) + return -1; }else{ - /* Fall back to factotum mschap */ + /* 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(chal, 8, + nresp = auth_respond(challenge, 8, user, sizeof(user)-1, ntresp, sizeof(ntresp), auth_getkey, @@ -209,10 +201,6 @@ nlahandshake(Rdp *c) return -1; } fprint(2, "nla: factotum returned user=%s nresp=%d\n", user, nresp); - /* factotum mschap returns MSchapreply: LMresp[24] at [0], NTresp[24] at [24] */ - nt_for_auth = ntresp + 24; - /* nil for non-ESS so NfESS is not set; ESS cnonce+zeros otherwise */ - lm_for_auth = lmrespptr; } /* Propagate user name if not yet set on the connection */ @@ -224,7 +212,10 @@ nlahandshake(Rdp *c) /* Phase C: NTLM Authenticate */ fprint(2, "nla: sending Phase C (NTLM Authenticate, user=%s, dom=%s)\n", c->user, dom); - n = mkntauth(ntauth, sizeof ntauth, c->user, dom, nt_for_auth, lm_for_auth); + if(pass[0] != '\0') + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntv2resp, ntv2len, lmv2resp); + else + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, 24, nil); if(n < 0) return -1; if(writetsreq(c->fd, ntauth, n) < 0) @@ -239,7 +230,7 @@ nlahandshake(Rdp *c) 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); + n = nlafinish(c->fd, c->tlscert, c->tlscertlen, cnonce, dom, c->user, pass, sesskey); memset(pass, 0, sizeof pass); return n; } From ea1b5ff62a69c90fc89a2d156aa2870dfbba567b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:06:33 +0000 Subject: [PATCH 36/45] nla: address code review feedback: zero sesskey, add NfESS comment Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/10669e98-e03f-4d33-8726-9303072cd21f Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 2 +- rpc.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/nla.c b/nla.c index af07fbd..726d301 100644 --- a/nla.c +++ b/nla.c @@ -1010,7 +1010,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntre PLONG(p+4, ntoff+ntresplen); p += 8; - /* NegotiateFlags */ + /* NegotiateFlags: no NfESS — NTLMv2 uses its own client challenge in the blob */ PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* payload */ diff --git a/rpc.c b/rpc.c index 1acf3b7..fcf70b9 100644 --- a/rpc.c +++ b/rpc.c @@ -88,9 +88,10 @@ nlahandshake(Rdp *c) char user[256], domfromchal[256], pass[256], *dom; uchar *ntp, *ti; int n, ntlen, ntv2len, nresp, tilen, i, tlen, toff; + UserPasswd *up; ntv2len = 0; - UserPasswd *up; + memset(sesskey, 0, sizeof sesskey); /* Phase A: NTLM Negotiate (CredSSP v5, with clientNonce) */ fprint(2, "nla: sending Phase A (NTLM Negotiate)\n"); From 9e8b7612b42440d38924b1ab96bc8d52000392eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:35:47 +0000 Subject: [PATCH 37/45] nla: add MIC to NTLM Authenticate for CredSSP Phase D MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the NTLM Challenge TargetInfo contains MsvAvTimestamp (AvId=7), MS-NLMP §3.1.5.1.2.3 requires the AUTHENTICATE_MESSAGE to include a MIC (Message Integrity Code). Without it, Windows accepts the NTLM authentication (logs event 4624) but closes the CredSSP TLS connection before sending Phase D pubKeyAuth. Changes: - ntv2frompasswd: build a modified TargetInfo copy for the blob by inserting MsvAvFlags=2 AvPair before the EOL (signals MIC is present). The server verifies NtChallengeResponse against the blob we send, so ExportedSessionKey is now derived from the modified TargetInfo. - mkntauth: add Version placeholder (8 zero bytes, offset 64) and MIC placeholder (16 zero bytes, offset 72) to AUTHENTICATE_MESSAGE; payload offsets shift from 64 to 88. - nlahandshake: save ntnego length in nnego; for NTLMv2 path, compute MIC = HMAC_MD5(ExportedSessionKey, nego||chal||auth_with_mic_zeroed) and fill ntauth[72..87] before writing Phase C. - nla_test.c: adjust minimum-size check 64→88. Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/45a843fa-a8f4-4cc4-8d17-6ab2320a0893 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 60 +++++++++++++++++++++++++++++++++++++++++++++--------- nla_test.c | 2 +- rpc.c | 29 +++++++++++++++++++++----- 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/nla.c b/nla.c index 726d301..5354499 100644 --- a/nla.c +++ b/nla.c @@ -859,19 +859,50 @@ ntv2frompasswd(char *pass, char *user, char *domain, uchar *ntbuf, int nntbuf, uchar lmbuf[24], uchar sesskey[MD5dlen]) { uchar nthash[MD4dlen], rkey[MD5dlen], ntproofstr[MD5dlen]; - uchar blob[32 + MaxNTLMTargetInfo]; + uchar blob[32 + MaxNTLMTargetInfo + 8]; /* +8 for MsvAvFlags AvPair */ + uchar mti[MaxNTLMTargetInfo + 8]; /* modified TargetInfo */ uchar unidata[1024], unipass[256], ts[8]; DigestState *ds; Rune r; char *p; - int n, bloblen; + int n, bloblen, mtilen, eol; uchar *w; if(tilen > MaxNTLMTargetInfo){ - werrstr("ntv2frompasswd: TargetInfo too large (%d)", tilen); + werrstr("ntv2frompasswd: TargetInfo too large (%d > %d)", tilen, MaxNTLMTargetInfo); return -1; } - bloblen = 32 + tilen; + + /* + * Build modified TargetInfo: insert MsvAvFlags=2 AvPair before EOL. + * Required by MS-NLMP §3.1.5.1.2.3 when providing a MIC — the flag + * value 2 signals to the server that a MIC is present in AUTHENTICATE_MESSAGE. + * The server uses this modified TargetInfo (from the blob) to recompute + * NtProofStr and ExportedSessionKey, so we must use it here too. + */ + 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); + mti[eol+0] = 6; mti[eol+1] = 0; /* AvId=6 (MsvAvFlags) */ + mti[eol+2] = 4; mti[eol+3] = 0; /* AvLen=4 */ + mti[eol+4] = 2; mti[eol+5] = 0; /* Value=0x00000002 (MIC present) */ + mti[eol+6] = 0; mti[eol+7] = 0; + mti[eol+8] = 0; mti[eol+9] = 0; /* EOL AvId=0 */ + mti[eol+10]= 0; mti[eol+11]= 0; /* EOL AvLen=0 */ + mtilen = eol + 12; + } + + bloblen = 32 + mtilen; if(MD5dlen + bloblen > nntbuf){ werrstr("ntv2frompasswd: NT response buffer too small"); return -1; @@ -911,11 +942,10 @@ ntv2frompasswd(char *pass, char *user, char *domain, * [0] RespType = 0x01 * [1] HiRespType = 0x01 * [2-7] Reserved (6 zero bytes) - * [8-15] Timestamp (MsvAvTimestamp or zeros) + * [8-15] Timestamp (MsvAvTimestamp from Challenge, or zeros) * [16-23] ClientChallenge * [24-27] Reserved (4 zero bytes) - * [28..] TargetInfo (verbatim from Challenge) - * [28+tilen..31+tilen] Reserved (4 zero bytes) + * [28..] TargetInfo (modified: MsvAvFlags=2 inserted before EOL) */ memset(blob, 0, bloblen); blob[0] = 0x01; @@ -924,8 +954,8 @@ ntv2frompasswd(char *pass, char *user, char *domain, memset(ts, 0, 8); memmove(blob + 8, ts, 8); memmove(blob + 16, cchal, 8); - if(ti != nil && tilen > 0) - memmove(blob + 28, ti, tilen); + if(mtilen > 0) + memmove(blob + 28, mti, mtilen); /* NtProofStr = HMAC_MD5(ResponseKeyNT, svchal ‖ blob) */ ds = hmac_md5(svchal, 8, rkey, MD5dlen, nil, nil); @@ -959,7 +989,7 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntre usrlen = toutf16(usr16, sizeof usr16, user, strlen(user)); lmlen = NTRespLen; /* LmChallengeResponse is always 24 bytes */ - domoff = 64; + domoff = 88; /* 64 fixed + 8 Version + 16 MIC */ usroff = domoff + domlen; lmoff = usroff + usrlen; ntoff = lmoff + lmlen; @@ -1013,6 +1043,16 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntre /* NegotiateFlags: no NfESS — NTLMv2 uses its own client challenge in the blob */ PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); 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 */ diff --git a/nla_test.c b/nla_test.c index ad0054c..7b04bad 100644 --- a/nla_test.c +++ b/nla_test.c @@ -214,7 +214,7 @@ testmkntauth(void) n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, NTRespLen, nil); if(n < 0) sysfatal("testmkntauth: unexpected error"); - if(n < 64) + if(n < 88) sysfatal("testmkntauth: message too short (%d)", n); if(memcmp(buf, "NTLMSSP\0", 8) != 0) sysfatal("testmkntauth: bad signature"); diff --git a/rpc.c b/rpc.c index fcf70b9..acf9b1b 100644 --- a/rpc.c +++ b/rpc.c @@ -87,7 +87,7 @@ nlahandshake(Rdp *c) 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; + int n, ntlen, ntv2len, nresp, tilen, i, tlen, toff, nnego; UserPasswd *up; ntv2len = 0; @@ -98,6 +98,7 @@ nlahandshake(Rdp *c) 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; @@ -213,12 +214,30 @@ nlahandshake(Rdp *c) /* Phase C: NTLM Authenticate */ fprint(2, "nla: sending Phase C (NTLM Authenticate, user=%s, dom=%s)\n", c->user, dom); - if(pass[0] != '\0') + if(pass[0] != '\0'){ n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntv2resp, ntv2len, lmv2resp); - else + 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). + */ + { + DigestState *mds; + mds = hmac_md5(ntnego, nnego, sesskey, MD5dlen, nil, nil); + mds = hmac_md5(ntp, ntlen, sesskey, MD5dlen, nil, mds); + hmac_md5(ntauth, n, sesskey, MD5dlen, ntauth+72, mds); + } + } else { n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, 24, nil); - if(n < 0) - return -1; + 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); From 6ed1fc73f01141fa266e37ba7481b07e7b8f792a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:50:17 +0000 Subject: [PATCH 38/45] nla: add debug prints for ExportedSessionKey, MIC, ntnego, and ntauth structure Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/89b1f853-b0d4-4e66-95b6-74ee95207650 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/rpc.c b/rpc.c index acf9b1b..69de3c0 100644 --- a/rpc.c +++ b/rpc.c @@ -186,6 +186,12 @@ nlahandshake(Rdp *c) ntv2resp, sizeof ntv2resp, lmv2resp, sesskey); if(ntv2len < 0) return -1; + fprint(2, "nla: ExportedSessionKey:"); + for(i = 0; i < MD5dlen; i++) fprint(2, " %02ux", sesskey[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); @@ -233,6 +239,12 @@ nlahandshake(Rdp *c) mds = hmac_md5(ntp, ntlen, sesskey, MD5dlen, nil, mds); hmac_md5(ntauth, n, sesskey, 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 header (first 92 bytes):"); + for(i = 0; i < 92 && i < n; i++) fprint(2, " %02ux", ntauth[i]); + fprint(2, "\n"); } else { n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, 24, nil); if(n < 0) From db21737e6cb69e4578c0cfff9df5ddbe5c3bb432 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:14:00 +0000 Subject: [PATCH 39/45] nla: fix NTLM flags, add NfKeyExch+EncryptedRandomSessionKey for CredSSP Phase D Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/c58a7288-50ba-4aeb-9212-a07ad4010bbb Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 2 +- nla.c | 60 ++++++++++++++++++++++++++++++++++++++---------------- nla_test.c | 8 ++++---- rpc.c | 43 ++++++++++++++++++++++++++------------ 4 files changed, 78 insertions(+), 35 deletions(-) diff --git a/fns.h b/fns.h index e77374c..6430e6d 100644 --- a/fns.h +++ b/fns.h @@ -44,7 +44,7 @@ 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*); -int mkntauth(uchar*, int, char*, char*, uchar*, int, uchar*); +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); diff --git a/nla.c b/nla.c index 5354499..d826337 100644 --- a/nla.c +++ b/nla.c @@ -23,12 +23,16 @@ enum { - /* NTLM NegotiateFlags (subset used here) */ + /* 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 */ + NfAlwaysSign = (1<<14), /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ + NfESS = (1<<17), /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ + Nf128 = (1<<28), /* NTLMSSP_NEGOTIATE_128 */ + NfKeyExch = (1<<29), /* NTLMSSP_NEGOTIATE_KEY_EXCH */ /* NTLM response sizes */ NTRespLen = 24, /* NTLMv1 NT/LM response length */ @@ -758,7 +762,7 @@ mkntnego(uchar *buf, int nbuf) p = buf; memmove(p, "NTLMSSP\0", 8); p += 8; PLONG(p, 1); p += 4; /* MessageType */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; /* NegotiateFlags */ + 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; @@ -977,23 +981,25 @@ ntv2frompasswd(char *pass, char *user, char *domain, } int -mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntresplen, uchar *lmresp) +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; - int lmlen, total; + 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; - total = ntoff + ntresplen; + eskoff = ntoff + ntresplen; + total = eskoff + esklen; if(total > nbuf){ werrstr("mkntauth: buffer too small (%d < %d)", nbuf, total); @@ -1034,14 +1040,14 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntre PLONG(p+4, lmoff); p += 8; - /* EncryptedRandomSessionKeyFields (empty) */ - PSHORT(p, 0); - PSHORT(p+2, 0); - PLONG(p+4, ntoff+ntresplen); + /* EncryptedRandomSessionKeyFields */ + PSHORT(p, esklen); + PSHORT(p+2, esklen); + PLONG(p+4, eskoff); p += 8; - /* NegotiateFlags: no NfESS — NTLMv2 uses its own client challenge in the blob */ - PLONG(p, NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign); p += 4; + /* 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; @@ -1062,6 +1068,9 @@ mkntauth(uchar *buf, int nbuf, char *user, char *domain, uchar *ntresp, int ntre 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; } @@ -1137,8 +1146,15 @@ 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: deriving sign/seal keys from session key\n"); +fprint(2, "nla: ExportedSessionKey (for sign/seal):"); +for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", sesskey[n]); +fprint(2, "\n"); +n = 0; ntlmkeys(sesskey, signkey, sealkey); +fprint(2, "nla: signKey:"); +for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", signkey[n]); +fprint(2, "\n"); +n = 0; /* Extract SubjectPublicKeyInfo from server's TLS certificate */ fprint(2, "nla: extracting server SubjectPublicKeyInfo (certlen=%d)\n", certlen); @@ -1149,7 +1165,14 @@ if(spki == nil || spkilen <= 0){ werrstr("NLA: cannot extract server public key from TLS certificate"); return -1; } -fprint(2, "nla: SPKI extracted (%d bytes)\n", spkilen); +fprint(2, "nla: SPKI (%d bytes):", spkilen); +for(n = 0; n < spkilen; n++) fprint(2, " %02ux", spki[n]); +fprint(2, "\n"); +n = 0; +fprint(2, "nla: clientNonce:"); +for(n = 0; n < 32; n++) fprint(2, " %02ux", cnonce[n]); +fprint(2, "\n"); +n = 0; /* * CredSSP v5 Phase E pubKeyAuth (MS-CSSP §3.1.5.1.1.1): @@ -1159,7 +1182,10 @@ fprint(2, "nla: SPKI extracted (%d bytes)\n", spkilen); pubkeyauthlen = mkpubkeyauth(pubkeyauth, sizeof pubkeyauth, sesskey, cnonce, spki, spkilen); if(pubkeyauthlen < 0) return -1; -fprint(2, "nla: pubKeyAuth computed (%d bytes)\n", pubkeyauthlen); +fprint(2, "nla: pubKeyAuth:"); +for(n = 0; n < pubkeyauthlen; n++) fprint(2, " %02ux", pubkeyauth[n]); +fprint(2, "\n"); +n = 0; n = mktscreds(creds, sizeof creds, dom, user, pass); if(n < 0) diff --git a/nla_test.c b/nla_test.c index 7b04bad..44e5430 100644 --- a/nla_test.c +++ b/nla_test.c @@ -19,9 +19,9 @@ testmkntnego(void) 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|NfNTLM|NfAlwaysSign)) + if(GLONG(buf+12) != (NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch)) sysfatal("testmkntnego: want NTLMFlags=%ux, got %lux", - NfUnicode|NfReqTarget|NfNTLM|NfAlwaysSign, (ulong)GLONG(buf+12)); + NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch, (ulong)GLONG(buf+12)); return 0; } @@ -211,7 +211,7 @@ testmkntauth(void) int n; memset(ntresp, 0x55, NTRespLen); - n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, NTRespLen, nil); + n = mkntauth(buf, sizeof buf, "joe", "CORP", ntresp, NTRespLen, nil, nil); if(n < 0) sysfatal("testmkntauth: unexpected error"); if(n < 88) @@ -220,7 +220,7 @@ testmkntauth(void) 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|NfNTLM|NfAlwaysSign)) + if(GLONG(buf+60) != (NfUnicode|NfReqTarget|NfSign|NfSeal|NfNTLM|NfAlwaysSign|NfESS|Nf128|NfKeyExch)) sysfatal("testmkntauth: bad NegotiateFlags"); return 0; } diff --git a/rpc.c b/rpc.c index 69de3c0..c039138 100644 --- a/rpc.c +++ b/rpc.c @@ -83,7 +83,10 @@ nlahandshake(Rdp *c) uchar lmv2resp[24]; /* NTLMv2 LmChallengeResponse */ uchar ntv2resp[16 + 32 + 1024]; /* NTLMv2 NtChallengeResponse */ uchar ntresp[64]; /* factotum mschap NTLMv1 fallback */ - uchar sesskey[MD5dlen]; /* NTLMv2 ExportedSessionKey */ + 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; @@ -92,6 +95,7 @@ nlahandshake(Rdp *c) 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"); @@ -186,9 +190,24 @@ nlahandshake(Rdp *c) ntv2resp, sizeof ntv2resp, lmv2resp, sesskey); if(ntv2len < 0) return -1; - fprint(2, "nla: ExportedSessionKey:"); + 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"); @@ -221,7 +240,7 @@ nlahandshake(Rdp *c) /* 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); + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntv2resp, ntv2len, lmv2resp, eskresp); if(n < 0) return -1; /* @@ -232,21 +251,22 @@ nlahandshake(Rdp *c) * 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, sesskey, MD5dlen, nil, nil); - mds = hmac_md5(ntp, ntlen, sesskey, MD5dlen, nil, mds); - hmac_md5(ntauth, n, sesskey, MD5dlen, ntauth+72, 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 header (first 92 bytes):"); - for(i = 0; i < 92 && i < n; i++) fprint(2, " %02ux", ntauth[i]); + 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); + n = mkntauth(ntauth, sizeof ntauth, c->user, dom, ntresp+24, 24, nil, nil); if(n < 0) return -1; } @@ -262,13 +282,10 @@ nlahandshake(Rdp *c) 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, sesskey); + 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) { Msg t; From 85aafcf96134770cf653f181e8404f5b4b0c0f72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:15:19 +0000 Subject: [PATCH 40/45] nla: remove unnecessary n=0 resets in debug print loops Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/c58a7288-50ba-4aeb-9212-a07ad4010bbb Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nla.c b/nla.c index d826337..ee38203 100644 --- a/nla.c +++ b/nla.c @@ -1149,12 +1149,10 @@ fprint(2, "nla: Phase D received (%d bytes)\n", n); fprint(2, "nla: ExportedSessionKey (for sign/seal):"); for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", sesskey[n]); fprint(2, "\n"); -n = 0; ntlmkeys(sesskey, signkey, sealkey); fprint(2, "nla: signKey:"); for(n = 0; n < MD5dlen; n++) fprint(2, " %02ux", signkey[n]); fprint(2, "\n"); -n = 0; /* Extract SubjectPublicKeyInfo from server's TLS certificate */ fprint(2, "nla: extracting server SubjectPublicKeyInfo (certlen=%d)\n", certlen); @@ -1168,11 +1166,9 @@ return -1; fprint(2, "nla: SPKI (%d bytes):", spkilen); for(n = 0; n < spkilen; n++) fprint(2, " %02ux", spki[n]); fprint(2, "\n"); -n = 0; fprint(2, "nla: clientNonce:"); for(n = 0; n < 32; n++) fprint(2, " %02ux", cnonce[n]); fprint(2, "\n"); -n = 0; /* * CredSSP v5 Phase E pubKeyAuth (MS-CSSP §3.1.5.1.1.1): @@ -1185,7 +1181,6 @@ return -1; fprint(2, "nla: pubKeyAuth:"); for(n = 0; n < pubkeyauthlen; n++) fprint(2, " %02ux", pubkeyauth[n]); fprint(2, "\n"); -n = 0; n = mktscreds(creds, sizeof creds, dom, user, pass); if(n < 0) From dbb102fb5bddc2ccc8989661a68e3b908b8b25f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:40:09 +0000 Subject: [PATCH 41/45] rpc.c: restore missing x224hangup function signature (syntax error at line 289) Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/ada06c42-774d-4807-b48c-eb04df00514e Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- rpc.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rpc.c b/rpc.c index c039138..fc57f66 100644 --- a/rpc.c +++ b/rpc.c @@ -286,6 +286,9 @@ nlahandshake(Rdp *c) memset(pass, 0, sizeof pass); return n; } + +int +x224hangup(Rdp* c) { Msg t; From 947d9eefe174e5d199e2a17d866845efc76dd6df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:58:37 +0000 Subject: [PATCH 42/45] nla: fix NTLM flag bit positions (NfAlwaysSign, NfESS, Nf128, NfKeyExch each off by one) Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/a7221936-776c-44a5-9d0f-357e79286335 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nla.c b/nla.c index ee38203..8aae318 100644 --- a/nla.c +++ b/nla.c @@ -29,10 +29,10 @@ enum NfSign = (1<<4), /* NTLMSSP_NEGOTIATE_SIGN */ NfSeal = (1<<5), /* NTLMSSP_NEGOTIATE_SEAL */ NfNTLM = (1<<9), /* NTLMSSP_NEGOTIATE_NTLM */ - NfAlwaysSign = (1<<14), /* NTLMSSP_NEGOTIATE_ALWAYS_SIGN */ - NfESS = (1<<17), /* NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY */ - Nf128 = (1<<28), /* NTLMSSP_NEGOTIATE_128 */ - NfKeyExch = (1<<29), /* NTLMSSP_NEGOTIATE_KEY_EXCH */ + 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 */ From 2e04f30af7a294c0f1b0b9d7320ed2b68a7915ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 02:52:58 +0000 Subject: [PATCH 43/45] nla: add EPA channel bindings (MsvAvChannelBindings + MsvAvTargetName) to NTLMv2 blob Root cause of Phase D hang: Windows 11 22H2 requires EPA (Extended Protection for Authentication). The NtChallengeResponse blob was missing MsvAvChannelBindings (AvId=10, TLS channel binding hash) and MsvAvTargetName (AvId=9, SPN like "TERMSRV/hostname") in its AvPairs. Changes: - dat.h: add char *server to Rdp struct (for SPN construction) - rd.c: set c->server = server (argv[0]) - nla.c: add tlscbind() to compute MD5(gss_channel_bindings_struct) from the server TLS cert per RFC 5929 "tls-server-end-point"; extend ntv2frompasswd() to accept cert/certlen/spn and insert MsvAvChannelBindings and MsvAvTargetName AvPairs before EOL; grow blob/mti buffers with MaxNTLMClientAvExtra; update NTv2RespMax - fns.h: update ntv2frompasswd declaration - rpc.c: build "TERMSRV/" SPN from c->server (stripping dial prefix), pass c->tlscert/c->tlscertlen/spn to ntv2frompasswd; grow ntv2resp buffer Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/968fff8c-5204-42a2-b26c-77d99fab2021 Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- dat.h | 1 + fns.h | 2 +- nla.c | 118 +++++++++++++++++++++++++++++++++++++++++++++++----------- rd.c | 1 + rpc.c | 26 +++++++++++-- 5 files changed, 121 insertions(+), 27 deletions(-) diff --git a/dat.h b/dat.h index b113d8c..0eb2c93 100644 --- a/dat.h +++ b/dat.h @@ -52,6 +52,7 @@ struct Rdp 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 */ diff --git a/fns.h b/fns.h index 6430e6d..0585685 100644 --- a/fns.h +++ b/fns.h @@ -43,7 +43,7 @@ uchar* tpdat(uchar*,uchar*); 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*); +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); diff --git a/nla.c b/nla.c index 8aae318..c46dfc9 100644 --- a/nla.c +++ b/nla.c @@ -36,8 +36,9 @@ enum /* NTLM response sizes */ NTRespLen = 24, /* NTLMv1 NT/LM response length */ - MaxNTLMTargetInfo = 1024, /* maximum TargetInfo AvPairs length */ - NTv2RespMax = 16 + 32 + MaxNTLMTargetInfo, /* max NTLMv2 NtChallengeResponse */ + MaxNTLMTargetInfo = 1024, /* maximum TargetInfo AvPairs length from challenge */ + MaxNTLMClientAvExtra = 8 + (4+16) + (4+512) + 4, /* MsvAvFlags+MsvAvChannelBindings+MsvAvTargetName+EOL */ + NTv2RespMax = 16 + 32 + MaxNTLMTargetInfo + MaxNTLMClientAvExtra, /* max NTLMv2 NtChallengeResponse */ /* ASN.1 Universal tags (BER/DER) */ TagInt = 2, /* INTEGER */ @@ -839,7 +840,53 @@ getavtimestamp(uchar *ti, int tilen, uchar ts[8]) } /* - * Compute NTLMv2 NT and LM challenge responses and the ExportedSessionKey. + * 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 @@ -849,27 +896,34 @@ getavtimestamp(uchar *ti, int tilen, uchar ts[8]) * ntbuf — output buffer for NtChallengeResponse * nntbuf — ntbuf size; must be ≥ NTv2RespMax * lmbuf — output LmChallengeResponse (exactly 24 bytes) - * sesskey — output ExportedSessionKey (16 bytes; used for CredSSP key derivation) + * 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+tilen] + * NtChallengeResponse = NtProofStr[16] ‖ Blob[32+mtilen] * LmChallengeResponse = HMAC_MD5(ResponseKeyNT, svchal‖cchal) ‖ cchal (24 bytes) - * ExportedSessionKey = HMAC_MD5(ResponseKeyNT, NtProofStr) + * 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 *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 + 8]; /* +8 for MsvAvFlags AvPair */ - uchar mti[MaxNTLMTargetInfo + 8]; /* modified TargetInfo */ + 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; + int n, bloblen, mtilen, eol, spn16len; uchar *w; if(tilen > MaxNTLMTargetInfo){ @@ -877,12 +931,19 @@ ntv2frompasswd(char *pass, char *user, char *domain, 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: insert MsvAvFlags=2 AvPair before EOL. - * Required by MS-NLMP §3.1.5.1.2.3 when providing a MIC — the flag - * value 2 signals to the server that a MIC is present in AUTHENTICATE_MESSAGE. - * The server uses this modified TargetInfo (from the blob) to recompute - * NtProofStr and ExportedSessionKey, so we must use it here too. + * 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){ @@ -897,13 +958,26 @@ ntv2frompasswd(char *pass, char *user, char *domain, eol += 4 + avlen; } memmove(mti, ti, eol); - mti[eol+0] = 6; mti[eol+1] = 0; /* AvId=6 (MsvAvFlags) */ - mti[eol+2] = 4; mti[eol+3] = 0; /* AvLen=4 */ - mti[eol+4] = 2; mti[eol+5] = 0; /* Value=0x00000002 (MIC present) */ - mti[eol+6] = 0; mti[eol+7] = 0; - mti[eol+8] = 0; mti[eol+9] = 0; /* EOL AvId=0 */ - mti[eol+10]= 0; mti[eol+11]= 0; /* EOL AvLen=0 */ - mtilen = eol + 12; + 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 = 32 + mtilen; diff --git a/rd.c b/rd.c index dc6a204..136474c 100644 --- a/rd.c +++ b/rd.c @@ -186,6 +186,7 @@ main(int argc, char *argv[]) usage(); server = argv[0]; + c->server = server; c->local = getenv("sysname"); c->user = getenv("user"); diff --git a/rpc.c b/rpc.c index fc57f66..f3c3b20 100644 --- a/rpc.c +++ b/rpc.c @@ -81,7 +81,7 @@ nlahandshake(Rdp *c) uchar challenge[8], tsreqbuf[4096]; uchar cchal[8]; /* NTLMv2 client challenge */ uchar lmv2resp[24]; /* NTLMv2 LmChallengeResponse */ - uchar ntv2resp[16 + 32 + 1024]; /* NTLMv2 NtChallengeResponse */ + uchar ntv2resp[16 + 32 + 1024 + 600]; /* NTLMv2 NtChallengeResponse (extra for EPA AvPairs) */ uchar ntresp[64]; /* factotum mschap NTLMv1 fallback */ uchar sesskey[MD5dlen]; /* NTLMv2 SessionBaseKey (= KeyExchangeKey) */ uchar exportedsk[MD5dlen]; /* random ExportedSessionKey (used for sign/seal) */ @@ -185,9 +185,27 @@ nlahandshake(Rdp *c) /* 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); - ntv2len = ntv2frompasswd(pass, user, dom, - challenge, cchal, ti, tilen, - ntv2resp, sizeof ntv2resp, lmv2resp, sesskey); + { + /* + * 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):"); From 483e26e1940723d3db0922a0bb8c884e5467c379 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:03:43 +0000 Subject: [PATCH 44/45] nla: fix NTLMv2 blob length (bloblen = 28 + mtilen, not 32) Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/32ec66be-fb93-4ac8-8e37-ed2139f2522f Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- nla.c | 14 ++++++++------ rpc.c | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nla.c b/nla.c index c46dfc9..e794778 100644 --- a/nla.c +++ b/nla.c @@ -38,7 +38,7 @@ enum NTRespLen = 24, /* NTLMv1 NT/LM response length */ MaxNTLMTargetInfo = 1024, /* maximum TargetInfo AvPairs length from challenge */ MaxNTLMClientAvExtra = 8 + (4+16) + (4+512) + 4, /* MsvAvFlags+MsvAvChannelBindings+MsvAvTargetName+EOL */ - NTv2RespMax = 16 + 32 + MaxNTLMTargetInfo + MaxNTLMClientAvExtra, /* max NTLMv2 NtChallengeResponse */ + NTv2RespMax = 16 + 28 + MaxNTLMTargetInfo + MaxNTLMClientAvExtra, /* max NTLMv2 NtChallengeResponse */ /* ASN.1 Universal tags (BER/DER) */ TagInt = 2, /* INTEGER */ @@ -980,7 +980,7 @@ ntv2frompasswd(char *pass, char *user, char *domain, mtilen = (int)(w - mti); } - bloblen = 32 + mtilen; + bloblen = 28 + mtilen; if(MD5dlen + bloblen > nntbuf){ werrstr("ntv2frompasswd: NT response buffer too small"); return -1; @@ -1016,14 +1016,16 @@ ntv2frompasswd(char *pass, char *user, char *domain, hmac_md5(unidata, w - unidata, nthash, MD4dlen, rkey, nil); /* - * Build the NTLMv2 blob (MS-NLMP §3.3.2): + * Build the NTLMv2 blob (MS-NLMP §3.3.2), 28-byte fixed header: * [0] RespType = 0x01 * [1] HiRespType = 0x01 - * [2-7] Reserved (6 zero bytes) + * [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] Reserved (4 zero bytes) - * [28..] TargetInfo (modified: MsvAvFlags=2 inserted before EOL) + * [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; diff --git a/rpc.c b/rpc.c index f3c3b20..14515e1 100644 --- a/rpc.c +++ b/rpc.c @@ -81,7 +81,7 @@ nlahandshake(Rdp *c) uchar challenge[8], tsreqbuf[4096]; uchar cchal[8]; /* NTLMv2 client challenge */ uchar lmv2resp[24]; /* NTLMv2 LmChallengeResponse */ - uchar ntv2resp[16 + 32 + 1024 + 600]; /* NTLMv2 NtChallengeResponse (extra for EPA AvPairs) */ + 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) */ From e0468efc9af441a24dba15b91bb2ebf98d0ff549 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:27:50 +0000 Subject: [PATCH 45/45] fix build: move NTv2RespMax and related constants to fns.h Agent-Logs-Url: https://github.com/yrk-lab/rd/sessions/9bb1fd7b-34b5-45f9-b5c8-33462261fc6a Co-authored-by: yrk-lab <11537279+yrk-lab@users.noreply.github.com> --- fns.h | 8 ++++++++ nla.c | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fns.h b/fns.h index 0585685..b6e75df 100644 --- a/fns.h +++ b/fns.h @@ -39,6 +39,14 @@ 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); diff --git a/nla.c b/nla.c index e794778..2106591 100644 --- a/nla.c +++ b/nla.c @@ -36,9 +36,6 @@ enum /* NTLM response sizes */ NTRespLen = 24, /* NTLMv1 NT/LM response length */ - 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 */ /* ASN.1 Universal tags (BER/DER) */ TagInt = 2, /* INTEGER */