Loading plugin/smb/smb.cpp +171 −8 Changes for plugin/smb/smb.cpp: 171 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -6,6 +6,10 @@ #include <cstring> #include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include "crypto/crypto.h" #include "crypto/gss.h" #include "crypto/ntlm.h" Loading @@ -21,6 +25,7 @@ namespace fenrirds { namespace { constexpr uint8_t AUTH_TYPE_SPNEGO = 9, AUTH_TYPE_KRB5 = 16; uint16_t rd16(const uint8_t *p) { return uint16_t(p[0] | p[1] << 8); } uint32_t rd32(const uint8_t *p) { return uint32_t(p[0]) | p[1]<<8 | p[2]<<16 | uint32_t(p[3])<<24; } uint64_t rd64(const uint8_t *p) { uint64_t v=0; for(int i=0;i<8;i++) v|=uint64_t(p[i])<<(8*i); return v; } Loading Loading @@ -164,19 +169,38 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { std::cerr << "[RPC] BIND auth=" << b.hasAuth << " type=" << int(b.authType) << " level=" << int(b.authLevel) << " token=" << b.authToken.size() << "B" << " iface=" << _name << std::endl; auto known = [](const rpc::SyntaxId &a) { return rpc::isLsarpc(a.uuid) || rpc::isSamr(a.uuid) || rpc::isNetlogon(a.uuid); auto known = [this](const rpc::SyntaxId &a) { return rpc::isLsarpc(a.uuid) || rpc::isSamr(a.uuid) || rpc::isNetlogon(a.uuid) || (_tcp && (rpc::isDrsuapi(a.uuid) || rpc::isEpm(a.uuid))); }; std::string secAddr = _tcp ? std::to_string(_localPort) : "\\PIPE\\" + _name; _boundAbstract = b.contexts[0].abstractSyntax; _bound = true; if (b.hasAuth && b.authType == rpc::AUTH_TYPE_SCHANNEL) { if (b.hasAuth && (b.authType == AUTH_TYPE_SPNEGO || b.authType == AUTH_TYPE_KRB5)) { if (h.pduType == rpc::PDU_ALTER && _gss.active) { /* SPNEGO's third leg: the client's AP-REP */ std::vector<uint8_t> out = gssThirdLeg(b.authToken); resp = rpc::buildBindAck(b, secAddr, out, b.authType, b.authLevel, known, b.authContextId); } else { bool ok = false; std::vector<uint8_t> out; if (known(b.contexts[0].abstractSyntax)) out = gssBind(b, ok); if (ok) resp = rpc::buildBindAck(b, secAddr, out, b.authType, b.authLevel, known, b.authContextId); else if (!known(b.contexts[0].abstractSyntax)) resp = rpc::buildBindAck(b, secAddr, {}, 0, 0, known); /* rejects the context */ else resp = rpc::buildBindNak(h.callId); } } else if (b.hasAuth && b.authType == rpc::AUTH_TYPE_SCHANNEL) { /* netlogon secure channel: the client names its computer, we * take the session key of its ServerAuthenticate3 */ rpc::NlAuthNegotiate neg; std::vector<uint8_t> sk; if (rpc::parseNlAuthNegotiate(b.authToken, neg) && rpc::netlogonChannelKey(neg.computer, sk)) { _netsec = rpc::NetsecContext(sk, neg.computer); resp = rpc::buildBindAck(b, "\\PIPE\\" + _name, rpc::buildNlAuthResponse(), resp = rpc::buildBindAck(b, secAddr, rpc::buildNlAuthResponse(), b.authType, b.authLevel, known, b.authContextId); std::cerr << "[RPC] schannel bind for '" << neg.computer << "'" << std::endl; } else { Loading @@ -184,7 +208,7 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { resp = rpc::buildBindNak(h.callId); } } else { resp = rpc::buildBindAck(b, "\\PIPE\\" + _name, {}, 0, 0, known); resp = rpc::buildBindAck(b, secAddr, {}, 0, 0, known); } if (h.pduType == rpc::PDU_ALTER && resp.size() > 2 && resp[2] == rpc::PDU_BIND_ACK) resp[2] = rpc::PDU_ALTER_ACK; Loading @@ -193,6 +217,8 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { } } else if (h.pduType == rpc::PDU_AUTH3) { std::cerr << "[RPC] AUTH3 received (authLen=" << h.authLength << ")" << std::endl; if (_gss.active && h.authLength && h.authLength <= pdu.size()) gssThirdLeg(std::vector<uint8_t>(pdu.end() - h.authLength, pdu.end())); } else if (h.pduType == rpc::PDU_REQUEST) { rpc::RequestPdu req; if (rpc::parseRequest(pdu.data(), pdu.size(), req)) { Loading @@ -209,6 +235,23 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { req.stub.resize(req.stub.size() - req.authPadLength); resp = dispatch(req, true); } } else if (req.hasAuth && (req.authType == AUTH_TYPE_SPNEGO || req.authType == AUTH_TYPE_KRB5)) { bool ok = _gss.active; if (ok && req.authLevel == rpc::AUTH_LEVEL_PRIVACY) { std::vector<uint8_t> tok = req.authToken, plain; tok.insert(tok.end(), req.stub.begin(), req.stub.end()); ok = crypto::gss::unwrap(_gss.key, _gss.etype, false, tok, plain, nullptr, true); if (ok) req.stub = plain; } if (!ok) { std::cerr << "[RPC] GSS request rejected (unwrap) stub=" << req.stub.size() << " pad=" << int(req.authPadLength) << " tok=" << crypto::hex(req.authToken) << std::endl; resp = rpc::buildFault(req.callId, req.contextId, 0x00000005); } else { if (req.authPadLength <= req.stub.size()) req.stub.resize(req.stub.size() - req.authPadLength); resp = dispatch(req); } } else { resp = dispatch(req); } Loading @@ -217,6 +260,73 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { _out.insert(_out.end(), resp.begin(), resp.end()); } /* bind leg: accept the AP-REQ. DCE style (GSS flag 0x1000, what RPC * clients ask for) answers with the raw AP-REP and waits for the * client's AP-REP (AUTH3, or an ALTER_CONTEXT under SPNEGO). */ std::vector<uint8_t> RpcPipe::gssBind(const rpc::BindPdu &b, bool &ok) { ok = false; bool spnego = b.authType == AUTH_TYPE_SPNEGO; std::vector<uint8_t> apReq, mechTypes, inner; if (spnego) { if (!crypto::gss::parseSpnegoInit(b.authToken, apReq, mechTypes)) return {}; } else { apReq = b.authToken; } uint16_t id = 0; if (!crypto::gss::parseInitialToken(apReq, id, inner)) inner = apReq; GssAcceptResult res; if (!g_KdcPlugin || !g_KdcPlugin->gssAccept(inner, res)) { std::cerr << "[RPC] GSS bind: AP-REQ not accepted" << std::endl; return {}; } bool dce = (res.gssFlags & 0x1000) != 0; _gss = Gss(); _gss.active = true; _gss.spnego = spnego; _gss.type = b.authType; _gss.level = b.authLevel; _gss.ctxId = b.authContextId; _gss.key = res.sessionKey; _gss.etype = res.sessionEtype; _gss.accSubkey = res.acceptorSubkey; _gss.sendSeq = res.acceptorSeq; _gss.principal = res.clientPrincipal; std::cerr << "[RPC] GSS bind " << res.clientPrincipal << " type=" << int(b.authType) << " level=" << int(b.authLevel) << " flags=0x" << std::hex << res.gssFlags << std::dec << " etype=" << res.sessionEtype << " dce=" << dce << std::endl; std::vector<uint8_t> krb = dce ? res.apRep : crypto::gss::wrapInitialToken(res.apRep, 0x0200); ok = true; if (!spnego) return krb; _gss.mechOid = crypto::gss::firstMechOid(mechTypes); _gss.mechTypes = mechTypes; return crypto::gss::buildSpnegoResp(dce ? crypto::gss::ACCEPT_INCOMPLETE : crypto::gss::ACCEPT_COMPLETED, &_gss.mechOid, krb, {}); } /* the client's AP-REP of a DCE-style context; under SPNEGO the reply * completes the negotiation */ std::vector<uint8_t> RpcPipe::gssThirdLeg(const std::vector<uint8_t> &tok) { std::cerr << "[RPC] GSS third leg (" << tok.size() << "B) for " << _gss.principal << std::endl; if (!_gss.spnego) return {}; /* the client's NegTokenResp carries its AP-REP and, as RPC clients * send it, a mechListMIC: then ours over the same mechTypes is due * (RFC 4178 5), the first token of our sequence */ std::vector<uint8_t> rt, mic; int st = 0; std::vector<uint8_t> ourMic; if (crypto::gss::parseSpnegoResp(tok, rt, mic, st) && !mic.empty()) { if (!crypto::gss::verifyMic(_gss.key, _gss.etype, false, mic, _gss.mechTypes)) std::cerr << "[RPC] GSS third leg: client mechListMIC does not verify" << std::endl; ourMic = crypto::gss::getMic(_gss.key, _gss.etype, true, _gss.sendSeq++, _gss.mechTypes, _gss.accSubkey); } return crypto::gss::buildSpnegoResp(crypto::gss::ACCEPT_COMPLETED, nullptr, {}, ourMic); } std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel) { rpc::DispatchResult res; const char *iface = "?"; Loading @@ -230,6 +340,17 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel iface = "netlogon"; res = rpc::netlogonDispatch(req.opnum, req.stub, _sessionKey, schannel ? _netsec.computer() : std::string()); } else if (_tcp && rpc::isEpm(_boundAbstract.uuid)) { iface = "epm"; res = rpc::epmDispatch(req.opnum, req.stub, _localIp, _rpcPort); } else if (_tcp && rpc::isDrsuapi(_boundAbstract.uuid)) { iface = "drsuapi"; if (_gss.active) { res = rpc::drsuapiDispatch(req.opnum, req.stub); } else { res.fault = true; res.faultCode = 0x00000005; /* access denied: needs an authenticated bind */ } } else { res.fault = true; res.faultCode = 0x1C010003; /* nca_unk_if */ Loading @@ -246,6 +367,15 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel } if (res.fault) return rpc::buildFault(req.callId, req.contextId, res.faultCode); if (req.hasAuth && (req.authType == AUTH_TYPE_SPNEGO || req.authType == AUTH_TYPE_KRB5) && _gss.active) { bool seal = req.authLevel == rpc::AUTH_LEVEL_PRIVACY; return rpc::buildAuthResponse(req.callId, req.contextId, res.stub, req.authType, req.authLevel, req.authContextId, [&](std::vector<uint8_t> &data) { if (seal) return crypto::gss::wrapDce(_gss.key, _gss.etype, true, _gss.sendSeq++, data, _gss.accSubkey); return crypto::gss::getMic(_gss.key, _gss.etype, true, _gss.sendSeq++, data, _gss.accSubkey); }); } if (schannel) { bool seal = req.authLevel == rpc::AUTH_LEVEL_PRIVACY; size_t sigLen = req.authToken.size(); Loading Loading @@ -275,13 +405,21 @@ namespace { } SmbServer::SmbServer(AuthBackend &adminBackend, const std::string &bindAddr, int port, const std::string &dcHost) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _DcHost(dcHost) { const std::string &dcHost, int epmPort, int rpcPort) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _EpmPort(epmPort), _RpcPort(rpcPort), _DcHost(dcHost) { DomainRegistry::instance().setAdminBackend(&adminBackend); auto g = crypto::randomBytes(16); std::memcpy(_ServerGuid, g.data(), 16); _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), port, 128, 0)); std::cerr << "SMB plugin listening on " << bindAddr << ":" << port << std::endl; /* DCE/RPC over TCP in the same event loop: the endpoint mapper and the * port it hands out (connections are told apart by their local port) */ for (int p : {epmPort, rpcPort}) { if (p <= 0) continue; _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), p, 128, 0)); std::cerr << "RPC (ncacn_ip_tcp) listening on " << bindAddr << ":" << p << std::endl; } } SmbServer::~SmbServer() { Loading @@ -303,7 +441,20 @@ void SmbServer::CreateConnection(std::shared_ptr<netplus::con> &res) { res = std::make_shared<netplus::con>(this); } void SmbServer::ConnectEvent(netplus::con &c, const int, ULONG_PTR) { std::lock_guard<std::mutex> l(_Lock); _Conns[&c] = Conn(); Conn conn; /* which listener: our side of the connection */ sockaddr_in la{}; socklen_t ll = sizeof(la); if (!c.slots.empty() && c.slots[0].csock && getsockname(c.slots[0].csock->fd(), reinterpret_cast<sockaddr*>(&la), &ll) == 0 && la.sin_family == AF_INET) { int lport = ntohs(la.sin_port); if (lport == _EpmPort || lport == _RpcPort) { conn.tcpRpc = new RpcPipe("tcp"); conn.tcpRpc->setTcp(ntohl(la.sin_addr.s_addr), uint16_t(lport), uint16_t(_RpcPort)); } } std::lock_guard<std::mutex> l(_Lock); _Conns[&c] = conn; } void SmbServer::DisconnectEvent(netplus::con &c, const int, ULONG_PTR) { std::cerr << "[SMB] disconnect" << std::endl; /* TEMPORARY interop diagnosis */ Loading @@ -314,6 +465,7 @@ void SmbServer::DisconnectEvent(netplus::con &c, const int, ULONG_PTR) { flush(o.second); delete o.second.pipe; } delete it->second.tcpRpc; _Conns.erase(it); } } Loading @@ -325,6 +477,17 @@ void SmbServer::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (it == _Conns.end()) return; Conn &c = it->second; if (c.tcpRpc) { /* DCE/RPC over TCP: the PDU stream itself */ const uint8_t *d = reinterpret_cast<const uint8_t*>(curcon.RecvData.data()); c.tcpRpc->write(d, curcon.RecvData.size()); curcon.RecvData.clear(); while (c.tcpRpc->hasOutput()) { auto out = c.tcpRpc->read(65536); curcon.SendData.insert(curcon.SendData.end(), out.begin(), out.end()); } return; } std::vector<uint8_t> rb(reinterpret_cast<const uint8_t*>(curcon.RecvData.data()), reinterpret_cast<const uint8_t*>(curcon.RecvData.data()) + curcon.RecvData.size()); size_t consumed = 0; Loading plugin/smb/smb.h +33 −1 Changes for plugin/smb/smb.h: 33 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -43,10 +43,32 @@ namespace fenrirds { /* dequeue queued response bytes (up to max) */ std::vector<uint8_t> read(size_t max); bool hasOutput() const { return _outPos < _out.size(); } /* an ncacn_ip_tcp endpoint: also serves the endpoint mapper and * drsuapi; towers point at localIp:rpcPort (host byte order) */ void setTcp(uint32_t localIp, uint16_t localPort, uint16_t rpcPort) { _tcp = true; _localIp = localIp; _localPort = localPort; _rpcPort = rpcPort; } private: void handlePdu(const std::vector<uint8_t> &pdu); std::vector<uint8_t> dispatch(const rpc::RequestPdu &req, bool schannel = false); /* Kerberos (16) / SPNEGO (9) RPC authentication, DCE style */ std::vector<uint8_t> gssBind(const rpc::BindPdu &b, bool &ok); std::vector<uint8_t> gssThirdLeg(const std::vector<uint8_t> &tok); struct Gss { bool active = false; /* bind accepted the AP-REQ */ bool spnego = false; uint8_t type = 0, level = 0; uint32_t ctxId = 0; std::vector<uint8_t> key; int etype = 0; bool accSubkey = false; uint64_t sendSeq = 0; std::string principal; std::vector<uint8_t> mechOid; std::vector<uint8_t> mechTypes; /* DER, for the mechListMIC */ }; std::string _name; std::vector<uint8_t> _in; /* request assembly */ Loading @@ -57,6 +79,11 @@ namespace fenrirds { std::vector<uint8_t> _sessionKey; std::vector<uint8_t> _appKey; rpc::NetsecContext _netsec; /* netlogon schannel bind (auth type 68) */ Gss _gss; bool _tcp = false; uint32_t _localIp = 0; uint16_t _localPort = 0; uint16_t _rpcPort = 0; }; /*************************************************************************** Loading @@ -73,8 +100,10 @@ namespace fenrirds { **************************************************************************/ class VISIBILITY SmbServer : public netplus::eventapi, public Plugin { public: /* epmPort/rpcPort: the endpoint mapper and the one DCE/RPC-over-TCP * port (drsuapi, lsarpc, samr, netlogon); 0 disables */ SmbServer(AuthBackend &adminBackend, const std::string &bindAddr, int port, const std::string &dcHost); int port, const std::string &dcHost, int epmPort = 135, int rpcPort = 49152); ~SmbServer() override; const char *name() const override { return "smb"; } Loading Loading @@ -140,6 +169,7 @@ namespace fenrirds { std::map<uint64_t, Session> sessions; std::map<uint64_t, Open> opens; /* volatile file id -> open */ uint64_t nextFid = 1; RpcPipe *tcpRpc = nullptr; /* an ncacn_ip_tcp connection */ }; /* one response of a (possibly compounded) reply */ Loading Loading @@ -191,6 +221,8 @@ namespace fenrirds { AuthBackend &_AdminBackend; std::string _BindAddr; int _Port; int _EpmPort = 135; int _RpcPort = 49152; std::string _DcHost; uint8_t _ServerGuid[16]; Loading src/CMakeLists.txt +2 −0 Changes for src/CMakeLists.txt: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -44,6 +44,8 @@ list(APPEND fenrirdsSrcs rpc/samr.cpp rpc/netlogon.cpp rpc/netsec.cpp rpc/epm.cpp rpc/drsuapi.cpp smb/smb2.cpp ) Loading src/crypto/gss.cpp +26 −1 Changes for src/crypto/gss.cpp: 26 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -135,6 +135,29 @@ std::vector<uint8_t> wrap(const Bytes &key, int etype, bool acceptor, uint64_t s return out; } std::vector<uint8_t> wrapDce(const Bytes &key, int etype, bool acceptor, uint64_t seq, Bytes &data, bool acceptorSubkey) { int usage = acceptor ? ACCEPTOR_SEAL : INITIATOR_SEAL; /* as Windows does: 16 filler octets (EC), encrypt data || filler || * header (RRC = 0 in the encrypted copy) */ const uint16_t ec = 16; auto inner = cfxHeader(acceptor, true, ec, 0, seq, acceptorSubkey); Bytes plain(data); plain.insert(plain.end(), ec, 0); plain.insert(plain.end(), inner.begin(), inner.end()); Bytes enc = krb5::encrypt(etype, key, usage, plain); /* conf | data | filler | hdr, HMAC */ const size_t ck = 12; /* HMAC-SHA1-96 */ const uint16_t rrc = uint16_t(16 + ck); /* rotate right by RRC + EC (the DCE quirk): filler, header copy and * checksum come first, then the confounder, then the data */ std::rotate(enc.begin(), enc.end() - (rrc + ec), enc.end()); std::vector<uint8_t> out = cfxHeader(acceptor, true, ec, rrc, seq, acceptorSubkey); size_t head = rrc + ec + 16; /* + confounder */ out.insert(out.end(), enc.begin(), enc.begin() + head); data.assign(enc.begin() + head, enc.end()); return out; } const uint8_t SPNEGO_OID[8] = {0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02}; std::vector<uint8_t> getMic(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &data, Loading Loading @@ -174,7 +197,7 @@ bool verifyMic(const Bytes &key, int etype, bool senderIsAcceptor, } bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed) { const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed, bool dce) { if (token.size() < 16 || token[0] != 0x05 || token[1] != 0x04 || token[3] != 0xff) return false; bool acceptor = token[2] & 0x01; Loading @@ -185,6 +208,8 @@ bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, *wasSealed = sealed; uint16_t ec = uint16_t(token[4] << 8 | token[5]); uint16_t rrc = uint16_t(token[6] << 8 | token[7]); if (dce && sealed) rrc = uint16_t(rrc + ec); /* undo the right rotation of everything after the header */ Bytes data(token.begin() + 16, token.end()); if (!data.empty() && rrc) { Loading src/crypto/gss.h +10 −1 Changes for src/crypto/gss.h: 10 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -39,11 +39,20 @@ namespace fenrirds { std::vector<uint8_t> wrap(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &plaintext, bool seal = false, bool acceptorSubkey = false); /* DCE/RPC style sealing (MS-KILE 3.4.5.4, GSS_WrapEx without * header signing): `data` (already padded to 16) is encrypted in * place; the returned auth_value is the token header plus the * rotated trailer (RRC = 16 + checksum) and confounder. To open * one, pass auth_value || data to unwrap(). */ std::vector<uint8_t> wrapDce(const Bytes &key, int etype, bool acceptor, uint64_t seq, Bytes &data, bool acceptorSubkey = false); /* verify/decrypt and strip a CFX Wrap token (sealed or not, any * RRC); false on a bad checksum. wasSealed reports which it was. */ /* dce: a DCE/RPC token (auth_value || data), where the EC filler * is rotated along but not counted in RRC (Windows, Heimdal) */ bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed = nullptr); bool *wasSealed = nullptr, bool dce = false); /* CFX MIC token (RFC 4121 §4.2.6.1): a detached signature over data */ std::vector<uint8_t> getMic(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &data, bool acceptorSubkey = false); Loading Loading
plugin/smb/smb.cpp +171 −8 Changes for plugin/smb/smb.cpp: 171 added lines, 8 removed lines. Original line number Diff line number Diff line Loading @@ -6,6 +6,10 @@ #include <cstring> #include <iostream> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/socket.h> #include "crypto/crypto.h" #include "crypto/gss.h" #include "crypto/ntlm.h" Loading @@ -21,6 +25,7 @@ namespace fenrirds { namespace { constexpr uint8_t AUTH_TYPE_SPNEGO = 9, AUTH_TYPE_KRB5 = 16; uint16_t rd16(const uint8_t *p) { return uint16_t(p[0] | p[1] << 8); } uint32_t rd32(const uint8_t *p) { return uint32_t(p[0]) | p[1]<<8 | p[2]<<16 | uint32_t(p[3])<<24; } uint64_t rd64(const uint8_t *p) { uint64_t v=0; for(int i=0;i<8;i++) v|=uint64_t(p[i])<<(8*i); return v; } Loading Loading @@ -164,19 +169,38 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { std::cerr << "[RPC] BIND auth=" << b.hasAuth << " type=" << int(b.authType) << " level=" << int(b.authLevel) << " token=" << b.authToken.size() << "B" << " iface=" << _name << std::endl; auto known = [](const rpc::SyntaxId &a) { return rpc::isLsarpc(a.uuid) || rpc::isSamr(a.uuid) || rpc::isNetlogon(a.uuid); auto known = [this](const rpc::SyntaxId &a) { return rpc::isLsarpc(a.uuid) || rpc::isSamr(a.uuid) || rpc::isNetlogon(a.uuid) || (_tcp && (rpc::isDrsuapi(a.uuid) || rpc::isEpm(a.uuid))); }; std::string secAddr = _tcp ? std::to_string(_localPort) : "\\PIPE\\" + _name; _boundAbstract = b.contexts[0].abstractSyntax; _bound = true; if (b.hasAuth && b.authType == rpc::AUTH_TYPE_SCHANNEL) { if (b.hasAuth && (b.authType == AUTH_TYPE_SPNEGO || b.authType == AUTH_TYPE_KRB5)) { if (h.pduType == rpc::PDU_ALTER && _gss.active) { /* SPNEGO's third leg: the client's AP-REP */ std::vector<uint8_t> out = gssThirdLeg(b.authToken); resp = rpc::buildBindAck(b, secAddr, out, b.authType, b.authLevel, known, b.authContextId); } else { bool ok = false; std::vector<uint8_t> out; if (known(b.contexts[0].abstractSyntax)) out = gssBind(b, ok); if (ok) resp = rpc::buildBindAck(b, secAddr, out, b.authType, b.authLevel, known, b.authContextId); else if (!known(b.contexts[0].abstractSyntax)) resp = rpc::buildBindAck(b, secAddr, {}, 0, 0, known); /* rejects the context */ else resp = rpc::buildBindNak(h.callId); } } else if (b.hasAuth && b.authType == rpc::AUTH_TYPE_SCHANNEL) { /* netlogon secure channel: the client names its computer, we * take the session key of its ServerAuthenticate3 */ rpc::NlAuthNegotiate neg; std::vector<uint8_t> sk; if (rpc::parseNlAuthNegotiate(b.authToken, neg) && rpc::netlogonChannelKey(neg.computer, sk)) { _netsec = rpc::NetsecContext(sk, neg.computer); resp = rpc::buildBindAck(b, "\\PIPE\\" + _name, rpc::buildNlAuthResponse(), resp = rpc::buildBindAck(b, secAddr, rpc::buildNlAuthResponse(), b.authType, b.authLevel, known, b.authContextId); std::cerr << "[RPC] schannel bind for '" << neg.computer << "'" << std::endl; } else { Loading @@ -184,7 +208,7 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { resp = rpc::buildBindNak(h.callId); } } else { resp = rpc::buildBindAck(b, "\\PIPE\\" + _name, {}, 0, 0, known); resp = rpc::buildBindAck(b, secAddr, {}, 0, 0, known); } if (h.pduType == rpc::PDU_ALTER && resp.size() > 2 && resp[2] == rpc::PDU_BIND_ACK) resp[2] = rpc::PDU_ALTER_ACK; Loading @@ -193,6 +217,8 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { } } else if (h.pduType == rpc::PDU_AUTH3) { std::cerr << "[RPC] AUTH3 received (authLen=" << h.authLength << ")" << std::endl; if (_gss.active && h.authLength && h.authLength <= pdu.size()) gssThirdLeg(std::vector<uint8_t>(pdu.end() - h.authLength, pdu.end())); } else if (h.pduType == rpc::PDU_REQUEST) { rpc::RequestPdu req; if (rpc::parseRequest(pdu.data(), pdu.size(), req)) { Loading @@ -209,6 +235,23 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { req.stub.resize(req.stub.size() - req.authPadLength); resp = dispatch(req, true); } } else if (req.hasAuth && (req.authType == AUTH_TYPE_SPNEGO || req.authType == AUTH_TYPE_KRB5)) { bool ok = _gss.active; if (ok && req.authLevel == rpc::AUTH_LEVEL_PRIVACY) { std::vector<uint8_t> tok = req.authToken, plain; tok.insert(tok.end(), req.stub.begin(), req.stub.end()); ok = crypto::gss::unwrap(_gss.key, _gss.etype, false, tok, plain, nullptr, true); if (ok) req.stub = plain; } if (!ok) { std::cerr << "[RPC] GSS request rejected (unwrap) stub=" << req.stub.size() << " pad=" << int(req.authPadLength) << " tok=" << crypto::hex(req.authToken) << std::endl; resp = rpc::buildFault(req.callId, req.contextId, 0x00000005); } else { if (req.authPadLength <= req.stub.size()) req.stub.resize(req.stub.size() - req.authPadLength); resp = dispatch(req); } } else { resp = dispatch(req); } Loading @@ -217,6 +260,73 @@ void RpcPipe::handlePdu(const std::vector<uint8_t> &pdu) { _out.insert(_out.end(), resp.begin(), resp.end()); } /* bind leg: accept the AP-REQ. DCE style (GSS flag 0x1000, what RPC * clients ask for) answers with the raw AP-REP and waits for the * client's AP-REP (AUTH3, or an ALTER_CONTEXT under SPNEGO). */ std::vector<uint8_t> RpcPipe::gssBind(const rpc::BindPdu &b, bool &ok) { ok = false; bool spnego = b.authType == AUTH_TYPE_SPNEGO; std::vector<uint8_t> apReq, mechTypes, inner; if (spnego) { if (!crypto::gss::parseSpnegoInit(b.authToken, apReq, mechTypes)) return {}; } else { apReq = b.authToken; } uint16_t id = 0; if (!crypto::gss::parseInitialToken(apReq, id, inner)) inner = apReq; GssAcceptResult res; if (!g_KdcPlugin || !g_KdcPlugin->gssAccept(inner, res)) { std::cerr << "[RPC] GSS bind: AP-REQ not accepted" << std::endl; return {}; } bool dce = (res.gssFlags & 0x1000) != 0; _gss = Gss(); _gss.active = true; _gss.spnego = spnego; _gss.type = b.authType; _gss.level = b.authLevel; _gss.ctxId = b.authContextId; _gss.key = res.sessionKey; _gss.etype = res.sessionEtype; _gss.accSubkey = res.acceptorSubkey; _gss.sendSeq = res.acceptorSeq; _gss.principal = res.clientPrincipal; std::cerr << "[RPC] GSS bind " << res.clientPrincipal << " type=" << int(b.authType) << " level=" << int(b.authLevel) << " flags=0x" << std::hex << res.gssFlags << std::dec << " etype=" << res.sessionEtype << " dce=" << dce << std::endl; std::vector<uint8_t> krb = dce ? res.apRep : crypto::gss::wrapInitialToken(res.apRep, 0x0200); ok = true; if (!spnego) return krb; _gss.mechOid = crypto::gss::firstMechOid(mechTypes); _gss.mechTypes = mechTypes; return crypto::gss::buildSpnegoResp(dce ? crypto::gss::ACCEPT_INCOMPLETE : crypto::gss::ACCEPT_COMPLETED, &_gss.mechOid, krb, {}); } /* the client's AP-REP of a DCE-style context; under SPNEGO the reply * completes the negotiation */ std::vector<uint8_t> RpcPipe::gssThirdLeg(const std::vector<uint8_t> &tok) { std::cerr << "[RPC] GSS third leg (" << tok.size() << "B) for " << _gss.principal << std::endl; if (!_gss.spnego) return {}; /* the client's NegTokenResp carries its AP-REP and, as RPC clients * send it, a mechListMIC: then ours over the same mechTypes is due * (RFC 4178 5), the first token of our sequence */ std::vector<uint8_t> rt, mic; int st = 0; std::vector<uint8_t> ourMic; if (crypto::gss::parseSpnegoResp(tok, rt, mic, st) && !mic.empty()) { if (!crypto::gss::verifyMic(_gss.key, _gss.etype, false, mic, _gss.mechTypes)) std::cerr << "[RPC] GSS third leg: client mechListMIC does not verify" << std::endl; ourMic = crypto::gss::getMic(_gss.key, _gss.etype, true, _gss.sendSeq++, _gss.mechTypes, _gss.accSubkey); } return crypto::gss::buildSpnegoResp(crypto::gss::ACCEPT_COMPLETED, nullptr, {}, ourMic); } std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel) { rpc::DispatchResult res; const char *iface = "?"; Loading @@ -230,6 +340,17 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel iface = "netlogon"; res = rpc::netlogonDispatch(req.opnum, req.stub, _sessionKey, schannel ? _netsec.computer() : std::string()); } else if (_tcp && rpc::isEpm(_boundAbstract.uuid)) { iface = "epm"; res = rpc::epmDispatch(req.opnum, req.stub, _localIp, _rpcPort); } else if (_tcp && rpc::isDrsuapi(_boundAbstract.uuid)) { iface = "drsuapi"; if (_gss.active) { res = rpc::drsuapiDispatch(req.opnum, req.stub); } else { res.fault = true; res.faultCode = 0x00000005; /* access denied: needs an authenticated bind */ } } else { res.fault = true; res.faultCode = 0x1C010003; /* nca_unk_if */ Loading @@ -246,6 +367,15 @@ std::vector<uint8_t> RpcPipe::dispatch(const rpc::RequestPdu &req, bool schannel } if (res.fault) return rpc::buildFault(req.callId, req.contextId, res.faultCode); if (req.hasAuth && (req.authType == AUTH_TYPE_SPNEGO || req.authType == AUTH_TYPE_KRB5) && _gss.active) { bool seal = req.authLevel == rpc::AUTH_LEVEL_PRIVACY; return rpc::buildAuthResponse(req.callId, req.contextId, res.stub, req.authType, req.authLevel, req.authContextId, [&](std::vector<uint8_t> &data) { if (seal) return crypto::gss::wrapDce(_gss.key, _gss.etype, true, _gss.sendSeq++, data, _gss.accSubkey); return crypto::gss::getMic(_gss.key, _gss.etype, true, _gss.sendSeq++, data, _gss.accSubkey); }); } if (schannel) { bool seal = req.authLevel == rpc::AUTH_LEVEL_PRIVACY; size_t sigLen = req.authToken.size(); Loading Loading @@ -275,13 +405,21 @@ namespace { } SmbServer::SmbServer(AuthBackend &adminBackend, const std::string &bindAddr, int port, const std::string &dcHost) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _DcHost(dcHost) { const std::string &dcHost, int epmPort, int rpcPort) : _AdminBackend(adminBackend), _BindAddr(bindAddr), _Port(port), _EpmPort(epmPort), _RpcPort(rpcPort), _DcHost(dcHost) { DomainRegistry::instance().setAdminBackend(&adminBackend); auto g = crypto::randomBytes(16); std::memcpy(_ServerGuid, g.data(), 16); _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), port, 128, 0)); std::cerr << "SMB plugin listening on " << bindAddr << ":" << port << std::endl; /* DCE/RPC over TCP in the same event loop: the endpoint mapper and the * port it hands out (connections are told apart by their local port) */ for (int p : {epmPort, rpcPort}) { if (p <= 0) continue; _ServerSockets.push_back(new netplus::tcp(bindAddr.c_str(), p, 128, 0)); std::cerr << "RPC (ncacn_ip_tcp) listening on " << bindAddr << ":" << p << std::endl; } } SmbServer::~SmbServer() { Loading @@ -303,7 +441,20 @@ void SmbServer::CreateConnection(std::shared_ptr<netplus::con> &res) { res = std::make_shared<netplus::con>(this); } void SmbServer::ConnectEvent(netplus::con &c, const int, ULONG_PTR) { std::lock_guard<std::mutex> l(_Lock); _Conns[&c] = Conn(); Conn conn; /* which listener: our side of the connection */ sockaddr_in la{}; socklen_t ll = sizeof(la); if (!c.slots.empty() && c.slots[0].csock && getsockname(c.slots[0].csock->fd(), reinterpret_cast<sockaddr*>(&la), &ll) == 0 && la.sin_family == AF_INET) { int lport = ntohs(la.sin_port); if (lport == _EpmPort || lport == _RpcPort) { conn.tcpRpc = new RpcPipe("tcp"); conn.tcpRpc->setTcp(ntohl(la.sin_addr.s_addr), uint16_t(lport), uint16_t(_RpcPort)); } } std::lock_guard<std::mutex> l(_Lock); _Conns[&c] = conn; } void SmbServer::DisconnectEvent(netplus::con &c, const int, ULONG_PTR) { std::cerr << "[SMB] disconnect" << std::endl; /* TEMPORARY interop diagnosis */ Loading @@ -314,6 +465,7 @@ void SmbServer::DisconnectEvent(netplus::con &c, const int, ULONG_PTR) { flush(o.second); delete o.second.pipe; } delete it->second.tcpRpc; _Conns.erase(it); } } Loading @@ -325,6 +477,17 @@ void SmbServer::RequestEvent(netplus::con &curcon, const int, ULONG_PTR) { if (it == _Conns.end()) return; Conn &c = it->second; if (c.tcpRpc) { /* DCE/RPC over TCP: the PDU stream itself */ const uint8_t *d = reinterpret_cast<const uint8_t*>(curcon.RecvData.data()); c.tcpRpc->write(d, curcon.RecvData.size()); curcon.RecvData.clear(); while (c.tcpRpc->hasOutput()) { auto out = c.tcpRpc->read(65536); curcon.SendData.insert(curcon.SendData.end(), out.begin(), out.end()); } return; } std::vector<uint8_t> rb(reinterpret_cast<const uint8_t*>(curcon.RecvData.data()), reinterpret_cast<const uint8_t*>(curcon.RecvData.data()) + curcon.RecvData.size()); size_t consumed = 0; Loading
plugin/smb/smb.h +33 −1 Changes for plugin/smb/smb.h: 33 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -43,10 +43,32 @@ namespace fenrirds { /* dequeue queued response bytes (up to max) */ std::vector<uint8_t> read(size_t max); bool hasOutput() const { return _outPos < _out.size(); } /* an ncacn_ip_tcp endpoint: also serves the endpoint mapper and * drsuapi; towers point at localIp:rpcPort (host byte order) */ void setTcp(uint32_t localIp, uint16_t localPort, uint16_t rpcPort) { _tcp = true; _localIp = localIp; _localPort = localPort; _rpcPort = rpcPort; } private: void handlePdu(const std::vector<uint8_t> &pdu); std::vector<uint8_t> dispatch(const rpc::RequestPdu &req, bool schannel = false); /* Kerberos (16) / SPNEGO (9) RPC authentication, DCE style */ std::vector<uint8_t> gssBind(const rpc::BindPdu &b, bool &ok); std::vector<uint8_t> gssThirdLeg(const std::vector<uint8_t> &tok); struct Gss { bool active = false; /* bind accepted the AP-REQ */ bool spnego = false; uint8_t type = 0, level = 0; uint32_t ctxId = 0; std::vector<uint8_t> key; int etype = 0; bool accSubkey = false; uint64_t sendSeq = 0; std::string principal; std::vector<uint8_t> mechOid; std::vector<uint8_t> mechTypes; /* DER, for the mechListMIC */ }; std::string _name; std::vector<uint8_t> _in; /* request assembly */ Loading @@ -57,6 +79,11 @@ namespace fenrirds { std::vector<uint8_t> _sessionKey; std::vector<uint8_t> _appKey; rpc::NetsecContext _netsec; /* netlogon schannel bind (auth type 68) */ Gss _gss; bool _tcp = false; uint32_t _localIp = 0; uint16_t _localPort = 0; uint16_t _rpcPort = 0; }; /*************************************************************************** Loading @@ -73,8 +100,10 @@ namespace fenrirds { **************************************************************************/ class VISIBILITY SmbServer : public netplus::eventapi, public Plugin { public: /* epmPort/rpcPort: the endpoint mapper and the one DCE/RPC-over-TCP * port (drsuapi, lsarpc, samr, netlogon); 0 disables */ SmbServer(AuthBackend &adminBackend, const std::string &bindAddr, int port, const std::string &dcHost); int port, const std::string &dcHost, int epmPort = 135, int rpcPort = 49152); ~SmbServer() override; const char *name() const override { return "smb"; } Loading Loading @@ -140,6 +169,7 @@ namespace fenrirds { std::map<uint64_t, Session> sessions; std::map<uint64_t, Open> opens; /* volatile file id -> open */ uint64_t nextFid = 1; RpcPipe *tcpRpc = nullptr; /* an ncacn_ip_tcp connection */ }; /* one response of a (possibly compounded) reply */ Loading Loading @@ -191,6 +221,8 @@ namespace fenrirds { AuthBackend &_AdminBackend; std::string _BindAddr; int _Port; int _EpmPort = 135; int _RpcPort = 49152; std::string _DcHost; uint8_t _ServerGuid[16]; Loading
src/CMakeLists.txt +2 −0 Changes for src/CMakeLists.txt: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -44,6 +44,8 @@ list(APPEND fenrirdsSrcs rpc/samr.cpp rpc/netlogon.cpp rpc/netsec.cpp rpc/epm.cpp rpc/drsuapi.cpp smb/smb2.cpp ) Loading
src/crypto/gss.cpp +26 −1 Changes for src/crypto/gss.cpp: 26 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -135,6 +135,29 @@ std::vector<uint8_t> wrap(const Bytes &key, int etype, bool acceptor, uint64_t s return out; } std::vector<uint8_t> wrapDce(const Bytes &key, int etype, bool acceptor, uint64_t seq, Bytes &data, bool acceptorSubkey) { int usage = acceptor ? ACCEPTOR_SEAL : INITIATOR_SEAL; /* as Windows does: 16 filler octets (EC), encrypt data || filler || * header (RRC = 0 in the encrypted copy) */ const uint16_t ec = 16; auto inner = cfxHeader(acceptor, true, ec, 0, seq, acceptorSubkey); Bytes plain(data); plain.insert(plain.end(), ec, 0); plain.insert(plain.end(), inner.begin(), inner.end()); Bytes enc = krb5::encrypt(etype, key, usage, plain); /* conf | data | filler | hdr, HMAC */ const size_t ck = 12; /* HMAC-SHA1-96 */ const uint16_t rrc = uint16_t(16 + ck); /* rotate right by RRC + EC (the DCE quirk): filler, header copy and * checksum come first, then the confounder, then the data */ std::rotate(enc.begin(), enc.end() - (rrc + ec), enc.end()); std::vector<uint8_t> out = cfxHeader(acceptor, true, ec, rrc, seq, acceptorSubkey); size_t head = rrc + ec + 16; /* + confounder */ out.insert(out.end(), enc.begin(), enc.begin() + head); data.assign(enc.begin() + head, enc.end()); return out; } const uint8_t SPNEGO_OID[8] = {0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02}; std::vector<uint8_t> getMic(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &data, Loading Loading @@ -174,7 +197,7 @@ bool verifyMic(const Bytes &key, int etype, bool senderIsAcceptor, } bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed) { const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed, bool dce) { if (token.size() < 16 || token[0] != 0x05 || token[1] != 0x04 || token[3] != 0xff) return false; bool acceptor = token[2] & 0x01; Loading @@ -185,6 +208,8 @@ bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, *wasSealed = sealed; uint16_t ec = uint16_t(token[4] << 8 | token[5]); uint16_t rrc = uint16_t(token[6] << 8 | token[7]); if (dce && sealed) rrc = uint16_t(rrc + ec); /* undo the right rotation of everything after the header */ Bytes data(token.begin() + 16, token.end()); if (!data.empty() && rrc) { Loading
src/crypto/gss.h +10 −1 Changes for src/crypto/gss.h: 10 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -39,11 +39,20 @@ namespace fenrirds { std::vector<uint8_t> wrap(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &plaintext, bool seal = false, bool acceptorSubkey = false); /* DCE/RPC style sealing (MS-KILE 3.4.5.4, GSS_WrapEx without * header signing): `data` (already padded to 16) is encrypted in * place; the returned auth_value is the token header plus the * rotated trailer (RRC = 16 + checksum) and confounder. To open * one, pass auth_value || data to unwrap(). */ std::vector<uint8_t> wrapDce(const Bytes &key, int etype, bool acceptor, uint64_t seq, Bytes &data, bool acceptorSubkey = false); /* verify/decrypt and strip a CFX Wrap token (sealed or not, any * RRC); false on a bad checksum. wasSealed reports which it was. */ /* dce: a DCE/RPC token (auth_value || data), where the EC filler * is rotated along but not counted in RRC (Windows, Heimdal) */ bool unwrap(const Bytes &key, int etype, bool senderIsAcceptor, const std::vector<uint8_t> &token, Bytes &plaintext, bool *wasSealed = nullptr); bool *wasSealed = nullptr, bool dce = false); /* CFX MIC token (RFC 4121 §4.2.6.1): a detached signature over data */ std::vector<uint8_t> getMic(const Bytes &key, int etype, bool acceptor, uint64_t seq, const Bytes &data, bool acceptorSubkey = false); Loading