<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Taha Rafi]]></title><description><![CDATA[Taha Rafi]]></description><link>https://taharafi.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 01 Nov 2026 16:37:19 GMT</lastBuildDate><atom:link href="https://taharafi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Reverse-Engineering Hardware Protocols in PHP 8: Inside ZKTeco Sockets and Direct Memory Injection]]></title><description><![CDATA[Demystifying binary framing, CommKey arithmetic scrambling, and writing attendance punches directly to terminal memory via CMD_SET_DATA.]]></description><link>https://taharafi.hashnode.dev/reverse-engineering-hardware-protocols-in-php-8-inside-zkteco-sockets-and-direct-memory-injection</link><guid isPermaLink="true">https://taharafi.hashnode.dev/reverse-engineering-hardware-protocols-in-php-8-inside-zkteco-sockets-and-direct-memory-injection</guid><category><![CDATA[PHP]]></category><category><![CDATA[Laravel]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[iot]]></category><dc:creator><![CDATA[Taha Rafi]]></dc:creator><pubDate>Fri, 30 Oct 2026 20:18:06 GMT</pubDate><content:encoded><![CDATA[<p>Hardware protocols are often viewed as the exclusive domain of C, C++, or Go systems programmers. Most web developers assume PHP cannot interact with low-level binary protocols on physical devices without intermediary wrappers.</p>
<p>This analysis breaks down the reverse engineering of the ZKTeco Standalone SDK protocol in pure userland PHP 8. We examine the exact wire layout, the 16-bit One's Complement Checksum algorithm, session authentication scrambling, and the mechanics of writing punch records directly into physical machine flash memory via command <code>CMD_SET_DATA (3001)</code>. The package requires PHP <code>ext-sockets</code> and <code>ext-json</code> but no separate vendor SDK, background daemon, or Windows DLL (no <code>zkemkeeper.dll</code>).</p>
<hr />
<h3>The ZKTeco Binary Protocol Architecture</h3>
<p>ZKTeco terminals use a custom binary protocol operating over port 4370. Every communication packet contains an 8-byte fixed header followed by optional variable-length command payloads.</p>
<h4>The 8-Byte Packet Header Structure</h4>
<table>
<thead>
<tr>
<th>Offset (Bytes)</th>
<th>Field Name</th>
<th>Data Type</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>0..1</code></td>
<td>Command / Reply Code</td>
<td><code>uint16_le</code></td>
<td>Numeric command identifier (e.g. <code>CMD_CONNECT = 1000</code>)</td>
</tr>
<tr>
<td><code>2..3</code></td>
<td>Checksum</td>
<td><code>uint16_le</code></td>
<td>16-bit One's complement sum of the entire packet</td>
</tr>
<tr>
<td><code>4..5</code></td>
<td>Session ID</td>
<td><code>uint16_le</code></td>
<td>Assigned by device during connection handshake</td>
</tr>
<tr>
<td><code>6..7</code></td>
<td>Reply / Sequence Number</td>
<td><code>uint16_le</code></td>
<td>Incremental transaction counter</td>
</tr>
</tbody></table>
<h4>The 16-Bit One's Complement Checksum</h4>
<p>The checksum algorithm calculates a 16-bit one's complement addition across 16-bit words, mirroring the classic RFC 1071 Internet checksum algorithm.</p>
<p>Here is the exact algorithmic implementation in modern PHP:</p>
<pre><code class="language-php">function calculateZkChecksum(string $packet): int
{
    $length = strlen($packet);
    $chksum = 0;
    $i = 0;

    // Sum 16-bit little-endian words
    while ($length &gt; 1) {
        $word = ord($packet[$i]) + (ord($packet[$i + 1]) &lt;&lt; 8);
        $chksum += $word;
        $i += 2;
        $length -= 2;
    }

    // Add remaining byte if odd length
    if ($length &gt; 0) {
        $chksum += ord($packet[$i]);
    }

    // Fold 32-bit sum into 16 bits
    while ($chksum &gt;&gt; 16) {
        $chksum = ($chksum &amp; 0xFFFF) + ($chksum &gt;&gt; 16);
    }

    // Bitwise NOT mask
    return (~$chksum) &amp; 0xFFFF;
}
</code></pre>
<p>During packet generation, bytes <code>2..3</code> are zeroed out while calculating the checksum. Once calculated, the resulting integer is packed as unsigned 16-bit little-endian (<code>v</code> in PHP <code>pack()</code>) and inserted into bytes <code>2..3</code>.</p>
<hr />
<h3>TCP Stream Framing Overcoming Packet Boundaries</h3>
<p>In raw UDP, each <code>socket_recvfrom()</code> call returns a discrete datagram. In TCP, data arrives as a continuous stream. If a server queries 5,000 attendance records, the hardware sends consecutive packets back-to-back. Without framing, a standard socket read will split packets midway through a record or merge two headers together.</p>
<p>To enforce boundaries over TCP stream sockets, <code>taha-rafi/php-zkteco</code> wraps the packet inside an 8-byte envelope:</p>
<pre><code class="language-plaintext">[ 4 Bytes Magic Header: \x50\x50\x82\x7d ] + [ 4 Bytes Payload Length LE ] + [ Raw ZK Packet ]
</code></pre>
<p>When reading from the socket, the client reads exactly 8 bytes first, extracts the 32-bit payload length, and then loops with <code>socket_recv()</code> until that exact payload length is consumed. This guarantees zero byte alignment corruption across high-latency WAN connections.</p>
<hr />
<h3>The CommKey Challenge: Reverse Engineering MakeKey</h3>
<p>When an administrator configures a numeric communication key on the terminal, standard connections fail with:</p>
<pre><code class="language-plaintext">Reply Code: CMD_ACK_UNAUTH (2005)
</code></pre>
<p>To authenticate, the client must compute an encrypted authorization token from the terminal session tick count and the CommKey password:</p>
<pre><code class="language-php">function makeCommKey(int $key, int $sessionId, int $ticks = 50): int
{
    $k = 0;
    for ($i = 0; $i &lt; 32; $i++) {
        if ($key &amp; (1 &lt;&lt; $i)) {
            $k = ($k &lt;&lt; 1) | 1;
        } else {
            $k = $k &lt;&lt; 1;
        }
    }

    $k += $sessionId;
    $k = pack('V', $k);
    
    // Scramble using hardware ticks modifier
    // Dispatches CMD_AUTH (1102) with scrambled 4-byte buffer
    return unpack('V', $k)[1] ^ $ticks;
}
</code></pre>
<p>Upon dispatching <code>CMD_AUTH (1102)</code> with the scrambled buffer, the terminal returns <code>CMD_ACK_OK (2000)</code>, transitioning the session into privileged state.</p>
<hr />
<h3>The Hardware Breakthrough: Direct Memory Injection (CMD_SET_DATA)</h3>
<p>Prior to <code>taha-rafi/php-zkteco</code>, every open-source PHP biometric package was read-only. If an employee registered a punch through a mobile app or web portal, there was no way to write that punch back into the physical device hardware memory.</p>
<p>The package unlocks this using command code <code>CMD_SET_DATA (3001)</code>.</p>
<h4>Hardware Table Encoding</h4>
<p>Modern ZKTeco firmware represents relational tables internally as null-terminated tabular strings. To write an attendance punch, the driver constructs an <code>ATT_LOG</code> buffer:</p>
<pre><code class="language-plaintext">TableName=ATT_LOG\r\nUser_PIN={userId}\tVerify_Type={verifyType}\tVerify_Time={isoTimestamp}\tStatus={state}\tWork_Code_ID={workCode}\0
</code></pre>
<p>Where:</p>
<ul>
<li><code>User_PIN</code>: The employee enrollment identifier (string).</li>
<li><code>Verify_Type</code>: Verification method (<code>1</code> = Fingerprint, <code>2</code> = RFID Badge, <code>15</code> = Face).</li>
<li><code>Verify_Time</code>: Formatted datetime string (<code>YYYY-MM-DD HH:MM:SS</code>).</li>
<li><code>Status</code>: Punch state (<code>0</code> = Check-In, <code>1</code> = Check-Out, <code>2</code> = Break-Out, <code>3</code> = Break-In).</li>
<li><code>Work_Code_ID</code>: Numeric work code or cost center.</li>
</ul>
<h4>The Crucial Refresh Step (CMD_REFRESHDATA)</h4>
<p>Writing via <code>CMD_SET_DATA</code> stages the record into the device SQLite or RAM buffer. If the connection drops here, the punch does not appear on the LCD screen or in exports. The driver immediately follows the write with command <code>CMD_REFRESHDATA (1013)</code>. This instructs the terminal firmware to flush dirty cache lines to flash storage, recalculate internal log counters, and update the on-screen punch log index.</p>
<hr />
<h3>Verifiable Demonstration Script</h3>
<p>The following script connects over TCP, writes an attendance punch into the machine hardware memory, verifies the record exists by reading back recent logs, and performs a selective range purge.</p>
<pre><code class="language-php">&lt;?php

declare(strict_types=1);

require_once __DIR__ . '/vendor/autoload.php';

use TahaRafi\ZKTeco\ZKTeco;
use TahaRafi\ZKTeco\Constants;

$ip = '192.168.1.201';
$port = 4370;

echo "[SYSTEM] Initializing low-level ZKTeco connection to {$ip}:{$port}...\n";
$zk = new ZKTeco(
    ip: $ip,
    port: $port,
    commKey: 0,
    protocol: Constants::PROTOCOL_TCP,
    timeoutSec: 8.0
);

if (!$zk-&gt;connect()) {
    echo "[ERROR] Could not establish TCP stream socket.\n";
    exit(1);
}

try {
    echo "[INFO] Connected successfully over TCP stream socket.\n";
    echo "[INFO] Device Serial: " . $zk-&gt;getSerialNumber() . "\n";
    echo "[INFO] Device Platform: " . $zk-&gt;getPlatform() . "\n";

    // 1. Direct Hardware Punch Injection via CMD_SET_DATA (3001)
    $targetUserId = "10042";
    $punchTimestamp = date('Y-m-d H:i:s');
    $punchState = Constants::ATT_STATE_CHECK_IN;
    $verifyType = Constants::VERIFY_FINGERPRINT;
    $workCode = 0;

    echo "\n--- Direct Memory Write Operation ---\n";
    echo "Injecting attendance record into physical device memory:\n";
    echo "  User PIN:    {$targetUserId}\n";
    echo "  Timestamp:   {$punchTimestamp}\n";
    echo "  State:       {$punchState} (Check-In)\n";
    echo "  Verify Type: {$verifyType} (Fingerprint)\n";

    $writeSuccess = $zk-&gt;writeAttendanceToDevice(
        userId: $targetUserId,
        state: $punchState,
        timestamp: $punchTimestamp,
        verifyType: $verifyType,
        workCode: $workCode
    );

    if ($writeSuccess) {
        echo "[SUCCESS] CMD_SET_DATA and CMD_REFRESHDATA accepted by hardware.\n";
    } else {
        echo "[FAILURE] Hardware rejected attendance insertion.\n";
    }

    // 2. Memory Verification: Read back punches to verify presence
    echo "\n--- Verifying Memory Persistence ---\n";
    $attendance = $zk-&gt;getAttendance();
    $found = false;
    foreach ($attendance as $record) {
        if ($record['id'] === $targetUserId &amp;&amp; $record['timestamp'] === $punchTimestamp) {
            $found = true;
            echo "[VERIFIED] Record found in terminal memory:\n";
            echo sprintf("  UID: %d | ID: %s | State: %d | Time: %s\n",
                $record['uid'],
                $record['id'],
                $record['state'],
                $record['timestamp']
            );
            break;
        }
    }

    if (!$found) {
        echo "[NOTICE] Record not immediately visible in sequential table (may require full buffer index reload).\n";
    }

    // 3. Selective Range Deletion via CMD_TIME_ATTLOG_DELE (10005)
    echo "\n--- Selective Range Deletion ---\n";
    $startTime = date('Y-m-d 00:00:00', strtotime('-7 days'));
    $endTime = date('Y-m-d 23:59:59', strtotime('-1 day'));

    echo "Deleting punch records between {$startTime} and {$endTime}...\n";
    $deleteSuccess = $zk-&gt;deleteAttendanceByTimeRange($startTime, $endTime);

    if ($deleteSuccess) {
        echo "[SUCCESS] Selective time range deletion completed without full wipe.\n";
    } else {
        echo "[WARNING] Device did not acknowledge range deletion command.\n";
    }

} catch (\Throwable $e) {
    echo "[EXCEPTION] Low-level communication failure: " . $e-&gt;getMessage() . "\n";
} finally {
    $zk-&gt;disconnect();
    echo "[INFO] TCP socket released.\n";
}
</code></pre>
<hr />
<h3>Architectural Implications</h3>
<p>Implementing binary protocols directly in PHP removes the operational complexity of maintaining multi-runtime stacks. By understanding binary framing, checksum verification, and session state machines, PHP applications can interact directly with embedded systems and physical infrastructure with native performance.</p>
]]></content:encoded></item></channel></rss>