Network Reconnaissance and Scanning
Network reconnaissance maps an environment from the inside — finding live hosts, open ports, services, and domain structure. Every technique leaves network-layer and host-layer telemetry. Detection engineering for recon relies heavily on anomaly over signature: a single port connection is normal; 254 connections to port 445 within 30 seconds from one host is a sweep. Understanding what's normal in your environment is the prerequisite.
You have code execution on a workstation inside a /16 corporate network. You need to: enumerate live hosts, find all machines with SMB open (potential lateral movement targets), identify domain controllers (LDAP/Kerberos on 389/88), and find any web servers or VPNs reachable from this segment — all within the first 20 minutes, before the NOC's anomaly detection fires on sustained scanning activity.
Recon Phases and Telemetry
| Phase | Technique | Network artifact | Detection source |
|---|---|---|---|
| Host discovery | ARP sweep (local subnet) | ARP requests to all .1–.254 | Network switch ARP table anomalies, NDR |
| Host discovery | ICMP ping sweep | ICMP echo to subnet range | Firewall logs, NetFlow, NDR |
| Port scan | TCP SYN scan | SYN packets to many ports/hosts | Firewall deny logs, NetFlow byte counts |
| Service ID | Banner grabbing | TCP connect + short read | IDS, application logs |
| AD recon | LDAP queries | LDAP traffic to DC:389 | DC Event 4662, Defender for Identity |
| Share enum | NetShareEnum via SMB | SMB tree connects to multiple hosts | Event 5140 (share access), NetFlow |
TCP Port Scanner in C
// Async TCP SYN-style scanner using non-blocking connect() + select().
// Scans a list of ports across a target IP range with configurable thread count.
// Faster than serial connect; avoids raw socket privileges needed for true SYN scan.
#include <winsock2.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")
BOOL PortOpen(DWORD ip, USHORT port, int timeoutMs) {
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) return FALSE;
// Set non-blocking
u_long nb = 1;
ioctlsocket(s, FIONBIO, &nb);
struct sockaddr_in sa = {0};
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = ip;
connect(s, (struct sockaddr*)&sa, sizeof(sa));
fd_set wfds; FD_ZERO(&wfds); FD_SET(s, &wfds);
struct timeval tv = { timeoutMs / 1000, (timeoutMs % 1000) * 1000 };
int r = select(0, NULL, &wfds, NULL, &tv);
BOOL open = FALSE;
if (r > 0) {
int err; int len = sizeof(err);
getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len);
open = (err == 0);
}
closesocket(s);
return open;
}
VOID ScanSubnet(BYTE baseA, BYTE baseB, BYTE baseC) {
WSADATA wsa; WSAStartup(MAKEWORD(2,2), &wsa);
static USHORT targets[] = {
22, 80, 88, 135, 139, 389, 443,
445, 3389, 5985, 8080, 8443
};
int nPorts = sizeof(targets) / sizeof(targets[0]);
for (int h = 1; h <= 254; h++) {
DWORD ip = inet_addr("");
BYTE ipBytes[4] = { baseA, baseB, baseC, (BYTE)h };
memcpy(&ip, ipBytes, 4);
for (int p = 0; p < nPorts; p++) {
if (PortOpen(ip, targets[p], 300)) {
printf("%d.%d.%d.%d:%d OPEN\n",
baseA, baseB, baseC, h, targets[p]);
}
}
}
WSACleanup();
}
Internal AD Recon via NetAPI
// Windows NetAPI32 functions enumerate domain structure without LDAP.
// NetServerEnum: list all servers by type in a domain.
// NetShareEnum: list shares on a host. Both require only domain user auth.
#include <windows.h>
#include <lm.h>
#pragma comment(lib, "netapi32.lib")
VOID EnumDomainControllers(LPCWSTR domain) {
SERVER_INFO_101* buf = NULL;
DWORD total, resume = 0;
// SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL = all DCs
NET_API_STATUS r = NetServerEnum(NULL, 101, (LPBYTE*)&buf,
MAX_PREFERRED_LENGTH, &total, &total,
SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL,
domain, &resume);
if (r == NERR_Success || r == ERROR_MORE_DATA) {
for (DWORD i = 0; i < total; i++)
wprintf(L"DC: \\\\%s\n", buf[i].sv101_name);
NetApiBufferFree(buf);
}
}
VOID EnumShares(LPCWSTR server) {
SHARE_INFO_1* buf = NULL;
DWORD read, total, resume = 0;
NET_API_STATUS r = NetShareEnum((LPWSTR)server, 1,
(LPBYTE*)&buf, MAX_PREFERRED_LENGTH,
&read, &total, &resume);
if (r == NERR_Success) {
for (DWORD i = 0; i < read; i++)
wprintf(L" %s\\%s (%s)\n",
server, buf[i].shi1_netname, buf[i].shi1_remark);
NetApiBufferFree(buf);
}
}
ARP Sweep
// ARP sweep: send ARP requests to all hosts in a subnet.
// Responses identify live hosts (even if they block ICMP).
// Requires raw socket (admin) or use SendARP() — no raw socket needed.
// SendARP: Windows built-in; fires ARP for each IP; live hosts respond with MAC.
#include <winsock2.h>
#include <iphlpapi.h>
#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")
VOID ArpSweep(DWORD networkBase, DWORD mask) {
// networkBase = e.g., inet_addr("192.168.1.0")
DWORD hostMin = (ntohl(networkBase) & ntohl(mask)) + 1;
DWORD hostMax = hostMin | (~ntohl(mask)) - 1;
for (DWORD h = hostMin; h <= hostMax; h++) {
DWORD ip = htonl(h);
ULONG mac[2] = {0};
ULONG macLen = 6;
DWORD r = SendARP(ip, 0, mac, &macLen);
if (r == NO_ERROR) {
BYTE* m = (BYTE*)mac;
IN_ADDR addr; addr.S_un.S_addr = ip;
printf("%s MAC: %02X:%02X:%02X:%02X:%02X:%02X\n",
inet_ntoa(addr), m[0],m[1],m[2],m[3],m[4],m[5]);
}
}
}
Service Banner Grabbing
// Banner grabbing: connect to an open port, send a minimal probe,
// read the response to identify the service and version.
// HTTP: send "HEAD / HTTP/1.0\r\n\r\n" → Server: header reveals web server
// SMTP: connect, read 220 banner
// SSH: connect, read SSH-2.0-OpenSSH_X.Y banner
// SMB: read first 4 bytes of NetBIOS header then SMB negotiate response
BOOL GrabBanner(LPCSTR ipStr, USHORT port,
char* banner, int bannerSz) {
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sa = {0};
sa.sin_family = AF_INET;
sa.sin_port = htons(port);
sa.sin_addr.s_addr = inet_addr(ipStr);
int to = 2000;
setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (char*)&to, sizeof(to));
setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (char*)&to, sizeof(to));
if (connect(s, (struct sockaddr*)&sa, sizeof(sa)) != 0) {
closesocket(s); return FALSE;
}
// Send HTTP probe for port 80/8080/443
if (port == 80 || port == 8080)
send(s, "HEAD / HTTP/1.0\r\n\r\n", 18, 0);
int n = recv(s, banner, bannerSz - 1, 0);
if (n > 0) banner[n] = 0;
closesocket(s);
return (n > 0);
}
Detection Engineering
title: Port Scan — Many Distinct Ports Contacted from Single Host
logsource:
product: windows
category: network_connection
detection:
selection:
EventID: 3 # Sysmon network connect
Initiated: 'true'
timeframe: 60s
condition: selection | count(DestinationPort) by SourceIp > 20
level: high
tags: [attack.discovery, T1046]
title: SMB Sweep — Connections to Many Hosts on Port 445
logsource:
product: windows
service: security
detection:
selection:
EventID: 5156 # Windows Filtering Platform connection allowed
DestPort: 445
timeframe: 30s
condition: selection | count(DestAddress) by SourceAddress > 10
level: high
-- MDE KQL: host scanning many IPs on privileged ports
DeviceNetworkEvents
| where Timestamp > ago(5m)
| where RemotePort in (22, 80, 135, 139, 389, 443, 445, 3389, 5985)
| where ActionType == "ConnectionAttempted"
| summarize
distinct_ips = dcount(RemoteIP),
distinct_ports = dcount(RemotePort),
total = count()
by DeviceName, InitiatingProcessFileName, bin(Timestamp, 1m)
| where distinct_ips > 15 or distinct_ports > 8
| order by distinct_ips desc
-- NetAPI share enum: many 5140 events (share access) from one source
SecurityEvent
| where EventID == 5140
| where ShareName != "\\*\\IPC$"
| summarize shares_accessed = dcount(ShareName), hosts = dcount(Computer)
by SubjectAccount, bin(TimeGenerated, 5m)
| where shares_accessed > 5 or hosts > 5
Q&A
An attacker uses Living-off-the-Land tools like net.exe, nltest.exe, and arp.exe for reconnaissance instead of a custom scanner — why is this harder to detect, and what behavioral analytics still catch it?
LOLBin-based reconnaissance is harder to detect at the tool level because each individual execution of net view, arp -a, nltest /dclist, or ping is a completely normal, signed Windows operation. Process creation signatures for these binaries fire constantly in enterprise environments. A helpdesk ticket triggers net user. Domain login scripts run nltest. There is no "this binary is suspicious" signal.
The detection pivot is temporal and contextual correlation, not per-process signatures. Three behavioral analytics that reliably catch LOLBin recon: (1) Process chain anomaly: net.exe, nltest.exe, and arp.exe executing within a 5-minute window with the same parent process (especially if that parent is cmd.exe or powershell.exe spawned from an unusual grandparent like winword.exe) is anomalous regardless of what each child does. The sequence, not the individual process, is the signal. (2) Account context anomaly: if the user account running these commands has never historically run nltest or net view /domain, and suddenly runs six of them in sequence from a workstation at 2 AM, ML-based user behavior analytics (UEBA) will flag the deviation from the user's baseline. (3) Output-to-network correlation: LOLBin recon discovers IP addresses and hostnames; a short time later, new outbound SMB connections to those IPs from the same host is the "recon → lateral movement" kill-chain pattern. Correlating discovery-phase LOLBin execution with subsequent new network connections to hosts not previously contacted by that workstation is a high-fidelity detection chain that is independent of which specific recon tool was used.