Skip to content

Instantly share code, notes, and snippets.

@zhangyoufu
Created April 28, 2026 05:04
Show Gist options
  • Select an option

  • Save zhangyoufu/29cd252dfa1fc70fd2c715c3f42a0708 to your computer and use it in GitHub Desktop.

Select an option

Save zhangyoufu/29cd252dfa1fc70fd2c715c3f42a0708 to your computer and use it in GitHub Desktop.
log argv/envp/stdin/stdout/stderr to pcap
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <poll.h>
#include <errno.h>
#include <arpa/inet.h>
#include <stdint.h>
/* The fixed target execution binary */
#ifndef TARGET_BINARY
#error TARGET_BINARY is not defined
#endif
/* Global PCAP Header (24 bytes) */
struct pcap_file_header {
uint32_t magic;
uint16_t version_major;
uint16_t version_minor;
int32_t thiszone;
uint32_t sigfigs;
uint32_t snaplen;
uint32_t network;
};
/* PCAP Packet Header (16 bytes) */
struct pcap_pkthdr {
uint32_t ts_sec;
uint32_t ts_usec;
uint32_t incl_len;
uint32_t orig_len;
};
/* Encapsulate payload into a forged Ethernet/IP/UDP packet and commit to PCAP safely */
void write_pcap_packet(int pcap_fd, int dport, int sport, const void *payload, size_t payload_len) {
size_t udp_len = 8 + payload_len;
size_t ip_len = 20 + udp_len;
size_t packet_len = 14 + ip_len;
size_t total_size = 16 + packet_len;
char *buf = malloc(total_size);
if (!buf) return;
// 1. PCAP packet header
struct timeval tv;
gettimeofday(&tv, NULL);
struct pcap_pkthdr pkthdr;
pkthdr.ts_sec = tv.tv_sec;
pkthdr.ts_usec = tv.tv_usec;
pkthdr.incl_len = packet_len;
pkthdr.orig_len = packet_len;
memcpy(buf, &pkthdr, 16);
// 2. Fake Ethernet Header (14 bytes)
uint8_t *eth = (uint8_t *)(buf + 16);
memset(eth, 0xaa, 12); // Dummy src/dest MACs
eth[12] = 0x08; // IPv4 EtherType (0x0800)
eth[13] = 0x00;
// 3. Fake IPv4 Header (20 bytes)
uint8_t *ip = eth + 14;
ip[0] = 0x45; // Version=4, IHL=5
ip[1] = 0; // TOS
uint16_t ip_len_n = htons(ip_len);
memcpy(ip + 2, &ip_len_n, 2);
uint16_t ip_id = htons(1);
memcpy(ip + 4, &ip_id, 2);
uint16_t ip_frag = htons(0); // No fragmentation
memcpy(ip + 6, &ip_frag, 2);
ip[8] = 64; // TTL
ip[9] = 17; // Protocol (UDP)
uint16_t zero_csum = 0;
memcpy(ip + 10, &zero_csum, 2);
uint32_t src_ip = htonl(0x0a000001); // Source IP: 10.0.0.1
memcpy(ip + 12, &src_ip, 4);
uint32_t dst_ip = htonl(0x0a000002); // Dest IP: 10.0.0.2
memcpy(ip + 16, &dst_ip, 4);
// Calculate IPv4 Checksum reliably (circumventing memory alignment faults on older architectures)
uint32_t sum = 0;
for (int i = 0; i < 10; i++) {
uint16_t word;
memcpy(&word, ip + i * 2, 2);
sum += word;
}
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
uint16_t csum = ~sum;
memcpy(ip + 10, &csum, 2);
// 4. Fake UDP Header (8 bytes)
uint8_t *udp = ip + 20;
uint16_t sport_n = htons(sport);
memcpy(udp + 0, &sport_n, 2);
uint16_t dport_n = htons(dport);
memcpy(udp + 2, &dport_n, 2);
uint16_t udp_len_n = htons(udp_len);
memcpy(udp + 4, &udp_len_n, 2);
uint16_t udp_csum = 0; // Checksum 0x0000 instructs parsers to skip validation
memcpy(udp + 6, &udp_csum, 2);
// 5. Encapsulate intercepted payload
memcpy(udp + 8, payload, payload_len);
// File lock and commit packet
flock(pcap_fd, LOCK_EX);
ssize_t w = write(pcap_fd, buf, total_size);
(void)w;
flock(pcap_fd, LOCK_UN);
free(buf);
}
/* Intercept and split Execve (argv / envp) over PCAP payload limits */
void write_execve_details(int pcap_fd, int pid, char **argv, char **envp) {
size_t total_size = 0;
for (int i = 0; argv && argv[i]; i++) total_size += strlen(argv[i]) + 1;
total_size += 1;
for (int i = 0; envp && envp[i]; i++) total_size += strlen(envp[i]) + 1;
total_size += 1;
char *buf = malloc(total_size);
if (!buf) return;
char *p = buf;
for (int i = 0; argv && argv[i]; i++) {
size_t len = strlen(argv[i]) + 1;
memcpy(p, argv[i], len);
p += len;
}
*p++ = '\0'; // Sequential marker separation
for (int i = 0; envp && envp[i]; i++) {
size_t len = strlen(envp[i]) + 1;
memcpy(p, envp[i], len);
p += len;
}
*p++ = '\0'; // End string iteration
size_t remaining = total_size;
char *data_ptr = buf;
// Stream payloads respecting the MTU threshold
while (remaining > 0) {
size_t chunk = remaining > 8192 ? 8192 : remaining;
write_pcap_packet(pcap_fd, 3, pid, data_ptr, chunk);
data_ptr += chunk;
remaining -= chunk;
}
free(buf);
}
int main(int argc, char **argv, char **envp) {
// Determine path & Initialize PCAP File Descriptor
const char *pcap_path = getenv("STDIO_PCAP");
if (!pcap_path || pcap_path[0] == '\0') {
pcap_path = "/tmp/stdio.pcap";
}
int pcap_fd = open(pcap_path, O_WRONLY | O_CREAT | O_APPEND, 0666);
if (pcap_fd < 0) {
perror("open pcap");
exit(1);
}
// Assess if file is empty and requires the 24-byte global PCAP header
flock(pcap_fd, LOCK_EX);
struct stat st;
if (fstat(pcap_fd, &st) == 0 && st.st_size == 0) {
struct pcap_file_header ghdr = {
.magic = 0xa1b2c3d4,
.version_major = 2,
.version_minor = 4,
.thiszone = 0,
.sigfigs = 0,
.snaplen = 65535,
.network = 1 // Data link layer: Ethernet
};
ssize_t w = write(pcap_fd, &ghdr, sizeof(ghdr));
(void)w;
}
flock(pcap_fd, LOCK_UN);
// Allocate Unix IPC standard anonymous pipes
int pipe_in[2], pipe_out[2], pipe_err[2];
if (pipe(pipe_in) < 0 || pipe(pipe_out) < 0 || pipe(pipe_err) < 0) {
perror("pipe creation");
exit(1);
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
if (pid == 0) { // == Target Process Runtime ==
dup2(pipe_in[0], STDIN_FILENO);
dup2(pipe_out[1], STDOUT_FILENO);
dup2(pipe_err[1], STDERR_FILENO);
close(pipe_in[0]); close(pipe_in[1]);
close(pipe_out[0]); close(pipe_out[1]);
close(pipe_err[0]); close(pipe_err[1]);
execve(TARGET_BINARY, argv, envp);
perror("execve");
exit(127);
}
// == Parent Wrapper Multiplexer Engine ==
close(pipe_in[0]);
close(pipe_out[1]);
close(pipe_err[1]);
// dport 3 log: Initial environment mappings
write_execve_details(pcap_fd, pid, argv, envp);
struct pollfd fds[3];
fds[0].fd = STDIN_FILENO;
fds[0].events = POLLIN;
fds[1].fd = pipe_out[0];
fds[1].events = POLLIN;
fds[2].fd = pipe_err[0];
fds[2].events = POLLIN;
int stdin_open = 1;
int stdout_open = 1;
int stderr_open = 1;
// Event loop continues while standard output/error pipes endure
while (stdout_open || stderr_open) {
int n = poll(fds, 3, -1);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
// 1. Process Parent STDIN -> Target STDIN (dport 0)
if (stdin_open && (fds[0].revents & (POLLIN | POLLHUP | POLLERR))) {
char buf[8192];
ssize_t r = read(STDIN_FILENO, buf, sizeof(buf));
if (r > 0) {
write_pcap_packet(pcap_fd, 0, pid, buf, r);
ssize_t written = 0;
while (written < r) {
ssize_t wr = write(pipe_in[1], buf + written, r - written);
if (wr < 0) {
if (errno == EINTR) continue;
break;
}
written += wr;
}
} else if (r == 0 || (r < 0 && errno != EAGAIN && errno != EINTR)) {
close(pipe_in[1]); // Close pipe mapping to target to issue EOF
fds[0].fd = -1;
stdin_open = 0;
}
}
// 2. Process Target STDOUT -> Parent STDOUT (dport 1)
if (stdout_open && (fds[1].revents & (POLLIN | POLLHUP | POLLERR))) {
char buf[8192];
ssize_t r = read(pipe_out[0], buf, sizeof(buf));
if (r > 0) {
write_pcap_packet(pcap_fd, 1, pid, buf, r);
ssize_t written = 0;
while (written < r) {
ssize_t wr = write(STDOUT_FILENO, buf + written, r - written);
if (wr < 0) {
if (errno == EINTR) continue;
break;
}
written += wr;
}
} else if (r == 0 || (r < 0 && errno != EAGAIN && errno != EINTR)) {
close(pipe_out[0]);
fds[1].fd = -1;
stdout_open = 0;
}
}
// 3. Process Target STDERR -> Parent STDERR (dport 2)
if (stderr_open && (fds[2].revents & (POLLIN | POLLHUP | POLLERR))) {
char buf[8192];
ssize_t r = read(pipe_err[0], buf, sizeof(buf));
if (r > 0) {
write_pcap_packet(pcap_fd, 2, pid, buf, r);
ssize_t written = 0;
while (written < r) {
ssize_t wr = write(STDERR_FILENO, buf + written, r - written);
if (wr < 0) {
if (errno == EINTR) continue;
break;
}
written += wr;
}
} else if (r == 0 || (r < 0 && errno != EAGAIN && errno != EINTR)) {
close(pipe_err[0]);
fds[2].fd = -1;
stderr_open = 0;
}
}
}
// Reap the child to avoid zombies and structure its status
int status;
waitpid(pid, &status, 0);
// dport 4: Yield and format WEXITSTATUS / Process Signals to an integer packet
uint32_t net_status = htonl(status);
write_pcap_packet(pcap_fd, 4, pid, &net_status, sizeof(net_status));
if (stdin_open) close(pipe_in[1]);
close(pcap_fd);
// Mimic the status so the caller processes environment gets exactly what it expects
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return 0;
}
main: main.c
cc -O2 -Wall -std=c11 -DTARGET_BINARY='"'/usr/lib/apt/methods/sqv.real'"' -o $@ $<

Implement a wrapper program under Linux according to this high-level specification:

1. Overview

The system is a transparent process wrapper designed to execute a fixed target binary. It acts as a middleman between the calling environment and the target, intercepting standard I/O (stdin, stdout, stderr), execution arguments/environment, and the final exit status. All intercepted data is encapsulated into forged Ethernet/IPv4/UDP packets and safely multiplexed into a shared PCAP file using file-level locking.

2. Configuration & Initialization

  • Environment Variable: Read STDIO_PCAP. If unset or empty, default to /tmp/stdio.pcap.
  • File Descriptor & Global Header:
    1. Open the PCAP file in O_WRONLY | O_CREAT | O_APPEND mode.
    2. Acquire an exclusive lock using flock(fd, LOCK_EX).
    3. Check the file size (e.g., via fstat).
    4. If the size is 0, write the 24-byte Global PCAP Header (Magic number 0xa1b2c3d4, version 2.4, snaplen 65535, network/linktype 1 for Ethernet).
    5. Release the lock: flock(fd, LOCK_UN).

3. Encapsulation & MTU Strategy

To adhere to the 9000 MTU constraint and ensure Wireshark parses the file natively, each captured event is encapsulated into a structure resembling a network frame.

  • Buffer Sizing: Maximum Transmission Unit (MTU) = 9000 bytes.
    • Ethernet Header: 14 bytes
    • IPv4 Header: 20 bytes
    • UDP Header: 8 bytes
    • Max Payload Size: 9000 - 14 - 20 - 8 = 8958 bytes. We further reduce it to 8192 bytes.
    • Read buffers for I/O should be allocated to exactly 8192 bytes.
  • Frame Structure (Per Write):
    1. PCAP Packet Header: 16 bytes (Epoch seconds, microseconds, captured length, original length).
    2. Fake Ethernet Header: Dummy source/dest MAC addresses, EtherType 0x0800 (IPv4).
    3. Fake IPv4 Header: Dummy source/dest IP addresses, Protocol 17 (UDP), proper length fields.
    4. Fake UDP Header:
      • Source Port (sport) = Target Process ID (PID).
      • Destination Port (dport) = Determined by the event type (0, 1, 2, 3, or 4).
      • Length and Checksum (Checksum can be 0x0000 to skip validation).
    5. Payload: The intercepted data.

4. Process & IPC Management

PTY allocation is forbidden, standard anonymous pipes should be used for IPC.

  • Create three unidirectional pipes: pipe_in, pipe_out, pipe_err.
  • Fork the process:
    • Child:
      • Duplicate (dup2) the read end of pipe_in to STDIN_FILENO.
      • Duplicate the write end of pipe_out to STDOUT_FILENO.
      • Duplicate the write end of pipe_err to STDERR_FILENO.
      • Close all unused pipe file descriptors.
      • Execute the fixed target using execve().
    • Parent (Wrapper Engine):
      • Close the child's ends of the pipes.
      • Obtain the child's PID for the UDP sport.

5. Event Types & Payload Mapping (The UDP dport Scheme)

Immediately after the fork, the parent writes the first packet, then enters the I/O loop, and finally writes the exit status.

  • dport 3 (Execve Details):

    • Trigger: Logged by the parent immediately after fork() but before entering the I/O loop.
    • Payload: Iterate through the argv array, copying each null-terminated string sequentially into the buffer. Add an extra \0 after the last argv string. Then, iterate through envp doing the same.
    • Note: If the combined argv/envp size exceeds the 8958-byte MTU payload limit, it should be split into multiple dport=3 packets.
  • dport 0 (Stdin):

    • Trigger: Parent reads from its own stdin.
    • Payload: The raw bytes read.
    • Action: Parent formats/writes the PCAP packet, then forwards the exact same bytes to the write-end of pipe_in so the child receives them.
  • dport 1 (Stdout):

    • Trigger: Parent reads from the read-end of pipe_out (child's output).
    • Payload: The raw bytes read.
    • Action: Parent formats/writes the PCAP packet, then writes the bytes to its own stdout.
  • dport 2 (Stderr):

    • Trigger: Parent reads from the read-end of pipe_err (child's errors).
    • Payload: The raw bytes read.
    • Action: Parent formats/writes the PCAP packet, then writes the bytes to its own stderr.
  • dport 4 (Exit Status):

    • Trigger: The child process terminates, and waitpid() returns.
    • Payload: A structured representation (e.g., 4-byte integer) of the exit status yielded by the wait system call macros (WEXITSTATUS, WTERMSIG, etc.).

6. Main I/O Multiplexing Loop

The parent operates an event-driven loop (using poll, select, or epoll):

  1. Monitor parent's stdin, child's stdout_pipe, and child's stderr_pipe for readability.
  2. When a file descriptor is readable, read up to 8192 bytes.
  3. Construct the full PCAP packet (PCAP Header + Eth + IP + UDP + Payload) in memory.
  4. Locking / Writing:
    • flock(pcap_fd, LOCK_EX)
    • write(pcap_fd, complete_packet_buffer, total_packet_size)
    • flock(pcap_fd, LOCK_UN)
  5. Forward the data to the appropriate destination (e.g., from parent stdin to child stdin pipe).
  6. Loop continues until all child pipes are closed (EOF) and the child process has been reaped.

7. Concurrency & Locking Guarantees

  • Because the PCAP file is opened in O_APPEND mode, standard POSIX writes are typically atomic. However, utilizing flock around every single packet write ensures that even if multiple wrappers are writing simultaneously, interleaved or fragmented packets (which would corrupt the PCAP parsing in Wireshark) are strictly prevented.
  • File locks are advisory, but since all instances of this wrapper will respect the exact same locking routine, concurrency is safely managed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment