Network Programming in Assembly: A Single-Pass Algorithm for Printing IP Addresses
When doing low-level network programming in Assembly, you experience firsthand the immense chaos running behind the scenes of operations we solve with a single line in high-level languages (Python, C, etc.). While developing the Nested-ICMP-Exploitation project, specifically an ICMP tunneling engine, I hit exactly this kind of wall: extracting an IP address from a packet header and printing it to the screen in the correct format.
Sounds simple, right? However, when x86 architecture and network protocols are involved, seeing 5.1.168.192 instead of 192.168.1.5 on your terminal is extremely common.
So why does this happen, and what kind of algorithm did I develop to overcome this issue during the debugging process? Let's dive into the background.
The Endianness Problem in Network Headers
When you capture a packet coming over the network and read the source/destination IP address inside the sockaddr_in structure, the data arrives in Network Byte Order (Big-Endian) format. This means the most significant byte is stored at the lowest memory address.
However, the x86/x64 processor architectures we use rely on Little-Endian (Host Byte Order). When the processor pulls this 4-byte IP data into a register, the reading direction is effectively reversed for our purposes.
The result? A packet that arrives as 192.168.1.5 appears scrambled if we try to naively print it from memory. The inet_ntoa() function in high-level languages handles this conversion in the background. But if you are writing a custom sniffer in pure Assembly, you must do this conversion byte by byte yourself.
Debugging Hell: The Problems Encountered
While writing this conversion, I encountered a few critical issues that cost me hours in GDB (GNU Debugger):
Register Clashes: While separating each octet (byte) of the IP address and converting it to an ASCII character (string), you must use the
AXregister for division operations (DIV). If you don't carefully manage your remainders (AH) and quotients (AL), the numbers of the IP address get completely corrupted.The Dot (.) Separator: It's not enough to just convert the numbers; a
.(0x2E / 46 in decimal) character must be inserted exactly between each octet, but not at the very end.Performance Loss (The Reversing Trap): In standard logic, you parse the IP, convert it to a string, and realize the string is backwards. Then, you write a second loop to reverse that string. This creates unnecessary memory read/write cycles and bloats the code.
The Solution: A Single-Pass Backward Build Algorithm
To solve the problem, instead of creating the string and then reversing it, I designed a more optimized algorithm.
The logic is simple but highly effective: Read the IP bytes backwards, and write the ASCII string backwards. By starting at the end of the IP address within the sockaddr_in structure (offset 7 down to 4) and writing from the end of a 15-byte output buffer (addr_ip) down to index 0, the string naturally formats itself correctly from left to right.
Here is the exact critical loop from my engine:
ASSEMBLY CODE:
-----------------------------------------------------------------------------------
; IP ADDRESS TO STRING ALGORITHM (EXTRACT REVERSE BYTE-BY-BYTE AND CONVERT TO ASCII)
This single-pass method successfully converts the raw network bytes into a human-readable ASCII string using minimal CPU cycles, entirely avoiding an extra "string reversing" loop.
Conclusion and Open Source
You can find this algorithm in action, along with the complete source code of the asm-icmp-sniffer, on my GitHub profile: JM00NJ/asm-icmp-snifferDisclaimer: This article and the associated source code are intended for educational purposes and authorized security research only. Understanding low-level network protocols is essential for building better defense mechanisms.