DEV Community

Taha Rafi
Taha Rafi

Posted on

How I Reverse-Engineered ZKTeco’s Binary Protocol in Pure PHP 8 to Build a Direct Hardware Attendance Writer

If you’ve ever had to integrate ZKTeco biometric time-and-attendance clocks (fingerprint scanners, facial recognition terminals, RFID readers) into a modern web stack, you already know the grim reality of the ecosystem:

  • Most existing Packagist packages are 6 to 10 years old abandonware.
  • They throw fatal deprecation notices on PHP 8.1, 8.2, 8.3, and 8.4.
  • They are strictly UDP-only—meaning when deployed across remote offices over WAN routers or NAT firewalls, UDP packets drop silently without error messages.
  • None of them support writing attendance back to the machine. If an employee forgot to clock in or your web app needs to push an authorized remote punch, you were told "it's impossible without ZKTeco's proprietary Windows C++ DLLs".

I decided to fix this once and for all. I reverse-engineered the binary protocol and built taha-rafi/php-zkteco (v2.1.0).


1. The Protocol Problem: UDP vs. TCP in Enterprise Networks

The original ZK protocol was designed in the late 1990s around raw UDP datagrams (CMD_CONNECT: 1000). While UDP is fast on a local switch, modern distributed enterprises connect clocks across branch offices, cloud VPNs, and port-forwarded WAN routers.

In php-zkteco, we implemented a Dual-Protocol Engine with automatic fallback:

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

// Automatically tries TCP stream first, then transparently falls back to UDP
$zk = new ZKTeco('192.168.1.201', 4370, commKey: 0, protocol: Constants::PROTOCOL_AUTO);

if ($zk->connectWithRetry(maxRetries: 3, retryDelayMs: 400)) {
    echo "Connected via: " . $zk->getActiveProtocol(); // TCP or UDP
}
Enter fullscreen mode Exit fullscreen mode

The TCP implementation encapsulates packets with a 32-bit length header (0x5050827D) and handles keep-alive pinging, ensuring zero packet drops across WAN routers.


2. Reverse-Engineering Direct Hardware Attendance Writing (CMD_SET_DATA 3001)

Reading logs from a biometric clock is easy (CMD_ATTLOG_RRQ: 501). But what happens when an HR manager manually approves a punch in your Laravel app and you need the physical biometric terminal to recognize it?

Through packet analysis of ZKTeco's internal protocol, we uncovered the payload schema required to write directly to the machine's internal Flash table (ATT_LOG):

TableName=ATT_LOG\r\nUser_PIN={UID}\tVerify_Type={VT}\tVerify_Time={TIMESTAMP}\tStatus={STATE}\tWork_Code_ID=0\0
Enter fullscreen mode Exit fullscreen mode

By packaging this buffer with CMD_SET_DATA (3001) and immediately dispatching CMD_REFRESHDATA (1013) with CMD_VOICE (1014), the machine not only permanently logs the check-in/out record inside its memory—it also plays the hardware voice: "Thank you"!

// Write a punch directly to physical hardware memory:
$zk->markAttendance(
    userId: '101',
    state: Constants::ATT_STATE_CHECK_IN,
    timestamp: '2026-10-17 09:00:00',
    verifyType: Constants::VERIFY_FINGERPRINT,
    options: [
        'write_to_device' => true,
        'device_feedback' => true // Triggers machine voice & LCD display
    ]
);
Enter fullscreen mode Exit fullscreen mode

3. Laravel 8–12 Native Support (Zero Configuration)

We included an automatic ServiceProvider and Facade. In any Laravel project:

composer require taha-rafi/php-zkteco
Enter fullscreen mode Exit fullscreen mode

In your Artisan command or scheduled console kernel:

use TahaRafi\ZKTeco\Facades\ZKTeco;
use App\Models\Attendance;

class SyncBiometricClock extends Command
{
    public function handle(): void
    {
        $logs = ZKTeco::getAttendanceStructured();

        foreach ($logs as $log) {
            Attendance::firstOrCreate([
                'user_id'   => $log['user_id'],
                'timestamp' => $log['timestamp'],
            ], [
                'state'     => $log['state_label'],
                'verify'    => $log['verify_label'],
            ]);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Continuous Integration & Benchmark

  • 100% Pure PHP 8.0 - 8.4: Zero vendor C++ SDKs, zero ext-sockets required (uses standard stream wrappers).
  • Automated CI Suite: 42 unit and protocol tests passing on PHP 8.0, 8.1, 8.2, 8.3, and 8.4.
  • Tested Models: TX628, iClock series, K40, UFace, and standard ZMM standalone terminals.

Check out the repository on GitHub:
👉 https://github.com/taha-rafi/php-zkteco

👉 Packagist: https://packagist.org/packages/taha-rafi/php-zkteco

Top comments (0)