Loading tests/proto/test_netlogon.cpp +188 −0 Changes for tests/proto/test_netlogon.cpp: 188 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ #include "crypto/crypto.h" #include "rpc/ndr.h" #include "rpc/services.h" #include "rpc/netsec.h" using namespace fenrirds; using namespace fenrirds::rpc; Loading Loading @@ -112,7 +113,194 @@ TEST(netlogon_secure_channel) { NdrPull ap2(ar2.stub); uint8_t x[8]; ap2.take(x,8); ap2.u32(); ap2.u32(); CHECK(ap2.u32() != 0); /* status != success */ /* --- the authenticator chain on the established channel --- */ { /* re-establish with the right password (the failed attempt above reset it) */ cr::Bytes cc3 = cr::randomBytes(8); NdrPush r3; r3.u32(0); { auto u = cr::utf16le("WS1"); r3.align(4); r3.u32(4); r3.u32(0); r3.u32(4); r3.bytes(u.data(),u.size()); r3.u8(0); r3.u8(0); } r3.bytes(cc3.data(), 8); NdrPull p3(netlogonDispatch(4, r3.buf).stub); uint8_t s3[8]; p3.take(s3, 8); cc = cc3; sc.assign(s3, s3 + 8); sk = clientSessionKey(nt, cc, sc); ccred = clientCred(sk, cc); NdrPush a3; a3.u32(0); { auto u = cr::utf16le("WS1$"); a3.align(4); a3.u32(5); a3.u32(0); a3.u32(5); a3.bytes(u.data(),u.size()); a3.u8(0); a3.u8(0); } a3.u16(2); { auto u = cr::utf16le("WS1"); a3.align(4); a3.u32(4); a3.u32(0); a3.u32(4); a3.bytes(u.data(),u.size()); a3.u8(0); a3.u8(0); } a3.bytes(ccred.data(), 8); a3.align(4); a3.u32(0x612fffff); NdrPull ap3(netlogonDispatch(26, a3.buf).stub); uint8_t x3[8]; ap3.take(x3,8); ap3.u32(); ap3.u32(); CHECK_EQ(ap3.u32(), uint32_t(0)); } cr::Bytes seed = ccred; auto nextAuth = [&](NdrPush &p, uint32_t ts, cr::Bytes &wantRet) { cr::Bytes t = seed; uint32_t v = t[0] | t[1] << 8 | t[2] << 16 | uint32_t(t[3]) << 24; v += ts; for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); p.bytes(clientCred(sk, t)); p.u32(ts); v += 1; for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); seed = t; wantRet = clientCred(sk, t); }; auto serverAndComputer = [](NdrPush &p) { p.u32(0x20000); { auto u = cr::utf16le("\\\\dc1"); p.u32(6); p.u32(0); p.u32(6); p.bytes(u); p.u8(0); p.u8(0); } p.u32(0x20004); { auto u = cr::utf16le("WS1"); p.align(4); p.u32(4); p.u32(0); p.u32(4); p.bytes(u); p.u8(0); p.u8(0); } }; for (uint32_t level : {1u, 2u}) { /* GetCapabilities: negotiated / requested flags */ NdrPush g; serverAndComputer(g); cr::Bytes want; g.align(4); nextAuth(g, 1000 + level, want); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(level); auto gr = netlogonDispatch(21, g.buf, {}, "WS1"); CHECK(!gr.fault); NdrPull gp(gr.stub); auto ret = gp.takeVec(8); gp.u32(); CHECK(cr::equal(ret, want)); CHECK_EQ(gp.u32(), level); CHECK_EQ(gp.u32(), uint32_t(0x612fffff)); CHECK_EQ(gp.u32(), uint32_t(0)); } { /* a replayed authenticator is rejected */ cr::Bytes keep = seed; cr::Bytes t = seed; cr::Bytes w; NdrPush g; serverAndComputer(g); g.align(4); uint32_t v = t[0] | t[1] << 8 | t[2] << 16 | uint32_t(t[3]) << 24; v -= 1; /* old seed */ for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); g.bytes(clientCred(sk, t)); g.u32(0); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(1); auto gr = netlogonDispatch(21, g.buf, {}, "WS1"); NdrPull gp(gr.stub); gp.takeVec(8); gp.u32(); gp.u32(); gp.u32(); CHECK(gp.u32() != 0); seed = keep; } { /* GetDomainInfo level 1 with a minimal workstation info */ NdrPush g; serverAndComputer(g); cr::Bytes want; g.align(4); nextAuth(g, 2000, want); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(1); g.u32(1); g.u32(0x20008); /* union + NETLOGON_WORKSTATION_INFO* */ g.u32(0); g.u32(0); /* LsaPolicy */ g.u32(0x2000c); for (int i = 0; i < 5; ++i) g.u32(0); /* DnsHostName only */ g.u16(0); g.u16(0); g.u32(0); /* OsVersion */ g.u16(20); g.u16(20); g.u32(0x20010); /* OsName */ for (int i = 0; i < 4; ++i) { g.u16(0); g.u16(0); g.u32(0); } g.u32(0x2); g.u32(0x18); g.u32(0); g.u32(0); { auto u = cr::utf16le("ws1.example.com"); g.u32(16); g.u32(0); g.u32(16); g.bytes(u); g.u8(0); g.u8(0); } { auto u = cr::utf16le("Windows 11"); g.align(4); g.u32(10); g.u32(0); g.u32(10); g.bytes(u); } auto gr = netlogonDispatch(29, g.buf, {}, "WS1"); CHECK(!gr.fault); CHECK_EQ(gr.status, uint32_t(0)); NdrPull gp(gr.stub); CHECK(cr::equal(gp.takeVec(8), want)); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(1)); CHECK(gp.u32() != 0); uint16_t l, m; gp.unistrHeader(l, m); CHECK_EQ(l, uint16_t(14)); /* EXAMPLE */ auto e = g_dir->bySamAccountName("WS1$"); CHECK(e && e->str("operatingSystem") == "Windows 11"); CHECK(e && e->str("dNSHostName") == "ws1.example.com"); CHECK(e && dsdb::intValue(e->attrs, "msDS-SupportedEncryptionTypes", 0) == 0x18); } { /* ServerPasswordSet2, then the new password authenticates */ std::string newPw = "N3w.Machine-Pw"; cr::Bytes u = cr::utf16le(newPw); cr::Bytes plain = cr::randomBytes(512 - u.size()); plain.insert(plain.end(), u.begin(), u.end()); uint32_t len = uint32_t(u.size()); for (int i = 0; i < 4; ++i) plain.push_back(uint8_t(len >> (8*i))); cr::Bytes enc = cr::aesCfb8Encrypt(sk, cr::Bytes(16, 0), plain); NdrPush g; g.u32(0x20000); { auto s1 = cr::utf16le("\\\\dc1"); g.u32(6); g.u32(0); g.u32(6); g.bytes(s1); g.u8(0); g.u8(0); } { auto s1 = cr::utf16le("WS1$"); g.align(4); g.u32(5); g.u32(0); g.u32(5); g.bytes(s1); g.u8(0); g.u8(0); } g.u16(2); { auto s1 = cr::utf16le("WS1"); g.align(4); g.u32(4); g.u32(0); g.u32(4); g.bytes(s1); g.u8(0); g.u8(0); } cr::Bytes want; g.align(4); nextAuth(g, 3000, want); g.bytes(enc); auto gr = netlogonDispatch(30, g.buf, {}, "WS1"); CHECK(!gr.fault); NdrPull gp(gr.stub); CHECK(cr::equal(gp.takeVec(8), want)); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(0)); auto e = g_dir->bySamAccountName("WS1$"); CHECK(e && cr::equal(cr::bytes(g_dir->secretsOf(e->objectGuid).ntHash), cr::ntHash(newPw))); } { /* DsrGetDcNameEx2 for our domain, and for a foreign one */ setDcIdentity("dc1.example.com", "10.1.1.50"); auto call = [](const std::string &dom) { NdrPush g; g.u32(0); /* ComputerName */ g.u32(0); /* AccountName */ g.u32(0); /* AllowableAccountControlBits */ g.u32(0x20000); { auto u = cr::utf16le(dom); g.u32(uint32_t(dom.size()+1)); g.u32(0); g.u32(uint32_t(dom.size()+1)); g.bytes(u); g.u8(0); g.u8(0); } g.u32(0); /* DomainGuid */ g.u32(0); /* SiteName */ g.u32(0x40000000); /* DS_RETURN_DNS_NAME */ return netlogonDispatch(34, g.buf); }; auto ok = call("example.com"); CHECK(!ok.fault); CHECK_EQ(ok.status, uint32_t(0)); NdrPull gp(ok.stub); CHECK(gp.u32() != 0); for (int i = 0; i < 2; ++i) gp.u32(); CHECK_EQ(gp.u32(), uint32_t(1)); /* DS_INET_ADDRESS */ gp.takeVec(16); gp.u32(); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(0xe00003fd)); gp.u32(); gp.u32(); CHECK(gp.unistrBuffer() == "\\\\dc1.example.com"); CHECK(gp.unistrBuffer() == "\\\\10.1.1.50"); auto bad = call("other.org"); CHECK_EQ(bad.status, uint32_t(1355)); } { /* the secure channel key is there for the schannel bind */ std::vector<uint8_t> k; CHECK(netlogonChannelKey("ws1", k)); CHECK(cr::equal(k, sk)); CHECK(!netlogonChannelKey("nosuch", k)); } unlink(path.c_str()); unlink((path + ".lock").c_str()); unlink((path + ".dom").c_str()); } TEST(netsec_nl_auth_message) { /* what W11 sends: flags 0x17, OEM domain + computer, DNS domain, UTF-8 computer */ std::vector<uint8_t> t = {0,0,0,0, 0x17,0,0,0}; for (char c : std::string("EXAMPLE")) t.push_back(uint8_t(c)); t.push_back(0); for (char c : std::string("W11OLD")) t.push_back(uint8_t(c)); t.push_back(0); const uint8_t dns[] = {7,'e','x','a','m','p','l','e',3,'c','o','m',0, 6,'W','1','1','O','L','D',0}; t.insert(t.end(), dns, dns + sizeof dns); NlAuthNegotiate n; CHECK(parseNlAuthNegotiate(t, n)); CHECK(n.domain == "EXAMPLE"); CHECK(n.computer == "W11OLD"); CHECK(n.dnsDomain == "example.com"); auto r = buildNlAuthResponse(); CHECK_EQ(r.size(), size_t(12)); CHECK_EQ(r[0], uint8_t(1)); } TEST(netsec_seal_roundtrip) { cr::Bytes sk = cr::randomBytes(16); NetsecContext client(sk, "WS1"), server(sk, "WS1"); for (bool seal : {true, false}) { for (int i = 0; i < 3; ++i) { std::vector<uint8_t> msg = cr::randomBytes(48), wire = msg; auto sig = client.wrapRequest(wire, seal, 56); CHECK_EQ(sig.size(), size_t(56)); CHECK(seal ? wire != msg : wire == msg); CHECK(server.unwrapRequest(wire, sig, seal)); CHECK(wire == msg); std::vector<uint8_t> resp = cr::randomBytes(32), rw = resp; auto rs = server.wrapResponse(rw, seal, 32); CHECK(client.unwrapResponse(rw, rs, seal)); CHECK(rw == resp); } } /* a flipped byte or a replay fails */ std::vector<uint8_t> msg = cr::randomBytes(32), wire = msg; auto sig = client.wrapRequest(wire, true); std::vector<uint8_t> bad = wire; bad[5] ^= 1; NetsecContext s2 = server; CHECK(!s2.unwrapRequest(bad, sig, true)); CHECK(server.unwrapRequest(wire, sig, true)); std::vector<uint8_t> again = msg; auto sig2 = sig; CHECK(!server.unwrapRequest(again, sig2, true)); } CHECK_MAIN Loading
tests/proto/test_netlogon.cpp +188 −0 Changes for tests/proto/test_netlogon.cpp: 188 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -10,6 +10,7 @@ #include "crypto/crypto.h" #include "rpc/ndr.h" #include "rpc/services.h" #include "rpc/netsec.h" using namespace fenrirds; using namespace fenrirds::rpc; Loading Loading @@ -112,7 +113,194 @@ TEST(netlogon_secure_channel) { NdrPull ap2(ar2.stub); uint8_t x[8]; ap2.take(x,8); ap2.u32(); ap2.u32(); CHECK(ap2.u32() != 0); /* status != success */ /* --- the authenticator chain on the established channel --- */ { /* re-establish with the right password (the failed attempt above reset it) */ cr::Bytes cc3 = cr::randomBytes(8); NdrPush r3; r3.u32(0); { auto u = cr::utf16le("WS1"); r3.align(4); r3.u32(4); r3.u32(0); r3.u32(4); r3.bytes(u.data(),u.size()); r3.u8(0); r3.u8(0); } r3.bytes(cc3.data(), 8); NdrPull p3(netlogonDispatch(4, r3.buf).stub); uint8_t s3[8]; p3.take(s3, 8); cc = cc3; sc.assign(s3, s3 + 8); sk = clientSessionKey(nt, cc, sc); ccred = clientCred(sk, cc); NdrPush a3; a3.u32(0); { auto u = cr::utf16le("WS1$"); a3.align(4); a3.u32(5); a3.u32(0); a3.u32(5); a3.bytes(u.data(),u.size()); a3.u8(0); a3.u8(0); } a3.u16(2); { auto u = cr::utf16le("WS1"); a3.align(4); a3.u32(4); a3.u32(0); a3.u32(4); a3.bytes(u.data(),u.size()); a3.u8(0); a3.u8(0); } a3.bytes(ccred.data(), 8); a3.align(4); a3.u32(0x612fffff); NdrPull ap3(netlogonDispatch(26, a3.buf).stub); uint8_t x3[8]; ap3.take(x3,8); ap3.u32(); ap3.u32(); CHECK_EQ(ap3.u32(), uint32_t(0)); } cr::Bytes seed = ccred; auto nextAuth = [&](NdrPush &p, uint32_t ts, cr::Bytes &wantRet) { cr::Bytes t = seed; uint32_t v = t[0] | t[1] << 8 | t[2] << 16 | uint32_t(t[3]) << 24; v += ts; for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); p.bytes(clientCred(sk, t)); p.u32(ts); v += 1; for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); seed = t; wantRet = clientCred(sk, t); }; auto serverAndComputer = [](NdrPush &p) { p.u32(0x20000); { auto u = cr::utf16le("\\\\dc1"); p.u32(6); p.u32(0); p.u32(6); p.bytes(u); p.u8(0); p.u8(0); } p.u32(0x20004); { auto u = cr::utf16le("WS1"); p.align(4); p.u32(4); p.u32(0); p.u32(4); p.bytes(u); p.u8(0); p.u8(0); } }; for (uint32_t level : {1u, 2u}) { /* GetCapabilities: negotiated / requested flags */ NdrPush g; serverAndComputer(g); cr::Bytes want; g.align(4); nextAuth(g, 1000 + level, want); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(level); auto gr = netlogonDispatch(21, g.buf, {}, "WS1"); CHECK(!gr.fault); NdrPull gp(gr.stub); auto ret = gp.takeVec(8); gp.u32(); CHECK(cr::equal(ret, want)); CHECK_EQ(gp.u32(), level); CHECK_EQ(gp.u32(), uint32_t(0x612fffff)); CHECK_EQ(gp.u32(), uint32_t(0)); } { /* a replayed authenticator is rejected */ cr::Bytes keep = seed; cr::Bytes t = seed; cr::Bytes w; NdrPush g; serverAndComputer(g); g.align(4); uint32_t v = t[0] | t[1] << 8 | t[2] << 16 | uint32_t(t[3]) << 24; v -= 1; /* old seed */ for (int i = 0; i < 4; ++i) t[i] = uint8_t(v >> (8*i)); g.bytes(clientCred(sk, t)); g.u32(0); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(1); auto gr = netlogonDispatch(21, g.buf, {}, "WS1"); NdrPull gp(gr.stub); gp.takeVec(8); gp.u32(); gp.u32(); gp.u32(); CHECK(gp.u32() != 0); seed = keep; } { /* GetDomainInfo level 1 with a minimal workstation info */ NdrPush g; serverAndComputer(g); cr::Bytes want; g.align(4); nextAuth(g, 2000, want); g.bytes(std::vector<uint8_t>(12, 0)); g.u32(1); g.u32(1); g.u32(0x20008); /* union + NETLOGON_WORKSTATION_INFO* */ g.u32(0); g.u32(0); /* LsaPolicy */ g.u32(0x2000c); for (int i = 0; i < 5; ++i) g.u32(0); /* DnsHostName only */ g.u16(0); g.u16(0); g.u32(0); /* OsVersion */ g.u16(20); g.u16(20); g.u32(0x20010); /* OsName */ for (int i = 0; i < 4; ++i) { g.u16(0); g.u16(0); g.u32(0); } g.u32(0x2); g.u32(0x18); g.u32(0); g.u32(0); { auto u = cr::utf16le("ws1.example.com"); g.u32(16); g.u32(0); g.u32(16); g.bytes(u); g.u8(0); g.u8(0); } { auto u = cr::utf16le("Windows 11"); g.align(4); g.u32(10); g.u32(0); g.u32(10); g.bytes(u); } auto gr = netlogonDispatch(29, g.buf, {}, "WS1"); CHECK(!gr.fault); CHECK_EQ(gr.status, uint32_t(0)); NdrPull gp(gr.stub); CHECK(cr::equal(gp.takeVec(8), want)); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(1)); CHECK(gp.u32() != 0); uint16_t l, m; gp.unistrHeader(l, m); CHECK_EQ(l, uint16_t(14)); /* EXAMPLE */ auto e = g_dir->bySamAccountName("WS1$"); CHECK(e && e->str("operatingSystem") == "Windows 11"); CHECK(e && e->str("dNSHostName") == "ws1.example.com"); CHECK(e && dsdb::intValue(e->attrs, "msDS-SupportedEncryptionTypes", 0) == 0x18); } { /* ServerPasswordSet2, then the new password authenticates */ std::string newPw = "N3w.Machine-Pw"; cr::Bytes u = cr::utf16le(newPw); cr::Bytes plain = cr::randomBytes(512 - u.size()); plain.insert(plain.end(), u.begin(), u.end()); uint32_t len = uint32_t(u.size()); for (int i = 0; i < 4; ++i) plain.push_back(uint8_t(len >> (8*i))); cr::Bytes enc = cr::aesCfb8Encrypt(sk, cr::Bytes(16, 0), plain); NdrPush g; g.u32(0x20000); { auto s1 = cr::utf16le("\\\\dc1"); g.u32(6); g.u32(0); g.u32(6); g.bytes(s1); g.u8(0); g.u8(0); } { auto s1 = cr::utf16le("WS1$"); g.align(4); g.u32(5); g.u32(0); g.u32(5); g.bytes(s1); g.u8(0); g.u8(0); } g.u16(2); { auto s1 = cr::utf16le("WS1"); g.align(4); g.u32(4); g.u32(0); g.u32(4); g.bytes(s1); g.u8(0); g.u8(0); } cr::Bytes want; g.align(4); nextAuth(g, 3000, want); g.bytes(enc); auto gr = netlogonDispatch(30, g.buf, {}, "WS1"); CHECK(!gr.fault); NdrPull gp(gr.stub); CHECK(cr::equal(gp.takeVec(8), want)); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(0)); auto e = g_dir->bySamAccountName("WS1$"); CHECK(e && cr::equal(cr::bytes(g_dir->secretsOf(e->objectGuid).ntHash), cr::ntHash(newPw))); } { /* DsrGetDcNameEx2 for our domain, and for a foreign one */ setDcIdentity("dc1.example.com", "10.1.1.50"); auto call = [](const std::string &dom) { NdrPush g; g.u32(0); /* ComputerName */ g.u32(0); /* AccountName */ g.u32(0); /* AllowableAccountControlBits */ g.u32(0x20000); { auto u = cr::utf16le(dom); g.u32(uint32_t(dom.size()+1)); g.u32(0); g.u32(uint32_t(dom.size()+1)); g.bytes(u); g.u8(0); g.u8(0); } g.u32(0); /* DomainGuid */ g.u32(0); /* SiteName */ g.u32(0x40000000); /* DS_RETURN_DNS_NAME */ return netlogonDispatch(34, g.buf); }; auto ok = call("example.com"); CHECK(!ok.fault); CHECK_EQ(ok.status, uint32_t(0)); NdrPull gp(ok.stub); CHECK(gp.u32() != 0); for (int i = 0; i < 2; ++i) gp.u32(); CHECK_EQ(gp.u32(), uint32_t(1)); /* DS_INET_ADDRESS */ gp.takeVec(16); gp.u32(); gp.u32(); CHECK_EQ(gp.u32(), uint32_t(0xe00003fd)); gp.u32(); gp.u32(); CHECK(gp.unistrBuffer() == "\\\\dc1.example.com"); CHECK(gp.unistrBuffer() == "\\\\10.1.1.50"); auto bad = call("other.org"); CHECK_EQ(bad.status, uint32_t(1355)); } { /* the secure channel key is there for the schannel bind */ std::vector<uint8_t> k; CHECK(netlogonChannelKey("ws1", k)); CHECK(cr::equal(k, sk)); CHECK(!netlogonChannelKey("nosuch", k)); } unlink(path.c_str()); unlink((path + ".lock").c_str()); unlink((path + ".dom").c_str()); } TEST(netsec_nl_auth_message) { /* what W11 sends: flags 0x17, OEM domain + computer, DNS domain, UTF-8 computer */ std::vector<uint8_t> t = {0,0,0,0, 0x17,0,0,0}; for (char c : std::string("EXAMPLE")) t.push_back(uint8_t(c)); t.push_back(0); for (char c : std::string("W11OLD")) t.push_back(uint8_t(c)); t.push_back(0); const uint8_t dns[] = {7,'e','x','a','m','p','l','e',3,'c','o','m',0, 6,'W','1','1','O','L','D',0}; t.insert(t.end(), dns, dns + sizeof dns); NlAuthNegotiate n; CHECK(parseNlAuthNegotiate(t, n)); CHECK(n.domain == "EXAMPLE"); CHECK(n.computer == "W11OLD"); CHECK(n.dnsDomain == "example.com"); auto r = buildNlAuthResponse(); CHECK_EQ(r.size(), size_t(12)); CHECK_EQ(r[0], uint8_t(1)); } TEST(netsec_seal_roundtrip) { cr::Bytes sk = cr::randomBytes(16); NetsecContext client(sk, "WS1"), server(sk, "WS1"); for (bool seal : {true, false}) { for (int i = 0; i < 3; ++i) { std::vector<uint8_t> msg = cr::randomBytes(48), wire = msg; auto sig = client.wrapRequest(wire, seal, 56); CHECK_EQ(sig.size(), size_t(56)); CHECK(seal ? wire != msg : wire == msg); CHECK(server.unwrapRequest(wire, sig, seal)); CHECK(wire == msg); std::vector<uint8_t> resp = cr::randomBytes(32), rw = resp; auto rs = server.wrapResponse(rw, seal, 32); CHECK(client.unwrapResponse(rw, rs, seal)); CHECK(rw == resp); } } /* a flipped byte or a replay fails */ std::vector<uint8_t> msg = cr::randomBytes(32), wire = msg; auto sig = client.wrapRequest(wire, true); std::vector<uint8_t> bad = wire; bad[5] ^= 1; NetsecContext s2 = server; CHECK(!s2.unwrapRequest(bad, sig, true)); CHECK(server.unwrapRequest(wire, sig, true)); std::vector<uint8_t> again = msg; auto sig2 = sig; CHECK(!server.unwrapRequest(again, sig2, true)); } CHECK_MAIN