AI-Assisted Security Audit of Open-Source Shared Libraries Before Production Use
The Problem
When a Linux package manager installs a shared library, it drops a .so file into /usr/lib or /usr/local/lib, runs ldconfig, and the library immediately enters the trusted path for every process on the host. From that moment, any process that dlopen()s the library or links against it at startup executes its code with the privileges of the calling process. There is no sandbox, no permission gate, no runtime verification that the binary you received matches what the project published as source.
Traditional security practice for shared libraries is to trust the distribution package: if the package was signed by the distribution’s key and the repository metadata was verified, the library is assumed safe. This model worked well when the threat was primarily a network attacker intercepting package downloads. It breaks down against the threats that have become dominant: upstream compromise (the package in the registry is tampered before distribution signing), trojanised builds (the source is clean but the CI system was compromised during the build), and dependency confusion (a maliciously named package that shadow-resolves ahead of the legitimate one).
The source-binary alignment problem is particularly acute. A project publishes libcurl 8.7.1 source code on GitHub. The apt repository serves libcurl4 8.7.1-1ubuntu1. Unless someone has performed a reproducible build of the source and diffed the result against the distributed binary, nobody has verified that those two things are the same. For the vast majority of libraries in production, they have not been verified.
AI-powered binary analysis changes the cost equation. Until recently, binary-level analysis of shared libraries — lifting disassembly to intermediate representation, performing taint analysis across function call graphs, identifying anomalous capabilities — required dedicated security researchers and weeks of work per library. Tools like Ghidra, Binary Ninja, and angr made the mechanics available but still demanded expert users. In 2026, LLM-assisted scripting has reduced the entry cost dramatically. An AI-generated Ghidra headless script can extract all external function calls from a library, group them by capability class, and flag any execve, socket, or /proc read in a library that declares itself a pure math utility. An LLM-generated angr harness can set up taint analysis on library input functions in minutes instead of hours.
The goal is not to replace expert reverse engineering for high-value targets. The goal is to apply a structured, automatable pre-production gate that catches the most common tampering signatures and unexpected capability additions before a library ever reaches a production host.
Threat Model
Upstream compromise. An attacker with access to a package registry — via stolen maintainer credentials, a compromised build server, or a malicious package mirror — modifies the binary that gets distributed without modifying the published source. The binary diverges from the source in ways that are not visible to anyone performing source code review. The XZ utils backdoor (CVE-2024-3094) was an extreme example: the malicious code was introduced in the compressed release tarball, not the git repository, specifically to evade source-based review. Binary-level analysis detects the divergence.
Dependency confusion. A package named libinternal-auth exists on an internal package server at version 1.2.3. An attacker publishes a package with the same name to PyPI, npm, or a public Cargo registry at version 9.9.9. Build systems that check public registries before private ones pull the attacker’s version. The malicious .so or .a is linked into the application and loaded at runtime. Capability analysis flags a library that should implement only authentication logic but instead opens sockets or reads environment variables containing credentials.
Trojanised build. The project’s CI/CD pipeline is compromised. Source code in the repository is clean and passes review. During the CI build, a malicious build dependency, a compromised build tool, or an injected pipeline step modifies the compiled output. The resulting binary has additional functionality not present in any source file. Reproducible build comparison — rebuilding from source and diffing against the distributed binary — is the primary detection mechanism.
Runtime injection. An attacker with local access to the host (initial access via web shell, SSRF, or credential theft) places a malicious .so into a library search path ahead of the legitimate library. LD_LIBRARY_PATH, /etc/ld.so.conf.d/ entries, or RPATH entries in executables can all be abused to redirect library loading. An LD_AUDIT hook that monitors the runtime linker’s resolution decisions detects unexpected library loads and alerts before the malicious code executes in a sensitive process.
Hardening Configuration
Step 1: Checksec and Readelf Baseline Audit
Before any deeper analysis, confirm that installed libraries have the expected binary security properties. A library missing RELRO, stack canaries, or with an executable stack is a red flag independent of its functional behaviour.
#!/usr/bin/env bash
# /usr/local/bin/lib-security-baseline.sh
# Run as root or with sudo to reach all library paths
set -euo pipefail
REPORT_DIR="/var/log/lib-security-audit"
mkdir -p "$REPORT_DIR"
TIMESTAMP=$(date +%Y%m%dT%H%M%S)
REPORT="$REPORT_DIR/baseline-$TIMESTAMP.json"
LIB_PATHS=(
/usr/lib
/usr/lib/x86_64-linux-gnu
/usr/local/lib
/lib/x86_64-linux-gnu
)
echo '{"timestamp":"'"$TIMESTAMP"'","libraries":[' > "$REPORT"
FIRST=1
for DIR in "${LIB_PATHS[@]}"; do
find "$DIR" -maxdepth 2 -name "*.so*" -type f 2>/dev/null | while read -r lib; do
# checksec outputs structured data with --format=json
CHECKSEC_OUT=$(checksec --format=json --file="$lib" 2>/dev/null || echo '{}')
# readelf for additional properties
HAS_RELRO="none"
if readelf -d "$lib" 2>/dev/null | grep -q 'GNU_RELRO'; then
HAS_RELRO="partial"
fi
if readelf -d "$lib" 2>/dev/null | grep -q 'BIND_NOW'; then
HAS_RELRO="full"
fi
NX_STACK=$(readelf -W -l "$lib" 2>/dev/null | \
awk '/GNU_STACK/ { print ($6=="RWE" || $6=="RW") ? "disabled" : "enabled" }')
# Flag libraries with missing protections
RISK="low"
if [[ "$HAS_RELRO" == "none" ]] || [[ "$NX_STACK" == "disabled" ]]; then
RISK="high"
elif [[ "$HAS_RELRO" == "partial" ]]; then
RISK="medium"
fi
[[ "$FIRST" -eq 1 ]] || echo "," >> "$REPORT"
FIRST=0
jq -n \
--arg path "$lib" \
--arg relro "$HAS_RELRO" \
--arg nx "$NX_STACK" \
--arg risk "$RISK" \
--argjson cs "$CHECKSEC_OUT" \
'{path: $path, relro: $relro, nx_stack: $nx, risk: $risk, checksec: $cs}' >> "$REPORT"
done
done
echo ']}' >> "$REPORT"
# Emit high-risk findings to stdout for CI/alerting integration
jq '.libraries[] | select(.risk == "high") | .path' "$REPORT"
echo "Full report: $REPORT"
Run this after every package installation as part of a post-install hook:
# /etc/apt/apt.conf.d/99-lib-security-hook
DPkg::Post-Invoke {"if [ -x /usr/local/bin/lib-security-baseline.sh ]; then /usr/local/bin/lib-security-baseline.sh >> /var/log/lib-security-audit/apt-hook.log 2>&1; fi";};
Step 2: AI-Assisted Ghidra Headless Analysis
Ghidra’s headless analyser can be scripted to perform automated function-level analysis. The following script uses Ghidra to extract external calls from a library and then passes the results to an LLM to classify them by capability.
# ghidra_capability_audit.py — run with: ghidra_headless.py
# Usage: $GHIDRA_HOME/support/analyzeHeadless /tmp/ghidra_proj LibAudit \
# -import /usr/lib/x86_64-linux-gnu/libtarget.so \
# -postScript ghidra_capability_audit.py \
# -scriptPath /opt/audit-scripts
from ghidra.app.util.headless import HeadlessScript
from ghidra.program.model.symbol import SymbolType
import json
def run():
fm = currentProgram.getFunctionManager()
external_calls = []
for func in fm.getFunctions(True):
for ref in func.getCalledFunctions(monitor):
if ref.isExternal():
external_calls.append({
"caller": func.getName(),
"callee": ref.getName(),
"address": str(func.getEntryPoint())
})
# Write for LLM analysis
output_path = "/tmp/ghidra_external_calls.json"
with open(output_path, "w") as f:
json.dump(external_calls, f, indent=2)
print(f"[+] Extracted {len(external_calls)} external calls to {output_path}")
After Ghidra extraction, pass the results to an LLM for capability classification:
#!/usr/bin/env python3
# llm_capability_classifier.py
import json
import sys
import anthropic
SUSPICIOUS_CAPABILITIES = {
"network": ["socket", "connect", "bind", "sendto", "recvfrom", "getaddrinfo"],
"execution": ["execve", "execvp", "system", "popen", "posix_spawn"],
"proc_access": ["open", "read", "readlink"], # flagged when path is /proc
"credential_access": ["getenv", "getpwuid", "getpwnam"],
"privilege": ["setuid", "setgid", "capset", "prctl"],
}
def classify_with_llm(lib_name: str, calls: list) -> dict:
client = anthropic.Anthropic()
calls_text = json.dumps(calls[:500], indent=2) # cap at 500 entries
prompt = f"""You are auditing a shared library named '{lib_name}' for unexpected capabilities.
Below is a list of external function calls extracted from the library binary via Ghidra.
Each entry shows which internal function makes a call to which external (libc or syscall wrapper) function.
External calls:
{calls_text}
For this library, identify:
1. Any network capability calls (socket, connect, bind, sendto, etc.) — flag as HIGH risk if present in a library that should have no network function
2. Any execution capability calls (execve, system, popen) — flag as CRITICAL
3. Any /proc filesystem access patterns — flag as MEDIUM
4. Any credential or environment variable access (getenv, getpwuid) — flag as MEDIUM
5. Any privilege manipulation (setuid, capset, prctl) — flag as HIGH
Respond as JSON with this exact structure:
{{
"capabilities_found": ["list of capability categories detected"],
"high_risk_calls": [
{{"function": "name", "called_by": "caller", "risk": "HIGH/CRITICAL/MEDIUM", "reason": "..."}}
],
"overall_risk": "LOW/MEDIUM/HIGH/CRITICAL",
"summary": "one paragraph summary",
"recommendation": "APPROVE/REVIEW/REJECT"
}}"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}]
)
return json.loads(response.content[0].text)
if __name__ == "__main__":
calls_file = sys.argv[1]
lib_name = sys.argv[2] if len(sys.argv) > 2 else "unknown"
with open(calls_file) as f:
calls = json.load(f)
result = classify_with_llm(lib_name, calls)
print(json.dumps(result, indent=2))
if result["overall_risk"] in ("HIGH", "CRITICAL"):
sys.exit(1)
Step 3: Taint Analysis with angr and LLM-Generated Harnesses
angr performs symbolic execution and taint analysis. LLM assistance dramatically reduces harness authoring time.
#!/usr/bin/env python3
# angr_taint_harness.py — LLM-generated harness template, then customised
# Checks: does attacker-controlled input to library entry points reach exec/socket/write?
import angr
import claripy
import sys
LIB_PATH = sys.argv[1] # e.g. /usr/lib/x86_64-linux-gnu/libfoo.so
ENTRY_FUNC = sys.argv[2] # e.g. foo_parse_packet
# Load binary — use full_init_state for library analysis
proj = angr.Project(LIB_PATH, load_options={
"auto_load_libs": False,
"main_opts": {"base_addr": 0x400000}
})
# Create symbolic input buffer (attacker-controlled network data)
INPUT_SIZE = 1024
sym_input = claripy.BVS("attacker_input", INPUT_SIZE * 8)
# Find the entry function address
entry_sym = proj.loader.find_symbol(ENTRY_FUNC)
if not entry_sym:
print(f"[!] Symbol '{ENTRY_FUNC}' not found in {LIB_PATH}")
sys.exit(1)
entry_addr = entry_sym.rebased_addr
# Initial state: call entry function with symbolic buffer pointer
state = proj.factory.call_state(
entry_addr,
sym_input,
claripy.BVV(INPUT_SIZE, 64),
add_options={angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS}
)
# Danger addresses: resolve libc functions we want to detect reaching
DANGER_FUNCS = ["execve", "system", "socket", "connect", "write", "sendto"]
danger_addrs = []
for func_name in DANGER_FUNCS:
sym = proj.loader.find_symbol(func_name)
if sym:
danger_addrs.append((func_name, sym.rebased_addr))
print(f"[*] Tracking {func_name} at {hex(sym.rebased_addr)}")
simgr = proj.factory.simulation_manager(state)
# Run for bounded steps to avoid infinite analysis
simgr.run(n=2000)
print(f"[*] Analysis complete: {len(simgr.deadended)} paths explored")
for stash in simgr.stashes:
if simgr.stashes[stash]:
print(f" {stash}: {len(simgr.stashes[stash])} states")
# Check if any path reaches a danger function with tainted (symbolic) arguments
for stash_name, states in simgr.stashes.items():
for s in states:
for func_name, addr in danger_addrs:
# If the instruction pointer can reach this address
if s.solver.satisfiable(extra_constraints=[s.ip == addr]):
print(f"[CRITICAL] Tainted input can reach {func_name}!")
Step 4: Reproducible Build Comparison with diffoscope
#!/usr/bin/env bash
# repro-build-check.sh — compare installed library against source rebuild
# Requires: diffoscope, docker (for hermetic build environment)
set -euo pipefail
PACKAGE_NAME="$1" # e.g. libcurl4
PACKAGE_VERSION="$2" # e.g. 8.7.1-1ubuntu1
WORK_DIR=$(mktemp -d)
INSTALLED_LIB=$(dpkg -L "$PACKAGE_NAME" | grep '\.so$' | head -1)
echo "[*] Comparing installed $INSTALLED_LIB against reproducible build"
cp "$INSTALLED_LIB" "$WORK_DIR/installed.so"
# Pull source and rebuild in hermetic container
docker run --rm \
-v "$WORK_DIR:/out" \
-e PACKAGE="$PACKAGE_NAME" \
-e VERSION="$PACKAGE_VERSION" \
ubuntu:24.04 bash -c '
apt-get update -qq
apt-get build-dep -y "$PACKAGE" 2>/dev/null
apt-get source "$PACKAGE=$VERSION" 2>/dev/null
cd $(ls -d */); dpkg-buildpackage -b -uc -us 2>/dev/null
cp ../*.so* /out/rebuilt.so 2>/dev/null || true
'
if [[ ! -f "$WORK_DIR/rebuilt.so" ]]; then
echo "[WARN] Could not produce rebuilt binary; skipping diffoscope"
exit 0
fi
# Run diffoscope — non-zero exit means differences found
if diffoscope \
--html "$WORK_DIR/diff-report.html" \
--json "$WORK_DIR/diff-report.json" \
"$WORK_DIR/installed.so" \
"$WORK_DIR/rebuilt.so"; then
echo "[PASS] Installed binary matches reproducible build"
else
echo "[FAIL] Binary divergence detected — review $WORK_DIR/diff-report.html"
# Extract section-level differences for quick triage
jq '.[] | select(.unified_diff != null) | .path' "$WORK_DIR/diff-report.json" 2>/dev/null || true
exit 1
fi
Step 5: LD_AUDIT Runtime Library Load Monitoring
LD_AUDIT is a glibc mechanism that lets a monitoring library intercept all dynamic linker decisions. This detects runtime injection of unexpected .so files.
/* ld_audit_monitor.c — compile as shared library */
/* gcc -shared -fPIC -o /usr/local/lib/ld_audit_monitor.so ld_audit_monitor.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <syslog.h>
#include <link.h>
/* Allowlist of expected library paths — loaded from environment or file */
static const char *EXPECTED_PREFIXES[] = {
"/usr/lib/",
"/usr/lib/x86_64-linux-gnu/",
"/lib/x86_64-linux-gnu/",
"/lib64/",
NULL
};
unsigned int la_version(unsigned int version) {
openlog("ld_audit_monitor", LOG_PID | LOG_NDELAY, LOG_SECURITY);
return LAV_CURRENT;
}
char *la_objsearch(const char *name, uintptr_t *cookie, unsigned int flag) {
return (char *)name; /* pass through search unchanged */
}
unsigned int la_objopen(struct link_map *map, Lmid_t lmid, uintptr_t *cookie) {
const char *path = map->l_name;
if (!path || path[0] == '\0') return LA_FLG_BINDFROM | LA_FLG_BINDTO;
int path_ok = 0;
for (int i = 0; EXPECTED_PREFIXES[i] != NULL; i++) {
if (strncmp(path, EXPECTED_PREFIXES[i], strlen(EXPECTED_PREFIXES[i])) == 0) {
path_ok = 1;
break;
}
}
if (!path_ok) {
syslog(LOG_CRIT,
"UNEXPECTED_LIBRARY_LOAD path=%s lmid=%ld — possible injection",
path, (long)lmid);
/* In enforcement mode, kill the process rather than load unknown library */
const char *enforce = getenv("LD_AUDIT_ENFORCE");
if (enforce && strcmp(enforce, "1") == 0) {
syslog(LOG_CRIT, "ENFORCEMENT: aborting process to prevent load of %s", path);
abort();
}
}
return LA_FLG_BINDFROM | LA_FLG_BINDTO;
}
Deploy this system-wide for sensitive processes:
# /etc/systemd/system/nginx.service.d/ld-audit.conf
[Service]
Environment="LD_AUDIT=/usr/local/lib/ld_audit_monitor.so"
Environment="LD_AUDIT_ENFORCE=1"
Step 6: Automated Pre-Deployment Pipeline
# .github/workflows/library-security-gate.yml
name: Shared Library Security Gate
on:
workflow_dispatch:
inputs:
package_name:
description: 'apt package name (e.g. libcurl4)'
required: true
package_version:
description: 'Version string (e.g. 8.7.1-1ubuntu1)'
required: true
jobs:
library-audit:
runs-on: ubuntu-24.04
container:
image: ubuntu:24.04
options: --privileged
steps:
- name: Install audit tooling
run: |
apt-get update -qq
apt-get install -y --no-install-recommends \
checksec readelf-tools binutils \
python3-pip diffoscope docker.io
pip3 install angr anthropic
- name: Install target package
run: |
apt-get install -y "${{ inputs.package_name }}=${{ inputs.package_version }}"
- name: Checksec baseline
id: checksec
run: |
/usr/local/bin/lib-security-baseline.sh 2>&1 | tee /tmp/checksec-output.txt
# Fail if any high-risk libraries found
if grep -q '"risk":"high"' /tmp/checksec-output.txt; then
echo "high_risk=true" >> "$GITHUB_OUTPUT"
fi
- name: Ghidra capability extraction
run: |
LIB_PATH=$(dpkg -L "${{ inputs.package_name }}" | grep '\.so$' | head -1)
$GHIDRA_HOME/support/analyzeHeadless /tmp/ghidra_proj LibAudit \
-import "$LIB_PATH" \
-postScript ghidra_capability_audit.py \
-scriptPath .github/scripts
- name: LLM capability classification
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
python3 .github/scripts/llm_capability_classifier.py \
/tmp/ghidra_external_calls.json \
"${{ inputs.package_name }}" | tee /tmp/llm-analysis.json
RISK=$(jq -r '.overall_risk' /tmp/llm-analysis.json)
echo "llm_risk=$RISK" >> "$GITHUB_OUTPUT"
- name: Reproducible build comparison
run: |
bash .github/scripts/repro-build-check.sh \
"${{ inputs.package_name }}" \
"${{ inputs.package_version }}"
- name: Generate approval record
run: |
jq -n \
--arg pkg "${{ inputs.package_name }}" \
--arg ver "${{ inputs.package_version }}" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--argjson llm "$(cat /tmp/llm-analysis.json)" \
'{package: $pkg, version: $ver, audited_at: $ts, llm_analysis: $llm, status: "APPROVED"}' \
> "approved-libs/${{ inputs.package_name }}-${{ inputs.package_version }}.json"
- name: Upload audit artifacts
uses: actions/upload-artifact@v4
with:
name: library-audit-${{ inputs.package_name }}
path: /tmp/
retention-days: 90
Expected Behaviour After Hardening
After implementing this pipeline, a library must pass all gates before being permitted into the production library allowlist. A new version of libpq5 (PostgreSQL client library) arriving via a routine apt upgrade is held until the automated workflow completes. If the reproducible build check passes and the LLM capability classifier returns "overall_risk": "LOW" with no flagged external calls beyond malloc, memcpy, and PostgreSQL protocol functions, the library is added to the approved manifest and permitted. If the Ghidra analysis flags an unexpected socket() call in a newly added function, the workflow fails, blocks the upgrade, and creates a GitHub issue with the LLM’s structured explanation.
The LD_AUDIT monitor in observation mode (non-enforcement) logs every library load for 30 days, building a baseline of expected load patterns per process. After the baseline period, enforcement mode activates and processes are terminated if they attempt to load a library not on the approved list.
Security properties checked:
- All production libraries carry Full RELRO, stack canaries, and NX stack
- No library with unexpected network, execution, or privilege capabilities is deployed without manual security review
- Checksums of deployed libraries are recorded in an immutable audit log and verified daily
- The
LD_AUDITmonitor generates aSECURITYsyslog alert for any out-of-allowlist library load within 5 seconds of the event
Trade-offs and Operational Considerations
| Consideration | Impact | Mitigation |
|---|---|---|
| Ghidra headless analysis is slow (5–30 min per library) | Adds latency to package deployment pipeline | Run analysis asynchronously; block production promotion, not package installation on dev systems |
| angr symbolic execution may not terminate for complex libraries | Analysis timeout causes indeterminate result | Set hard timeout (15 min); treat timeout as REVIEW rather than PASS or FAIL |
| Reproducible builds not available for all packages | Significant proportion of packages cannot be source-verified | Track coverage metric; prioritise libraries with network-facing attack surface |
| LLM classification has false positive rate on generic libc calls | Legitimate libraries flagged for review | Tune allowlist of expected calls per library category; train classifiers on confirmed-safe examples |
| LD_AUDIT in enforcement mode may abort legitimate processes | Application availability risk during rollout | Phase rollout: observe mode (30 days) → enforce for new processes → enforce for all |
| LLM API cost for large libraries | Budget impact at scale | Cache results by library content hash; only re-analyse on binary change |
| angr taint analysis misses obfuscated indirect calls | True positive gap for sophisticated attackers | Combine with Ghidra dynamic analysis; LD_AUDIT provides runtime defence in depth |
Failure Modes
| Failure Mode | Trigger | Detection | Response |
|---|---|---|---|
| Ghidra headless script crashes on malformed ELF | Corrupt or hand-crafted binary | Non-zero exit from analyzeHeadless | Fail the gate; flag library for manual review |
| LLM returns non-JSON response | API error or unexpected model output | JSON parse exception in classifier | Retry once; if second failure, route to manual review queue |
| diffoscope OOM on large library | Library binary exceeds available RAM | OOM kill in CI runner | Increase runner memory; fall back to sha256sum comparison against pinned known-good hash |
| LD_AUDIT monitor conflicts with sanitiser runtimes | ASAN/MSAN in development builds also uses LD_PRELOAD hooks | Process crashes at startup | Disable LD_AUDIT enforcement in dev/test environments; production only |
| Approved library hash changes after silent CDN update | Package mirror serves stale or tampered content | Daily hash verification job detects mismatch | Immediate alert; quarantine affected hosts; re-run full audit against current binary |
| angr fails on stripped binary | Library has no symbol table | Empty function list with no errors | Complement with Ghidra’s own stripped-binary analysis; note in audit record |
| LD_AUDIT enforcement mode kills init or critical daemon | Broad deployment before baseline established | Host instability | Always exempt PID 1 and system services during initial rollout |