diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1a72df..d3d8d4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,18 +14,18 @@ Start by forking the repository to your own GitHub account. Clone the forked repository to your local machine: -\`\`\`bash +```bash git clone https://github.com//hostRecon.git cd hostRecon -\`\`\` +``` ### 3. Create a new branch Before making any changes, create a new branch from \`main\`: -\`\`\`bash +```bash git checkout -b feature-branch -\`\`\` +``` ### 4. Make your changes @@ -35,9 +35,9 @@ Work on your changes, ensuring they follow the project's coding style and guidel Before submitting your changes, run the existing tests to ensure everything works as expected: -\`\`\`bash +```bash make test -\`\`\` +``` If you're adding a new feature or fixing a bug, consider adding corresponding tests in the \`tests/\` directory. @@ -45,18 +45,18 @@ If you're adding a new feature or fixing a bug, consider adding corresponding te Once you're done, commit your changes to the branch: -\`\`\`bash +```bash git add . git commit -m "Describe your changes" -\`\`\` +``` ### 7. Push your changes Push the changes to your fork: -\`\`\`bash +```bash git push origin feature-branch -\`\`\` +``` ### 8. Create a pull request @@ -75,4 +75,3 @@ If you find a bug or have a feature request, please open an issue in the [Issues By contributing to **hostRecon**, you agree that your contributions will be licensed under the project's license (e.g., MIT, GPL). Thank you for your interest in contributing to **hostRecon**! We look forward to your contributions. -""" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b4512af --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +CXX = g++ +CXXFLAGS = -Wall -std=c++17 +LIBS = -lpcap + +OBJS = src/networkScanner.o src/hostReconLib.o + +networkScanner: $(OBJS) + $(CXX) $(OBJS) $(LIBS) -o $@ + +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJS) networkScanner diff --git a/README.md b/README.md index b6dad76..480c08a 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -![Workflow Status](https://github.com/kmccol1/hostRecon/actions/workflows/cpp.yml/badge.svg) +![Workflow Status](https://github.com/mcckyle/hostRecon/actions/workflows/cpp.yml/badge.svg) # hostRecon +## Intelligent Local Network Discovery with libpcap + ## Overview -**hostRecon** is a fast, efficient, and easy-to-use CLI-based network scanner that facilitates host discovery and availability checks on local networks. Leveraging the power of `libpcap`, this tool performs network reconnaissance through ICMP Echo Request (ping) messages and captures responses from live hosts. It’s a valuable tool for network administrators, cybersecurity professionals, and anyone wanting to explore their local network. +**hostRecon** is a simple, lightweight, CLI-based network scanner for discovering active hosts on local networks. Powered by `libpcap`, it performs low-level packet injection and capture for precise and reliable network reconnaissance. This tool is ideal for developers, sysadmins, cybersecurity enthusiasts seeking a deeper look at the devices on their LAN. ## Table of Contents - [Features](#features) - [Installation](#installation) -- [Usage](#usage) +- [Build](#usage) - [Current State](#current-state) - [Future State](#future-state) - [Contributing](#contributing) @@ -18,64 +20,33 @@ ## Features -- **Fast & Efficient**: Designed for quick host discovery via ICMP Echo Requests. -- **Low-Level Packet Manipulation**: Uses `libpcap` for low-level packet capture and injection. -- **Multi-Host Scanning**: Allows the concurrent scanning of multiple hosts to save time. -- **Customizable Network Configuration**: Set custom source and destination IP addresses. -- **Real-Time Active Host Display**: Instantly shows hosts that are up and responsive. -- **Error Resilience**: Includes robust error handling for packet capture failures and network interface issues. +- **Accurate Host Detection**: Uses ARP and ICMP scanning to identify active devices on the local network. +- **Low-Level Network Access**: Builds and injects Ethernet, IP, and ICMP frames directly with `libpcap`. +- **Real-Time Output**: Displays responsive hosts immediately during scanning. +- **Error-Resilient**: Gracefully handles interface, packet, and permission-related failures. +- **Extensible Architecture** - Clean, modular design for future protocol and feature expansion. ## Installation -### Prerequisites +### Requirements To run **hostRecon**, you will need the following: -- **[libpcap](https://www.tcpdump.org/)**: - - A packet capture library required for capturing and injecting packets. - - Install via your package manager: - - For Ubuntu/Debian: - - ```bash - sudo apt-get install libpcap-dev - ``` - - For Fedora: - - ```bash - sudo dnf install libpcap-devel - ``` - - For macOS: +- **[libpcap](https://www.tcpdump.org/)** - For packet capture/injection: + - Ubuntu/Debian: ```bash sudo apt install libpcap-dev``` + - Fedora: ```bash sudo dnf install libpcap-devel``` + - macOS: ```bash brew install libpcap``` - ```bash - brew install libpcap - ``` +- **C++17 or newer** compiler (`g++`, `clang++`, etc.): -- **C++ Compiler**: - - A C++ compiler such as `g++`, `clang++`, or any standard C++ compiler. +- **CMake** (optional, but recommended). -- **CMake** (Optional for build automation): - - While optional, **CMake** is recommended for automating the build process, especially for larger projects. - - Installation (for Linux): - - ```bash - sudo apt-get install cmake - ``` - - For macOS: - - ```bash - brew install cmake - ``` - -### Steps +### Build 1. Clone this repository: ```bash - git clone https://github.com/kmccol1/hostRecon.git + git clone https://github.com/mcckyle/hostRecon.git cd hostRecon ``` @@ -109,28 +80,27 @@ To run **hostRecon**, you will need the following: ## Usage -Once compiled, **hostRecon** can be run from the command line. The tool will automatically detect and scan the local subnet for active hosts, displaying the list of hosts that respond to the ICMP Echo Request. - -### Example: +Simply execute ```bash sudo ./networkScanner ``` -The tool will output the list of active hosts in your local network. +**hostRecon** automatically identifies your active network interfaces and scans the +local subnet for reachable hosts. Each responsive device is printed in real-time, showing its IP and MAC address. ## Current State As of now, **hostRecon** provides the following functionality: -- Establishes a capture session to listen for ICMP Echo Replies. -- Constructs and sends ICMP Echo Request packets to specified IP addresses in a /24 subnet. -- Captures responses and accurately identifies active hosts based on received packets. -- Displays the results in a clear and concise format. +- Direct ARP-based host discovery on local networks. +- ICMP echo (ping) scanning for active device verification. +- Real-time result display with informative status output. +- Reliable interface initalization and error handling. -### Current Limitations: -- **Single-threaded Operation**: Scans hosts sequentially, which may limit speed in larger networks. -- **Limited Protocol Support**: Currently supports only ICMP-based host discovery. +### Known Limitations: +- **Single-threaded Scanning**: Scans hosts sequentially, which may limit speed in larger networks. +- **Routed Scanning**: Focused on local subnet (no routed scanning, yet). ## Future State diff --git a/src/hostReconLib.cpp b/src/hostReconLib.cpp index 1e11a66..33bd7de 100644 --- a/src/hostReconLib.cpp +++ b/src/hostReconLib.cpp @@ -1,24 +1,24 @@ //**************************************************************************************** // // Filename: hostReconLib.cpp -// Author: Kyle McColgan -// Date: 18 November 2024 +// Author: Kyle D. McColgan (Saint Louis, MO) +// Date: 6 November 2025 // Description: CLI based networking utility for local network host enumeration. // //**************************************************************************************** #include "hostReconLib.h" + #include #include #include -#include #include -#include //For std::reverse() +#include #include #include -#include //For struct definitions -#include +#include +#include using namespace std; //**************************************************************************************** @@ -42,13 +42,19 @@ using namespace std; * from the `source` parameter, ensuring null termination. */ -void copyAddr(char (*hostList)[16], const char * source, int index) +void copyAddr(char (*hostList)[16], const char * source, int index) noexcept { - int adrLen = strlen(source); - cout << "Copying " << adrLen << " chars to list at index: " << index << endl; - strncpy(hostList[index], source, adrLen); - hostList[index][adrLen] = '\0'; - cout << "\nhostList updated." << endl; + if ( ( ! source) || (index < 0) || (index >= MAX_HOSTS) ) + { + return; + } + + //cout << "Copying " << adrLen << " chars to list at index: " << index << endl; + + strncpy(hostList[index], source, 15); + hostList[index][15] = '\0'; + + cout << "\n[INFO] Host list updated.\n"; } //**************************************************************************************** @@ -72,25 +78,15 @@ void copyAddr(char (*hostList)[16], const char * source, int index) * of the input integer `num`. */ -void intToCharArray(int num, char * buffer) +void intToCharArray(int num, char * buffer) noexcept { - int i = 0; - if (num ==0) + //Write decimal string for 0..255. + if ( ! buffer) { - buffer[i++] = '0'; + return; } - else - { - while(num > 0) - { - buffer[i++] = '0' + (num % 10); - num /= 10; - } - } - buffer[i] = '\0'; - //Reverse the buffer... - reverse(buffer, buffer + i ); + snprintf(buffer, 4, "%d", num); } //**************************************************************************************** @@ -113,30 +109,30 @@ void intToCharArray(int num, char * buffer) * @post The function returns the computed checksum based on the provided data buffer. */ -unsigned short computeChecksum(void * data, int length) +unsigned short computeChecksum(void * data, int length) noexcept { - unsigned short * buffer = (unsigned short *)data; - unsigned int sum = 0; + auto * buffer = static_cast(data); + unsigned long sum = 0; unsigned short result; - for(sum = 0; length > 1; length -= 2) + while (length > 1) { sum += *buffer++; + length -= 2; } if (length == 1) { - sum += *(unsigned char *)buffer; + sum += *reinterpret_cast(buffer); } sum = (sum >> 16) + (sum & 0xFFFF); sum += (sum >> 16); - result = ~sum; + result = static_cast(~sum); return result; } - //**************************************************************************************** /** @@ -150,14 +146,14 @@ unsigned short computeChecksum(void * data, int length) * @param user A user-defined data pointer passed by `pcap_dispatch`. In this * implementation, it is a pointer to a `context` object that contains * relevant state information for the network scan. - * @param pkthdr A pointer to the packet header structure (`pcap_pkthdr`) + * @param header A pointer to the packet header structure (`pcap_pkthdr`) * containing metadata about the captured packet, such as its * length, captured length, and timestamp. - * @param capPacket A pointer to the raw packet data captured by `pcap_dispatch`. + * @param packet A pointer to the raw packet data captured by `pcap_dispatch`. * This includes the packet's payload and headers (e.g., Ethernet, IP, etc.). * * @pre The `user`, `pkthdr`, and `capPacket` pointers must all be valid. The `user` - * parameter must point to a properly initialized `context` object. The `capPacket` + * parameter must point to a properly initialized `context` object. The `packet` * buffer must contain at least `pkthdr->caplen` bytes. * @post The captured packet is processed as defined by the implementation. Any * resulting data or state changes should be managed through the `context` object @@ -167,57 +163,258 @@ unsigned short computeChecksum(void * data, int length) * specified packet limit is reached or an error occurs. */ -static void callBack(u_char * user, const struct pcap_pkthdr * pkthdr, const u_char * capPacket) +void callBack(u_char * user, const pcap_pkthdr * header, const u_char * packet) { - auto context = reinterpret_cast(user); - - const int ethHeaderLen = 14; + if ( ( ! user) || ( ! packet) ) + { + return; + } + auto * context = reinterpret_cast(user); context->result = false; - struct ip * ipHeader = (struct ip *)(capPacket + ethHeaderLen); //Skip Ethernet header... + constexpr int ETH_HDR_LEN = 14; + const auto * ipHeader = reinterpret_cast(packet + ETH_HDR_LEN); //Skip Ethernet header... - if(ipHeader->ip_p == IPPROTO_ICMP) + if (ipHeader->ip_p != IPPROTO_ICMP) { - char sourceStr[INET_ADDRSTRLEN]; - char destStr[INET_ADDRSTRLEN]; - char target[INET_ADDRSTRLEN]; + return; + } - inet_ntop(AF_INET, &(ipHeader->ip_src.s_addr), sourceStr, INET_ADDRSTRLEN); - inet_ntop(AF_INET, &(ipHeader->ip_dst.s_addr), destStr, INET_ADDRSTRLEN); - inet_ntop(AF_INET, &(context->destination), target, INET_ADDRSTRLEN); + char sourceStr[INET_ADDRSTRLEN]{}; + char destStr[INET_ADDRSTRLEN]{}; + char targetStr[INET_ADDRSTRLEN]{}; - //Uncomment line below for debugging purposes: - //cout << "Captured packet from: " << sourceStr << " to " << destStr << endl; + inet_ntop(AF_INET, &ipHeader->ip_src, sourceStr, sizeof(sourceStr)); + inet_ntop(AF_INET, &ipHeader->ip_dst, destStr, sizeof(destStr)); + inet_ntop(AF_INET, &context->destination, targetStr, sizeof(targetStr)); - if (strcmp(sourceStr, target) == 0) - { - int ipHeaderLen = ipHeader->ip_hl * 4; + //Uncomment line below for debugging purposes: + //cout << "Captured packet from: " << sourceStr << " to " << destStr << endl; - struct icmphdr * icmpHeader = (struct icmphdr *)(capPacket + ethHeaderLen + ipHeaderLen); + if (strcmp(sourceStr, targetStr) != 0) + { + return; + } - if ( ( icmpHeader->type ) == ( ICMP_ECHOREPLY) ) - { - cout << "Received ICMP ECHO Reply packet from " << sourceStr << endl; - context->result = true; - pcap_breakloop(context->captureSession); - } - else - { - cout << "ICMP type " << static_cast(icmpHeader->type) << " received, ignoring..." << endl; - } + int ipHeaderLen = ipHeader->ip_hl * 4; + const auto * icmpHeader = reinterpret_cast(packet + ETH_HDR_LEN + ipHeaderLen); + + if ( (icmpHeader->type) == (ICMP_ECHOREPLY) ) + { + cout << "Reply from " << sourceStr << '\n'; + context->result = true; + pcap_breakloop(context->captureSession); + } +} + +//**************************************************************************************** + +#pragma pack(push, 1) +struct arp_hdr +{ + uint16_t htype; //Hardware type (1 = Ethernet). + uint16_t ptype; //Protocol type (0x0800 = IPv4). + uint8_t hlen; //Hardware size (6). + uint8_t plen; //Protocol size (4). + uint16_t oper; //Opcode (1 = request, 2 = reply). + uint8_t sha[6]; //Sender hardware address. + uint8_t spa[4]; //Sender protocol address. + uint8_t tha[6]; //Target hardware address. + uint8_t tpa[4]; //Target protocol address. +}; +#pragma pack(pop) + +struct ArpResolveContext +{ + in_addr_t targetAddr; //Network-order 32-bit. + uint8_t * out_mac; //Pointer to 6-byte output buffer. + bool found; + pcap_t * capture; +}; + +/** + * @brief Callback function invoked by libpcap for each captured packet. + * + * This function inspects incoming packets, filters for ARP replies, + * and checks whether the sender protocol address (SPA) matches the + * current target host being resolved. If a match is found, it copies + * the sender hardware address (SHA) into the CaptureContext result field + * and marks the resolution as complete. + * + * @param user A pointer to a CaptureContext structure containing + * session data and the expected target address. + * @param header The pcap_pkthdr structure containing packet metadata. + * @param packet The raw packet data captured by libpcap. + * + * @pre The CaptureContext pointer (user) must be non-null and initalized. + * @pre The pcap capture session must have an active filter for ARP or ICMP. + * + * @post If an ARP reply matches the target IP, the target's MAC address + * is stored in context->result, and context->resolved is set to true. + * + * @note This function ignores all non-ARP packets and mismatched ARP replies. + * @note Uses memcpy() to safely extract the sender protocol address (SPA). + */ + +void arpCallBack(u_char * user, const pcap_pkthdr * header, const u_char * packet) +{ + if ( ( ! user) || ( ! packet) ) + { + return; + } + + auto * context = reinterpret_cast(user); + + //Expect: Ethernet header (14) + ARP header. + const int ETH_HDR_LEN = 14; + const struct ethhdr * eth = reinterpret_cast(packet); + + //Check ethertype == ARP. + if (ntohs(eth->h_proto) != ETH_P_ARP) + { + return; + } + + const arp_hdr * arp = reinterpret_cast(packet + ETH_HDR_LEN); + + //Check it's an ARP reply. + if (ntohs(arp->oper) != 2) + { + return; + } + + //Compare sender protocol address (tpa) to our requested target. + uint32_t spa; + memcpy(&spa, arp->spa, 4); + + if (spa != context->targetAddr) + { + return; + } + + //Found it: copy sender hardware address (sha) to out_mac. + memcpy(context->out_mac, arp->sha, 6); + context->found = true; + + //Break out of pcap_loop(). + if (context->capture) + { + pcap_breakloop(context->capture); + } +} + +/** + * @brief Resolves the MAC address of a given IPv4 host using ARP requests. + * + * This function sends an ARP request targeting the specified IPv4 address + * and waits for a reply within a given timeout. It captures incoming ARP + * responses via pcap and uses arpCallBack() to process them. On success, + * the target host's MAC address is written into the provided buffer. + * + * @param targetIP The IPv4 address (in network byte order) to resolve. + * @param context A reference to a CaptureContext structure containing both + * the send and capture sessions and the target IP address. + * @param out_mac Output buffer (6 bytes) to receive the resolved MAC address. + * @param timeout_ms + * + * @return true if the MAC address was successfully resolved; false otherwise. + * + * @pre context.captureSession and context.sendSession must both be valid + * pcap_t * handles opened on the same network interface. + * @pre targetIP must be within the same subnet as the capture interface. + * + * @post On success, targetMAC contains the hardware address corresponding + * to targetIP, and context.result is updated with the same value. + * + * @note This function performs direct ARP resolution and does not reply on + * the system ARP cache. It uses pcap_inject() for sending raw packets. + * @note Timeouts or missing responses will result in a false return value. + */ + +bool resolveMAC(const char * targetIP, CaptureContext & context, uint8_t out_mac[6], int timeout_ms) +{ + if ( (!targetIP) || (!context.sendSession) || (!context.captureSession) || (!out_mac) ) + { + return false; + } + + // LOCAL settings - plan to replace with auto-detect later... + const uint8_t LOCAL_MAC[6] = {0x00, 0xD8, 0x61, 0xAB, 0x11, 0x03}; + const char LOCAL_IP_STR[] = "192.168.1.110"; // Must match the capture local IP. + + // Prepare addresses. + in_addr target_in; + if (inet_pton(AF_INET, targetIP, &target_in) != 1) + { + return false; + } + + in_addr local_in; + if (inet_pton(AF_INET, LOCAL_IP_STR, &local_in) != 1) + { + return false; + } + + // Build Ethernet (broadcast) + ARP request. + const int ETH_LEN = sizeof(struct ethhdr); + const int ARP_LEN = sizeof(arp_hdr); + const int PKT_LEN = ETH_LEN + ARP_LEN; + unsigned char packet [PKT_LEN]; + memset(packet, 0, PKT_LEN); + + // Ethernet header: dest = broadcast, src = local, protocol = ARP. + struct ethhdr eth; + memset(ð, 0, sizeof(eth)); + memset(eth.h_dest, 0xff, ETH_ALEN); + memcpy(eth.h_source, LOCAL_MAC, 6); + eth.h_proto = htons(ETH_P_ARP); + memcpy(packet, ð, sizeof(eth)); + + // ARP header: request. + arp_hdr arp; + arp.htype = htons(1); //Ethernet. + arp.ptype = htons(ETH_P_IP); //IPv4. + arp.hlen = 6; + arp.plen = 4; + arp.oper = htons(1); //ARP request. + memcpy(arp.sha, LOCAL_MAC, 6); //Our MAC. + memcpy(arp.spa, &local_in.s_addr, 4); //Our IP address (in network order). + memset(arp.tha, 0x00, 6); //Target MAC = currently unknown. + memcpy(arp.tpa, &target_in.s_addr, 4); //Target IP address. + + memcpy(packet + ETH_LEN, &arp, sizeof(arp)); + + // Setup the context for callbacks. + ArpResolveContext arc{}; + arc.targetAddr = target_in.s_addr; //In network byte order. + arc.out_mac = out_mac; + arc.found = false; + arc.capture = context.captureSession; + + // Send the ARP request (try a couple of times). + const int numTries = 2; + for (int attempt = 0; (attempt < numTries) && ( ! arc.found); ++ attempt) + { + //pcap_dispatch(context.captureSession, 0, nullptr, nullptr); + if (pcap_inject(context.sendSession, packet, PKT_LEN) == -1) + { + cerr << "[ERROR] resolveMAC: pcap_inject(): " << pcap_geterr(context.sendSession) << endl; + return false; } - else + + // Wait for a reply using pcap_dispatch. It will return when arpCallBack calls pcap_breakloop. + // Use a small number of packets; pcap_dispatch respects the capture session timeout set above. + int rc = pcap_dispatch(context.captureSession, -1, arpCallBack, reinterpret_cast(&arc)); + if (rc == -1) { - //cout << "Response from a different host. Skipping..." << endl; - //cout << "."; - cout << endl; + // error; continue to next attempt. } + + //If arc.found is set, we have found the MAC. } - else - { - cout << "Not an ICMP packet. Skipping..." << endl; - } + + return arc.found; } //**************************************************************************************** @@ -247,90 +444,120 @@ static void callBack(u_char * user, const struct pcap_pkthdr * pkthdr, const u_c * or multiple destinations. In the calling function, the `context.result` * should be checked to determine if the destination is active. * + * @note ***Disclaimer***: Using broadcast MAC for local network ping sweeping. Some devices may + * ignore broadcast ICMP, but this works reliably on flat LANs. In the future, + * we may consider implementing real per-host ARP resolution to support any network. + * * @see `getHosts()` for an example of how this function is used in a scanning loop. */ bool pingSweep(char (&destination)[16], CaptureContext & context) { bool result = false; - struct ip ipHdr; - struct icmphdr msgHdr; - unsigned char myPacket[sizeof(struct ethhdr) + sizeof(struct ip) + sizeof(struct icmphdr)]; - // Initialize the destination IP in the context struct + if ( (!context.sendSession) || (!context.captureSession) ) + { + return false; + } + + // Initialize the destination IP in the context struct. inet_pton(AF_INET, destination, &context.destination); - // Fill in the Ethernet header + // Resolve the destination MAC address with ARP. + uint8_t targetMAC[6]{}; + cout << "[INFO] Resolving MAC for: " << destination << "...\n"; + + if ( ! resolveMAC(destination, context, targetMAC, 1000)) + { + cout << "[WARN] Failed to resolve the MAC address for: " << destination + << ". Skipping host.\n"; + + return false; + } + + cout << "[OK] MAC address resolved for: " << destination << ": "; + for (int i = 0; i < 6; ++ i) + { + printf("%02X%s", targetMAC[i], (i < 5 ? ":" : "")); + } + cout << "\n"; + + //Build the ethernet + IP + ICMP without payload. + unsigned char myPacket[sizeof(struct ethhdr) + sizeof(struct ip) + sizeof(struct icmphdr)]; memset(myPacket, 0, sizeof(myPacket)); + + // Fill in the Ethernet header. struct ethhdr ethHdr; - memset(ðHdr, 0, sizeof(struct ethhdr)); - memset(ðHdr.h_dest, 0xff, ETH_ALEN); // Broadcast MAC address - ethHdr.h_source[0] = 0x00; // Set your own MAC address + memset(ðHdr, 0, sizeof(ethHdr)); + memcpy(ethHdr.h_dest, targetMAC, 6); // Use the resolved MAC address here. + ethHdr.h_source[0] = 0x00; // Set your own MAC address. ethHdr.h_source[1] = 0xD8; ethHdr.h_source[2] = 0x61; ethHdr.h_source[3] = 0xAB; ethHdr.h_source[4] = 0x11; ethHdr.h_source[5] = 0x03; - ethHdr.h_proto = htons(ETH_P_IP); // Protocol type for IP - - // Fill in the IP header - memset(&ipHdr, 0, sizeof(struct ip)); - ipHdr.ip_hl = 5; // Header length - ipHdr.ip_v = 4; // IP version - ipHdr.ip_tos = 0; // Type of service - ipHdr.ip_len = htons(sizeof(struct ip) + sizeof(struct icmphdr)); // Total length - ipHdr.ip_id = htons(54321); // Identification - ipHdr.ip_off = 0; // Fragment Offset - ipHdr.ip_ttl = 64; // Time to live - ipHdr.ip_p = IPPROTO_ICMP; // Protocol (ICMP) + ethHdr.h_proto = htons(ETH_P_IP); // Protocol type for IP. + + // Fill in the IP header. + struct ip ipHdr; + memset(&ipHdr, 0, sizeof(ipHdr)); + ipHdr.ip_hl = 5; // Header length. + ipHdr.ip_v = 4; // IP version. + ipHdr.ip_tos = 0; // Type of service. + ipHdr.ip_len = htons(sizeof(struct ip) + sizeof(struct icmphdr)); // Total length. + ipHdr.ip_id = htons(54321); // Identification. + ipHdr.ip_off = 0; // Fragment Offset. + ipHdr.ip_ttl = 64; // Time to live. + ipHdr.ip_p = IPPROTO_ICMP; // Protocol (ICMP). ipHdr.ip_sum = 0; // Checksum - inet_pton(AF_INET, "192.168.1.110", &ipHdr.ip_src); // Source IP - inet_pton(AF_INET, destination, &ipHdr.ip_dst); // Destination IP - ipHdr.ip_sum = computeChecksum((unsigned short *)&ipHdr, sizeof(struct ip)); // Calculate checksum - - // Fill in the ICMP header - memset(&msgHdr, 0, sizeof(struct icmphdr)); - msgHdr.type = ICMP_ECHO; // ICMP Echo request type - msgHdr.code = 0; // Code - msgHdr.checksum = 0; // Checksum - msgHdr.un.echo.id = htons(1234); // Identifier - msgHdr.un.echo.sequence = htons(1); // Sequence number - msgHdr.checksum = computeChecksum((unsigned short *)&msgHdr, sizeof(struct icmphdr)); // Calculate checksum - - // Construct the packet by combining the headers - memcpy(myPacket, ðHdr, sizeof(struct ethhdr)); - memcpy(myPacket + sizeof(struct ethhdr), &ipHdr, sizeof(struct ip)); - memcpy(myPacket + sizeof(struct ethhdr) + sizeof(struct ip), &msgHdr, sizeof(struct icmphdr)); - - // Send the packet using pcap_inject - cout << "\n***Pinging " << destination << "..." << endl; - if (pcap_inject(context.sendSession, myPacket, sizeof(myPacket)) == -1) { - cout << "Error sending the packet: " << pcap_geterr(context.sendSession) << endl; + inet_pton(AF_INET, "192.168.1.110", &ipHdr.ip_src); // Choose the correct local IP. + inet_pton(AF_INET, destination, &ipHdr.ip_dst); // Destination IP. + ipHdr.ip_sum = computeChecksum(&ipHdr, sizeof(ipHdr)); // Calculate checksum. + + // Fill in the ICMP header. + struct icmphdr icmpHdr; + memset(&icmpHdr, 0, sizeof(struct icmphdr)); + icmpHdr.type = ICMP_ECHO; // ICMP Echo request type. + icmpHdr.code = 0; // Code. + icmpHdr.un.echo.id = htons(1234); // Identifier. + icmpHdr.un.echo.sequence = htons(1); // Sequence number. + icmpHdr.checksum = 0; // Checksum. + icmpHdr.checksum = computeChecksum(&icmpHdr, sizeof(icmpHdr)); // Calculate checksum. + + // Construct the full packet by combining the headers. + memcpy(myPacket, ðHdr, sizeof(ethHdr)); + memcpy(myPacket + sizeof(ethHdr), &ipHdr, sizeof(ipHdr)); + memcpy(myPacket + sizeof(ethHdr) + sizeof(ipHdr), &icmpHdr, sizeof(icmpHdr)); + + // Send the packet using pcap_inject. + cout << "***Pinging " << destination << "..." << endl; + if (pcap_inject(context.sendSession, myPacket, sizeof(myPacket)) == -1) + { + cout << "[ERROR] pcap_inject(): " << pcap_geterr(context.sendSession) << endl; return false; } - //cout << "\n***Searching for a response..." << endl; - - // Use pcap_dispatch for a specified number of packets - if (pcap_dispatch(context.captureSession, 10, callBack, reinterpret_cast(&context)) == -1) { - cout << "Error in pcap_dispatch(): " << pcap_geterr(context.captureSession) << endl; + // capture: dispatch a small number of packets. context.result will be set in the callBack. + int captured = pcap_dispatch(context.captureSession, 50, callBack, reinterpret_cast(&context)); + if (captured == -1) + { + cout << "[ERROR] pcap_dispatch(): " << pcap_geterr(context.captureSession) << endl; return false; } - // Check the result after capturing packets + // Check the result after capturing packets. if (context.result) { - cout << "Response received from " << destination << endl; - cout << "Host " << destination << " is active!" << endl; + cout << "Destination " << destination << " is active!" << endl; result = true; } else { - cout << "No response." << endl; cout << "Host " << destination << " is inactive." << endl; } return result; } + //**************************************************************************************** /** @@ -366,48 +593,32 @@ bool pingSweep(char (&destination)[16], CaptureContext & context) void getHosts(char (*hostList)[16], int &numHosts, CaptureContext & context) { const char base[] = "192.168.1."; - char destIP[16]; + char destIP[16]{}; int hostCount = 0; - for (int i = 1; i < 255; i++) + for (int i = 90; i <= 254; ++ i) { - strcpy(destIP, base); - char suffix[4]; - intToCharArray(i, suffix); - strcat(destIP, suffix); - - //cout << "***Pinging " << destIP << "...\n"; + intToCharArray(i, destIP); + //Build the full IP. + char fullIP[16]; + snprintf(fullIP, sizeof(fullIP), "%s%s", base, destIP); context.result = false; - // Start timing for response - auto startTime = std::chrono::steady_clock::now(); - while (std::chrono::steady_clock::now() - startTime < std::chrono::milliseconds(2000)) + if (pingSweep(fullIP, context)) { - // Call pingSweep with the correct context - pingSweep(destIP, context); - - // Check if we got a response - if (context.result) + if (hostCount < MAX_HOSTS) + { + copyAddr(hostList, fullIP, hostCount); + ++ hostCount; + } + else { - //cout << "Host " << destIP << " is active.\n"; - if (hostCount < MAX_HOSTS) - { - copyAddr(hostList, destIP, hostCount); - hostCount++; - } - else - { - cout << "Error: hostList is full, unable to add more hosts." << endl; - break; - } break; } } - - // if (!context.result) { - // cout << "Inactive host detected. Skipping...\n"; - // } + //Pause to avoid flooding... + this_thread::sleep_for(chrono::milliseconds(50)); } numHosts = hostCount; @@ -438,18 +649,18 @@ void getHosts(char (*hostList)[16], int &numHosts, CaptureContext & context) * are excluded from the output. */ -void filterSpecialChars(const char * address, char * filtered) +void filterSpecialChars(const char * address, char * filtered) noexcept { int index = 0; - for ( int i = 0; address[i] != '\0'; i ++) + for (int i = 0; address[i] != '\0'; ++ i) { - if(isdigit(address[i]) || address[i] == '.') + if ( (isdigit(static_cast(address[i])) || (address[i] == '.') )) { - filtered[index] = address[i]; - index ++; + filtered[index ++] = address[i]; } } + filtered[index] = '\0'; } @@ -473,62 +684,17 @@ void filterSpecialChars(const char * address, char * filtered) * handle edge cases like addresses with leading zeros or malformed segments. */ -bool isValidIPAddress(const char* address) +bool isValidIPAddress(const char* address) noexcept { - int numDots = 0; - int numDigits = 0; - int currentOctetValue = 0; - bool result = true; - - if (address == nullptr) + if ( !address ) { return false; } - while (*address) - { - if (*address == '.') - { - numDots++; - - // After the last digit of an octet, check the octet value - if ( ( numDigits == 0 ) || ( numDigits > 3 ) || ( currentOctetValue > 255) ) - { - result = false; - } - - // Reset for next octet - currentOctetValue = 0; - numDigits = 0; - } - else if ( ( *(address) >= '0') && ( *(address) <= '9') ) - { - currentOctetValue = currentOctetValue * 10 + (*address - '0'); - numDigits++; - } - else - { - result = false; - break; - } - - address++; - } - - // Final validation for the last octet - if ( (numDigits == 0) || (numDigits > 3) || (currentOctetValue > 255) ) - { - result = false; - } - - // IP should have exactly 3 dots - if (numDots != 3) - { - result = false; - } - - return result; + sockaddr_in sa{}; + return inet_pton(AF_INET, address, &(sa.sin_addr)) == 1; } + //**************************************************************************************** /** @@ -554,19 +720,22 @@ bool isValidIPAddress(const char* address) * It assumes that all IP addresses in `hostList` are valid and formatted correctly. */ -bool inList(const char* address, char (*hostList)[16], int listSize) +bool inList(const char * address, char (*hostList)[16], int listSize) noexcept { - bool result = false; + if ( ! address) + { + return false; + } - for (int i = 0; i < listSize; i ++) + for (int i = 0; i < listSize; ++ i) { - if(strcmp(address, hostList[i]) == 0) + if (strcmp(address, hostList[i]) == 0) { - result = true; //Return true if the host is already in the list... + return true; //Return true if the host is already in the list... } } - return result; + return false; } //**************************************************************************************** @@ -589,19 +758,16 @@ bool inList(const char* address, char (*hostList)[16], int listSize) * @note This function assumes that each entry in the `hostList` is a valid, null-terminated IP address. */ -void displayHostList(char (*hostList)[16], int numHosts) +void displayHostList(char (*hostList)[16], int numHosts) noexcept { - //cout << "\n\nPrinting list with " << numHosts << " hosts included." << endl; - //cout << "*****************************************" << endl; - cout << "Active Hosts List: " << endl; + cout << "\nActive Hosts (" << numHosts << "):\n"; - for (int i = 0; i < numHosts; i ++) + for (int i = 0; i < numHosts; ++ i) { - cout << i + 1 << ". " << hostList[i] << endl; + cout << i + 1 << ". " << hostList[i] << '\n'; } - //cout << "*****************************************" << endl; - cout << "----------------------------------" << endl; - //cout << "\nDone." << endl; + + cout << "---------------------------------\n"; } //**************************************************************************************** @@ -622,16 +788,16 @@ void displayHostList(char (*hostList)[16], int numHosts) void openNetworkInterface() { - pcap_t * session; - char errors [PCAP_ERRBUF_SIZE]; - cout << "\n***Opening session..." << endl; + pcap_t * session = pcap_open_live("enp34s0", BUFSIZ, 1, 1000, errors); - session = pcap_open_live("enp34s0", BUFSIZ, 1, 1000, errors); - - if(session == NULL) + if ( ! session) + { + cerr << "[ERROR] openNetworkInterface(): " << errors << endl; + } + else { - cout << "Error opening the NIC: " << errors << endl; + pcap_close(session); } } @@ -658,25 +824,17 @@ void openNetworkInterface() void extractDeviceInfo(const u_char * packet, char (&source)[16], char(&destination)[16]) { //Extract device information from the packet...(IPs, MACs) + if ( ! packet) + { + source[0] = destination[0] = '\0'; + return; + } //Assuming IPv4 packet structure... - // struct ip *ip_header = (struct ip*) (packet + SIZE_ETHERNET); //Assuming Ethernet frame... - struct ip *ip_header = (struct ip*) (packet + 14); //Assuming Ethernet frame... - - //Extract source and destination IP addresses... - char sourceIP[INET_ADDRSTRLEN]; - char destinationIP[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &(ip_header->ip_src), sourceIP, INET_ADDRSTRLEN); - inet_ntop(AF_INET, &(ip_header->ip_dst), destinationIP, INET_ADDRSTRLEN); - - // //Print the extracted IP addresses... - // cout << "Source IP: " << sourceIP << endl; - // cout << "Destination IP: " << destinationIP << endl; - - //cout << "Copying..." << endl; - //cout << "Before source: " << sourceIP << endl; - strncpy(source, sourceIP, sizeof(source)); - strncpy(destination, destinationIP, sizeof(destination)); + const auto * ipHeader = reinterpret_cast(packet + 14); //Assuming Ethernet frame... + + inet_ntop(AF_INET, &ipHeader->ip_src, source, INET_ADDRSTRLEN); + inet_ntop(AF_INET, &ipHeader->ip_dst, destination, INET_ADDRSTRLEN); } //**************************************************************************************** diff --git a/src/hostReconLib.h b/src/hostReconLib.h index 3f5d65f..e352ef5 100644 --- a/src/hostReconLib.h +++ b/src/hostReconLib.h @@ -1,9 +1,9 @@ //**************************************************************************************** // // Filename: hostReconLib.h -// Author: Kyle McColgan -// Date: 7 October 2024 -// Description: CLI based networking utility for local network host enumeration. +// Author: Kyle D. McColgan (Saint Louis, MO) +// Date: 6 November 2025 +// Description: This file contains the function declarations for the network scanner. // //**************************************************************************************** @@ -11,38 +11,49 @@ #define HOST_RECON_LIB_H #include +#include //**************************************************************************************** -const int MAX_HOSTS = 254; //Covers a typical /24 subnet, with 254 usable hosts. +constexpr int MAX_HOSTS = 254; //**************************************************************************************** struct CaptureContext { - pcap_t * captureSession; - bool & result; - struct in_addr destination; - pcap_t * sendSession; + pcap_t * captureSession = nullptr; + pcap_t * sendSession = nullptr; + in_addr destination{}; + bool result = false; + + CaptureContext() = default; + CaptureContext(pcap_t * cap, pcap_t * send) + : captureSession(cap), sendSession(send) {} }; //**************************************************************************************** -// Function declarations -bool isValidIPAddress(const char* ip); -unsigned short computeChecksum(void * data, int length); -void copyAddr(char (*hostList)[16], const char * source, int index); -void intToCharArray(int num, char * buffer); -static void callBack(u_char * user, const struct pcap_pkthdr * pkthdr, const u_char * capPacket); +// Utility and validation. +bool isValidIPAddress(const char * ip) noexcept; +unsigned short computeChecksum(void * data, int length) noexcept; +void intToCharArray(int num, char * buffer) noexcept; +void filterSpecialChars(const char * address, char * filtered) noexcept; + +// Host list management. +void copyAddr(char (*hostList)[16], const char * source, int index) noexcept; +bool inList(const char* address, char (*hostList)[16], int listSize) noexcept; +void displayHostList(char (*hostList)[16], int numHosts) noexcept; + +// Core scanning logic. bool pingSweep(char (&destination)[16], CaptureContext & context); -void getHosts(char (*hostList)[16], int &numHosts, CaptureContext & context); -void filterSpecialChars(const char * address, char * filtered); -bool inList(const char* address, char (*hostList)[16], int listSize); -void displayHostList(char (*hostList)[16], int numHosts); +void getHosts(char (*hostList)[16], int & numHosts, CaptureContext & context); +void callBack(u_char * user, const pcap_pkthdr * header, const u_char * packet); + +// Miscellaneous utilities. +bool resolveMAC(const char * targetIP, CaptureContext & context, uint8_t out_mac[6], int timeout_ms = 2000); void openNetworkInterface(); void extractDeviceInfo(const u_char * packet, char (&source)[16], char(&destination)[16]); //**************************************************************************************** - #endif // HOST_RECON_LIB_H diff --git a/src/networkScanner.cpp b/src/networkScanner.cpp index d752c0a..eee34be 100644 --- a/src/networkScanner.cpp +++ b/src/networkScanner.cpp @@ -1,146 +1,157 @@ //**************************************************************************************** // // Filename: networkScanner.cpp -// Author: Kyle McColgan -// Date: 14 October 2024 -// Description: CLI based networking utility for local network host enumeration. +// Author: Kyle D. McColgan (Saint Louis, MO) +// Date: 6 November 2025 +// Description: A simple C++17 libpcap-based host scanner. // //**************************************************************************************** -#include #include "hostReconLib.h" +#include + using namespace std; //**************************************************************************************** int main() { - bool result = false; - char hostList [MAX_HOSTS][16]; //Assuming each IP addr is stored in a 16-character array. + constexpr int TIMEOUT_MS = 2000; //Timeout value in milliseconds. + char hostList [MAX_HOSTS][16]{}; //Assuming each IP stored in a 16-character array. int numHosts = 0; - char sendErrorMsg [PCAP_ERRBUF_SIZE]; - char capErrorMsg [PCAP_ERRBUF_SIZE]; - pcap_t * captureSession; - pcap_t * sendSession; - int timeout = 2000; //Timeout value in milliseconds. - - std::cout << " _ _ ____ \n"; - std::cout << "| |__ ___ ___| |_| _ \\ ___ ___ ____ _____ \n"; - std::cout << "| '_ \\/ _ \\/ __| __| |_) / _ \\/ __/ _ \\| '_ \\ \n"; - std::cout << "| | | | (_) \\__ \\ |_| _ < __/ (_| (_) | | | | \n"; - std::cout << "|_| |_|\\___/|___/\\__|_| \\_\\___|\\____/\\_|_| |_| \n"; - cout << "----------------------------------\n"; - - //Open session handlers with a timeout of 1000 ms... - cout << "Initializing network interface sessions...\n"; - sendSession = pcap_open_live("enp34s0", BUFSIZ, 0, timeout, sendErrorMsg); - captureSession = pcap_open_live("enp34s0", BUFSIZ, 0, timeout, capErrorMsg); - - if(sendSession == nullptr) - { - cerr << "[ERROR] Could not access network interface for injection: " << sendErrorMsg << endl; - return 1; - } - else if (captureSession == nullptr) + + char sendError [PCAP_ERRBUF_SIZE]{}; + char capError [PCAP_ERRBUF_SIZE]{}; + const char * iface = "enp34s0"; + + pcap_t * captureSession = nullptr; + pcap_t * sendSession = nullptr; + + cout << "hostRecon - libpcap-based network scanner\n" + << "-----------------------------------------\n"; + + //Initalize the network session handlers... + cout << "Initializing the network interfaces...\n"; + sendSession = pcap_open_live(iface, BUFSIZ, 1, TIMEOUT_MS, sendError); + captureSession = pcap_open_live(iface, BUFSIZ, 1, TIMEOUT_MS, capError); + + if ( ( ! sendSession) || ( ! captureSession) ) { - cerr << "[ERROR] Could not access the network interface for capture: " << capErrorMsg << endl; + cerr << "[ERROR] Failed to initalize pcap sessions.\n"; + + if (sendSession) + { + pcap_close(sendSession); + } + return 1; } - cout << "[OK] Network interfaces initialized successfully!\n"; + pcap_setdirection(captureSession, PCAP_D_INOUT); + cout << "[OK] interfaces initialized successfully!\n"; + + //Set the packet filter... + bpf_program filter; + const char filterExp[] = "arp or icmp"; + cout << "Applying filters...\n"; + //bpf_u_int32 net = 0, mask = 0; + + // if (pcap_lookupnet(iface, &net, &mask, capError) == -1) + // { + // //Not fatal error...continue with net = 0. + // net = 0; + // } //Set the filter for ICMP packets - struct bpf_program filter; - bpf_u_int32 net; - char filterExp[] = "icmp"; // char filterExp[] = "icmp[icmptype] == icmp-echoreply"; // char filterExp[] = "icmp and icmp[icmptype] == 0"; //char filterExp[] = "icmp[icmpcode] == 0 and icmp[icmptype] == 0"; - cout << "Applying ICMP filter...\n"; - - //Compile the filter expression... - if (pcap_compile(captureSession, &filter, filterExp, 0, net) == -1) + if (pcap_compile(captureSession, &filter, filterExp, 1, PCAP_NETMASK_UNKNOWN) == -1 || + pcap_setfilter(captureSession, &filter) == -1) { - cerr << "[ERROR] Failed to compile filter: " << filterExp << "\n"; - cerr << " " << pcap_geterr(captureSession) << "\n"; + cerr << "[ERROR] Failed to set filter: " << pcap_geterr(captureSession) << '\n'; - return 1; - } - - //Apply the filter expression... - if (pcap_setfilter(captureSession, &filter) == -1) - { - cerr << "[ERROR] Failed to apply filter: " << filterExp << "\n"; - cerr << " " << pcap_geterr(captureSession) << "\n"; + pcap_close(sendSession); + pcap_close(captureSession); return 1; } - cout << "[OK] ICMP filter applied successfully!\n"; - //cout << "----------------------------------" << endl; - - cout << "\nStarting host discovery...\n"; - CaptureContext context{captureSession, result, .sendSession=sendSession}; + cout << "[OK] Capture filter applied.\n"; + //Begin scan. + CaptureContext context(captureSession, sendSession); + cout << "\nScanning 192.168.1.1 - 192.168.1.254...\n"; getHosts(hostList, numHosts, context); - cout << "----------------------------------" << endl; - cout << "Host discovery completed.\n"; + cout << "\nScan complete.\n"; displayHostList(hostList, numHosts); - cout << "\nReleasing resources...\n"; + //Cleanup. + cout << "\nCleaning up...\n"; pcap_freecode(&filter); - pcap_close(context.captureSession); - pcap_close(context.sendSession); - cout << "[OK] Resources released successfully.\n"; + pcap_close(captureSession); + pcap_close(sendSession); - cout << "Scan complete.\n"; - cout << "----------------------------------\n"; + cout << "[OK] Done.\n"; return 0; } //**************************************************************************************** - +// std::cout << " _ _ ____ \n"; +// std::cout << "| |__ ___ ___| |_| _ \\ ___ ___ ____ _____ \n"; +// std::cout << "| '_ \\/ _ \\/ __| __| |_) / _ \\/ __/ _ \\| '_ \\ \n"; +// std::cout << "| | | | (_) \\__ \\ |_| _ < __/ (_| (_) | | | | \n"; +// std::cout << "|_| |_|\\___/|___/\\__|_| \\_\\___|\\____/\\_|_| |_| \n"; +// Sample output below... /* - _ _ ____ -| |__ ___ ___| |_| _ \ ___ ___ ____ _____ -| '_ \/ _ \/ __| __| |_) / _ \/ __/ _ \| '_ \ -| | | | (_) \__ \ |_| _ < __/ (_| (_) | | | | -|_| |_|\___/|___/\__|_| \_\___|\____/\_|_| |_| ----------------------------------- -Initializing network interface sessions... -[OK] Network interfaces initialized successfully! -Applying ICMP filter... -[OK] ICMP filter applied successfully! - -No response. -Host 192.168.1.253 is inactive. - -***Pinging 192.168.1.254... - -Received ICMP ECHO Reply packet from 192.168.1.254 -Response received from 192.168.1.254 -Host 192.168.1.254 is active! -Copying 13 chars to list at index: 13 - -hostList updated. ----------------------------------- -Active Hosts List: -1. 192.168.1.66 -2. 192.168.1.67 -3. 192.168.1.94 -4. 192.168.1.100 -5. 192.168.1.108 -6. 192.168.1.214 -7. 192.168.1.215 -8. 192.168.1.224 -9. 192.168.1.227 -10. 192.168.1.228 -11. 192.168.1.235 -12. 192.168.1.236 -13. 192.168.1.246 -14. 192.168.1.254 ----------------------------------- + * hostRecon - libpcap-based network scanner + * ----------------------------------------- + * Initializing the network interfaces... + * [OK] interfaces initialized successfully! + * Applying filters... + * [OK] Capture filter applied. + * + * Scanning 192.168.1.1 - 192.168.1.254... + * [INFO] Resolving MAC for: 192.168.1.1... + * [WARN] Failed to resolve the MAC address for: 192.168.1.1. Skipping host. + * + * ... + * + * + * [INFO] Resolving MAC for: 192.168.1.253... + * [OK] MAC address resolved for: 192.168.1.253: A0:72:2C:67:88:94 + * ***Pinging 192.168.1.253... + * Reply from 192.168.1.253 + * Destination 192.168.1.253 is active! + * + * [INFO] Host list updated. + * [INFO] Resolving MAC for: 192.168.1.254... + * [OK] MAC address resolved for: 192.168.1.254: BC:9A:8E:20:22:F1 + * ***Pinging 192.168.1.254... + * Host 192.168.1.254 is inactive. + * + * Scan complete. + * + * Active Hosts (13): + * 1. 192.168.1.94 + * 2. 192.168.1.100 + * 3. 192.168.1.102 + * 4. 192.168.1.103 + * 5. 192.168.1.201 + * 6. 192.168.1.205 + * 7. 192.168.1.224 + * 8. 192.168.1.225 + * 9. 192.168.1.228 + * 10. 192.168.1.235 + * 11. 192.168.1.245 + * 12. 192.168.1.246 + * 13. 192.168.1.253 + * --------------------------------- + * + * Cleaning up... + * [OK] Done. + * */