Commit 3a903ac5 authored by jan.koester's avatar jan.koester
Browse files

netlogon added

parent fb44d04c
Loading
Loading
Loading
Loading

src/netlogon.cpp

0 → 100644
+445 −0
Changes for src/netlogon.cpp: 445 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*******************************************************************************
Copyright (c) 2026, Jan Koester jan.koester@gmx.net
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright
      notice, this list of conditions and the following disclaimer in the
      documentation and/or other materials provided with the distribution.
    * Neither the name of the <organization> nor the
      names of its contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*******************************************************************************/

#include "netlogon.h"

#include <cstring>
#include <stdexcept>
#include <vector>

#include <fcntl.h>
#include <unistd.h>

#include <netplus/socket.h>
#include <netplus/crypto/aes.h>
#include <netplus/crypto/sha.h>

namespace adconnect {
namespace {

using Bytes = std::string;

/* ---- little helpers ---- */
void p16(Bytes &b, uint16_t v) { b += (char)(v & 0xff); b += (char)(v >> 8); }
void p32(Bytes &b, uint32_t v) { for (int i = 0; i < 4; i++) b += (char)(v >> (8 * i)); }
uint16_t g16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); }
uint32_t g32(const uint8_t *p) { return (uint32_t)(p[0] | p[1] << 8 | p[2] << 16 | (uint32_t)p[3] << 24); }
void align4(Bytes &b) { while (b.size() % 4) b += (char)0; }
std::vector<uint8_t> vb(const Bytes &s) { return std::vector<uint8_t>(s.begin(), s.end()); }
Bytes utf16(const std::string &s) { Bytes o; for (char c : s) { o += c; o += (char)0; } return o; }

/* ---- MD4 (the machine account's NT hash: MD4(UTF16LE(password))) ---- */
Bytes md4(const Bytes &msg) {
    auto R = [](uint32_t x, int c) { return (x << c) | (x >> (32 - c)); };
    uint32_t a = 0x67452301, b = 0xefcdab89, c = 0x98badcfe, d = 0x10325476;
    Bytes m = msg;
    uint64_t bl = (uint64_t)msg.size() * 8;
    m += (char)0x80;
    while (m.size() % 64 != 56) m += (char)0;
    for (int i = 0; i < 8; i++) m += (char)(bl >> (8 * i));
    for (size_t off = 0; off < m.size(); off += 64) {
        uint32_t X[16];
        for (int i = 0; i < 16; i++) X[i] = g32((const uint8_t *)m.data() + off + i * 4);
        uint32_t A = a, B = b, C = c, D = d;
        auto F = [&](uint32_t x, uint32_t y, uint32_t z) { return (x & y) | (~x & z); };
        auto G = [&](uint32_t x, uint32_t y, uint32_t z) { return (x & y) | (x & z) | (y & z); };
        auto H = [&](uint32_t x, uint32_t y, uint32_t z) { return x ^ y ^ z; };
        int r1[] = {3, 7, 11, 19};
        for (int i = 0; i < 16; i++) { uint32_t t = A + F(B, C, D) + X[i]; A = D; D = C; C = B; B = R(t, r1[i % 4]); }
        int o2[] = {0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15}, r2[] = {3, 5, 9, 13};
        for (int i = 0; i < 16; i++) { uint32_t t = A + G(B, C, D) + X[o2[i]] + 0x5a827999u; A = D; D = C; C = B; B = R(t, r2[i % 4]); }
        int o3[] = {0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}, r3[] = {3, 9, 11, 15};
        for (int i = 0; i < 16; i++) { uint32_t t = A + H(B, C, D) + X[o3[i]] + 0x6ed9eba1u; A = D; D = C; C = B; B = R(t, r3[i % 4]); }
        a += A; b += B; c += C; d += D;
    }
    Bytes o;
    for (uint32_t v : {a, b, c, d}) p32(o, v);
    return o;
}

Bytes hmacSha256(const Bytes &key, const Bytes &data) {
    auto h = netplus::hmac_sha256(vb(key), vb(data));
    return Bytes(h.begin(), h.end());
}

/* ---- AES-128-CFB8 with a 16-byte IV (symmetric; enc=true to encrypt) ---- */
void aesCfb8(const Bytes &key16, const uint8_t iv16[16], uint8_t *buf, size_t n, bool enc) {
    netplus::aes128 c(vb(key16));
    uint8_t sr[16];
    memcpy(sr, iv16, 16);
    for (size_t i = 0; i < n; i++) {
        uint8_t ks[16];
        c.encrypt_ecb(sr, ks);
        uint8_t in = buf[i], out = (uint8_t)(in ^ ks[0]);
        uint8_t fb = enc ? out : in;
        memmove(sr, sr + 1, 15);
        sr[15] = fb;
        buf[i] = out;
    }
}

/* ---- netsec schannel AES sign+seal (MS-NRPC 3.3.4.2.1; samba schannel_sign.c) ---- */
void ssval(uint8_t *p, int o, uint16_t v) { p[o] = (uint8_t)(v & 0xff); p[o + 1] = (uint8_t)(v >> 8); }

/* checksum (8 bytes) over the signature header plus optional confounder plus data */
void netsecSign(const Bytes &sk, const uint8_t *conf, const uint8_t *data, size_t len, uint8_t header[8], uint8_t checksum[8]) {
    ssval(header, 0, 0x0013);                 /* NL_SIGN_HMAC_SHA256 */
    ssval(header, 2, conf ? 0x001A : 0xFFFF); /* NL_SEAL_AES128 / NL_SEAL_NONE */
    ssval(header, 4, 0xFFFF);
    ssval(header, 6, 0x0000);
    Bytes m;
    m.append((char *)header, 8);
    if (conf) m.append((char *)conf, 8);
    m.append((char *)data, len);
    Bytes d = hmacSha256(sk, m);
    memcpy(checksum, d.data(), 8);
}

void seqBytes(uint64_t seq, bool initiator, uint8_t sq[8]) {
    uint32_t s = (uint32_t)seq;                  /* RSIVAL: low 32 bits big-endian */
    sq[0] = (uint8_t)(s >> 24); sq[1] = (uint8_t)(s >> 16); sq[2] = (uint8_t)(s >> 8); sq[3] = (uint8_t)s;
    sq[4] = initiator ? 0x80 : 0x00; sq[5] = 0; sq[6] = 0; sq[7] = 0;
}

/* seal stub in place (outgoing), return the 56-byte signature blob */
Bytes netsecSealOut(const Bytes &sk, uint64_t seq, uint8_t *stub, size_t len) {
    uint8_t sq[8];
    seqBytes(seq, true, sq);
    uint8_t conf[8];
    { int fd = ::open("/dev/urandom", O_RDONLY); if (fd >= 0) { if (::read(fd, conf, 8) != 8) memset(conf, 0, 8); ::close(fd); } else memset(conf, 0, 8); }
    uint8_t header[8], checksum[8];
    netsecSign(sk, conf, stub, len, header, checksum);
    Bytes key = sk;
    for (auto &ch : key) ch = (char)((unsigned char)ch ^ 0xf0);
    uint8_t iv[16];
    memcpy(iv, sq, 8); memcpy(iv + 8, sq, 8);
    Bytes blob;
    blob.append((char *)conf, 8);
    blob.append((char *)stub, len);
    aesCfb8(key, iv, (uint8_t *)blob.data(), blob.size(), true);
    memcpy(conf, blob.data(), 8);
    memcpy(stub, blob.data() + 8, len);
    uint8_t iv2[16];
    memcpy(iv2, checksum, 8); memcpy(iv2 + 8, checksum, 8);
    aesCfb8(sk, iv2, sq, 8, true);
    Bytes sig(56, '\0');
    memcpy(&sig[0], header, 8);
    memcpy(&sig[8], sq, 8);
    memcpy(&sig[16], checksum, 8);
    memcpy(&sig[24], conf, 8);
    return sig;
}

/* decrypt an incoming response's sealed stub in place (seq is our counter) */
void netsecUnseal(const Bytes &sk, uint64_t seq, const uint8_t confWire[8], uint8_t *stub, size_t len) {
    uint8_t sq[8];
    seqBytes(seq, false, sq);
    Bytes key = sk;
    for (auto &ch : key) ch = (char)((unsigned char)ch ^ 0xf0);
    uint8_t iv[16];
    memcpy(iv, sq, 8); memcpy(iv + 8, sq, 8);
    Bytes blob;
    blob.append((char *)confWire, 8);
    blob.append((char *)stub, len);
    aesCfb8(key, iv, (uint8_t *)blob.data(), blob.size(), false);
    memcpy(stub, blob.data() + 8, len);
}

/* session key (AES): HMAC-SHA256(NThash, ClientChallenge||ServerChallenge)[0..15] */
Bytes sessionKey(const Bytes &nthash, const Bytes &cc, const Bytes &sc) {
    return hmacSha256(nthash, cc + sc).substr(0, 16);
}
/* credential for the handshake: AES-128-CFB8(sessionkey, challenge), IV 0 */
Bytes credential(const Bytes &sk, const Bytes &chal8) {
    uint8_t iv[16] = {0};
    Bytes out = chal8.substr(0, 8);
    aesCfb8(sk, iv, (uint8_t *)out.data(), out.size(), true);
    return out;
}

/* ---- NDR for the SamLogonEx (NetlogonNetworkInformation) request stub ----
 * Built to match a captured, DC-accepted request. Top-level [unique,string]
 * pointers are inline (referent then the NUL-terminated conformant string);
 * the netr_NetworkInfo pointee is inline after its referent, with its own
 * member string/response pointees deferred to the end of the struct; then
 * validation_level + flags; then the DCE/RPC verification trailer. */
void ndrUniqStr(Bytes &s, uint32_t &ref, const std::string &w) {
    p32(s, ref); ref += 4;
    uint32_t n = (uint32_t)w.size() + 1; /* incl NUL */
    p32(s, n); p32(s, 0); p32(s, n);
    s += utf16(w); s += (char)0; s += (char)0;
    align4(s);
}
void lsaFixed(Bytes &s, uint32_t &ref, const std::string &w) {
    uint16_t lb = (uint16_t)(w.size() * 2); /* bytes, no NUL */
    p16(s, lb); p16(s, lb); p32(s, ref); ref += 4;
}
void lsaData(Bytes &s, const std::string &w) {
    uint32_t u = (uint32_t)w.size(); /* units */
    p32(s, u); p32(s, 0); p32(s, u);
    s += utf16(w);
    align4(s);
}
void crFixed(Bytes &s, uint32_t &ref, const Bytes &r) {
    uint16_t l = (uint16_t)r.size();
    p16(s, l); p16(s, l);
    if (r.empty()) { p32(s, 0); } else { p32(s, ref); ref += 4; }
}
void crData(Bytes &s, const Bytes &r) {
    if (r.empty()) return;
    p32(s, (uint32_t)r.size());
    s += r;
    align4(s);
}
void secVt(Bytes &s) {
    align4(s);
    static const uint8_t magic[8] = {0x8a, 0xe3, 0x13, 0x71, 0x02, 0xf4, 0x36, 0x71};
    s.append((char *)magic, 8);
    /* SEC_VT_COMMAND_BITMASK_1: value 0 (we do not sign the PDU header) */
    p16(s, 0x0001); p16(s, 0x0004); p32(s, 0x00000000);
    /* SEC_VT_COMMAND_PCONTEXT (| END): abstract NETLOGON v1.0 + transfer NDR v2.0 */
    p16(s, 0x4002); p16(s, 0x0028);
    static const uint8_t nl[16] = {0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0xcd, 0xab, 0xef, 0x00, 0x01, 0x23, 0x45, 0x67, 0xcf, 0xfb};
    s.append((char *)nl, 16); p32(s, 1);
    static const uint8_t ndr[16] = {0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60};
    s.append((char *)ndr, 16); p32(s, 2);
}

Bytes buildNetworkStub(const std::string &server, const std::string &computer, const std::string &domain,
                       const std::string &account, const std::string &workstation, const Bytes &challenge8,
                       const Bytes &nt, const Bytes &lm) {
    Bytes s;
    uint32_t ref = 0x00020000;
    ndrUniqStr(s, ref, server);
    ndrUniqStr(s, ref, computer);
    p16(s, 2);               /* logon_level = NetlogonNetworkInformation */
    p16(s, 2);               /* union discriminant */
    p32(s, ref); ref += 4;   /* netr_NetworkInfo referent (pointee inline below) */
    /* netr_NetworkInfo fixed part */
    lsaFixed(s, ref, domain);
    p32(s, 0);               /* parameter_control */
    p32(s, 0); p32(s, 0);    /* logon_id (hyper) */
    lsaFixed(s, ref, account);
    lsaFixed(s, ref, workstation);
    s.append(challenge8, 0, 8);
    crFixed(s, ref, nt);
    crFixed(s, ref, lm);
    /* netr_NetworkInfo deferred part */
    lsaData(s, domain);
    lsaData(s, account);
    lsaData(s, workstation);
    crData(s, nt);
    crData(s, lm);
    /* remaining top-level [in] params */
    p16(s, 6);               /* validation_level = NetlogonValidationSamInfo4 */
    p16(s, 0);               /* align to 4 */
    p32(s, 0);               /* flags */
    secVt(s);
    return s;
}

/* ---- DCE/RPC over a libnetplus TCP connection ---- */
struct Rpc {
    std::unique_ptr<netplus::tcp> sock;
    Bytes rx;
    uint32_t callid = 1;
    uint32_t assoc = 0;

    void conn(const std::string &host, int port, int ms) {
        sock = std::make_unique<netplus::tcp>();
        sock->connectTimeout(host, port, ms);
        int fd = sock->fd();
        int fl = fcntl(fd, F_GETFL, 0);
        if (fl != -1) fcntl(fd, F_SETFL, fl & ~O_NONBLOCK); /* block in recvData */
    }
    void sendAll(const Bytes &d) {
        size_t off = 0;
        while (off < d.size()) {
            netplus::buffer b(d.data() + off, d.size() - off);
            size_t n = sock->sendData(b);
            if (n == 0) throw std::runtime_error("netlogon: connection closed while sending");
            off += n;
        }
        while (sock->hasPendingWrite()) sock->flush_out();
    }
    Bytes recvN(size_t n) {
        char b[16384];
        while (rx.size() < n) {
            netplus::buffer buf(sizeof b);
            size_t k = sock->recvData(buf);
            if (k == 0) throw std::runtime_error("netlogon: connection closed while receiving");
            rx.append(buf.data.buf, k);
        }
        Bytes r = rx.substr(0, n);
        rx.erase(0, n);
        return r;
    }
    Bytes recvPdu() {
        Bytes hdr = recvN(16);
        uint16_t frag = g16((const uint8_t *)hdr.data() + 8);
        if (frag < 16) throw std::runtime_error("netlogon: short PDU");
        return hdr + recvN(frag - 16);
    }
    void putCommonHeader(Bytes &h, uint8_t ptype, uint16_t fragLen, uint16_t authLen) {
        h += (char)5; h += (char)0; h += (char)ptype; h += (char)3; /* ver, minor, ptype, flags first+last */
        p32(h, 0x00000010);                                         /* little-endian data rep */
        p16(h, fragLen); p16(h, authLen); p32(h, callid++);
    }
    void bindOrAlter(uint8_t ptype, const Bytes &authval, uint8_t authType, uint8_t authLevel) {
        Bytes body;
        p16(body, 5840); p16(body, 5840); p32(body, ptype == 11 ? 0 : assoc);
        body += (char)1; body += (char)0; p16(body, 0);
        p16(body, 0); body += (char)1; body += (char)0;
        static const uint8_t ns[16] = {0x78, 0x56, 0x34, 0x12, 0x34, 0x12, 0xcd, 0xab, 0xef, 0x00, 0x01, 0x23, 0x45, 0x67, 0xcf, 0xfb};
        body.append((char *)ns, 16); p32(body, 1);
        static const uint8_t ts[16] = {0x04, 0x5d, 0x88, 0x8a, 0xeb, 0x1c, 0xc9, 0x11, 0x9f, 0xe8, 0x08, 0x00, 0x2b, 0x10, 0x48, 0x60};
        body.append((char *)ts, 16); p32(body, 2);
        Bytes tail;
        if (!authval.empty()) {
            tail += (char)authType; tail += (char)authLevel; tail += (char)0; tail += (char)0; p32(tail, 0);
            tail += authval;
        }
        Bytes h;
        putCommonHeader(h, ptype, (uint16_t)(16 + body.size() + tail.size()), (uint16_t)authval.size());
        sendAll(h + body + tail);
        Bytes r = recvPdu();
        uint8_t want = ptype == 11 ? 12 : 15;
        if ((uint8_t)r[2] != want) throw std::runtime_error("netlogon: unexpected bind/alter response");
        if (ptype == 11) assoc = g32((const uint8_t *)r.data() + 20);
    }
    /* a plaintext request (handshake calls), returns the response stub */
    Bytes call(uint16_t opnum, const Bytes &stub) {
        Bytes h;
        putCommonHeader(h, 0, (uint16_t)(16 + 8 + stub.size()), 0);
        Bytes req = h;
        p32(req, (uint32_t)stub.size()); p16(req, 0); p16(req, opnum);
        req += stub;
        sendAll(req);
        Bytes r = recvPdu();
        if ((uint8_t)r[2] != 2) throw std::runtime_error("netlogon: request faulted");
        return r.substr(24);
    }
    /* a schannel-sealed request, returns the decrypted response stub */
    Bytes callSealed(uint16_t opnum, Bytes stub, const Bytes &sk, uint64_t &seq) {
        Bytes data = stub;
        align4(data);
        uint8_t padlen = (uint8_t)(data.size() - stub.size());
        Bytes sig = netsecSealOut(sk, seq++, (uint8_t *)data.data(), data.size());
        Bytes sect;
        sect += (char)68; sect += (char)6; sect += (char)padlen; sect += (char)0; p32(sect, 0);
        Bytes h;
        putCommonHeader(h, 0, (uint16_t)(16 + 8 + data.size() + 8 + 56), 56);
        Bytes req = h;
        p32(req, (uint32_t)stub.size()); p16(req, 0); p16(req, opnum);
        req += data; req += sect; req += sig;
        sendAll(req);
        Bytes r = recvPdu();
        if ((uint8_t)r[2] != 2) throw std::runtime_error("netlogon: sealed request faulted");
        uint16_t al = g16((const uint8_t *)r.data() + 10);
        size_t frag = r.size();
        if (al == 0 || frag < 24 + 8 + al) throw std::runtime_error("netlogon: bad sealed response");
        Bytes rstub = r.substr(24, frag - 8 - al - 24);
        const uint8_t *rsig = (const uint8_t *)r.data() + frag - al;
        uint8_t padl = (uint8_t)r[frag - al - 8 + 2];
        netsecUnseal(sk, seq++, rsig + 24, (uint8_t *)rstub.data(), rstub.size());
        if (padl <= rstub.size()) rstub.resize(rstub.size() - padl);
        return rstub;
    }
};

Bytes randBytes(size_t n) {
    Bytes r(n, '\0');
    int fd = ::open("/dev/urandom", O_RDONLY);
    if (fd >= 0) { if (::read(fd, &r[0], n) != (ssize_t)n) {} ::close(fd); }
    return r;
}

} // namespace

NtlmResult samLogonNetwork(const NetlogonConfig &cfg, const NtlmPassthrough &in) {
    if (cfg.rpcPort <= 0) throw std::runtime_error("netlogon: no RPC port");
    if (in.serverChallenge.size() != 8) throw std::runtime_error("netlogon: server challenge must be 8 bytes");

    Bytes nthash = md4(utf16(cfg.machinePassword));
    Rpc r;
    r.conn(cfg.dcHost, cfg.rpcPort, cfg.timeoutMs);
    r.bindOrAlter(11, Bytes(), 0, 0); /* plain bind */

    /* ServerReqChallenge (opnum 4): a random client challenge */
    Bytes cc = randBytes(8);
    Bytes reqStub;
    uint32_t ref = 0x00020000;
    { Bytes dummy; (void)dummy; }
    p32(reqStub, 0);                       /* PrimaryName = NULL */
    { uint32_t n = (uint32_t)cfg.computer.size() + 1; p32(reqStub, n); p32(reqStub, 0); p32(reqStub, n);
      reqStub += utf16(cfg.computer); reqStub += (char)0; reqStub += (char)0; align4(reqStub); }
    reqStub += cc;
    Bytes o = r.call(4, reqStub);
    Bytes sc = o.substr(0, 8);
    (void)ref;

    Bytes sk = sessionKey(nthash, cc, sc);

    /* ServerAuthenticate3 (opnum 26) */
    Bytes a;
    p32(a, 0);                             /* PrimaryName = NULL */
    { uint32_t n = (uint32_t)cfg.machineAccount.size() + 1; p32(a, n); p32(a, 0); p32(a, n);
      a += utf16(cfg.machineAccount); a += (char)0; a += (char)0; align4(a); }
    p16(a, 2);                             /* secure_channel_type = WorkstationSecureChannel (enum16) */
    { uint32_t n = (uint32_t)cfg.computer.size() + 1; p32(a, n); p32(a, 0); p32(a, n);
      a += utf16(cfg.computer); a += (char)0; a += (char)0; align4(a); }
    a += credential(sk, cc);
    uint32_t neg = 0x610fffff | 0x01000000 | 0x00004000;
    p32(a, neg);
    Bytes ao = r.call(26, a);
    uint32_t authStatus = g32((const uint8_t *)ao.data() + ao.size() - 4);
    if (authStatus != 0) throw std::runtime_error("netlogon: ServerAuthenticate3 failed (0x" +
                                                   [&] { char b[16]; snprintf(b, sizeof b, "%08x", authStatus); return std::string(b); }() + ")");
    Bytes servCred = ao.substr(0, 8);
    if (servCred != credential(sk, sc)) throw std::runtime_error("netlogon: server credential mismatch");

    /* schannel: alter_context carrying NL_AUTH_MESSAGE (domain + computer, OEM) */
    Bytes nl;
    p32(nl, 0); p32(nl, 0x00000003);
    nl += cfg.domainNetbios; nl += (char)0;
    nl += cfg.computer; nl += (char)0;
    r.bindOrAlter(14, nl, 68, 6);

    /* SamLogonEx (opnum 39), network info, sealed */
    Bytes stub = buildNetworkStub(cfg.dcHost, cfg.computer, in.domain, in.account, in.workstation,
                                  in.serverChallenge, in.ntResponse, in.lmResponse);
    uint64_t seq = 0;
    Bytes resp = r.callSealed(39, stub, sk, seq);

    NtlmResult res;
    if (resp.size() < 4) throw std::runtime_error("netlogon: short SamLogonEx response");
    res.status = g32((const uint8_t *)resp.data() + resp.size() - 4);
    if (res.status == 0 && resp.size() >= 128 + 16) res.sessionKey = resp.substr(128, 16);
    return res;
}

} // namespace adconnect