<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Offensive Context</title><description>Simplified, powerful security tooling built from an attacker&apos;s perspective, for small and medium teams.</description><link>https://offensivecontext.com/</link><language>en-us</language><item><title>Abusing eBPF, Part 1: What even is eBPF?</title><link>https://offensivecontext.com/posts/abusing-ebpf-part-1-what-even-is-ebpf/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-ebpf-part-1-what-even-is-ebpf/</guid><description>What eBPF actually is, why it is interesting from an attacker&apos;s seat, and a first counter running in the kernel with Rust and aya.</description><pubDate>Tue, 23 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Welcome to a new series. The last one was about learning &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;DNS&lt;/a&gt; while abusing its functionality to do unintuitive things. This one is about doing fun things to the Linux kernel, with the kernel&apos;s full cooperation.&lt;/p&gt;
&lt;p&gt;We are going to abuse &lt;a href=&quot;https://en.wikipedia.org/wiki/EBPF&quot;&gt;eBPF&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A quick note before we get going. Everything in this series is built and tested on lab boxes I own. Use this stuff on systems you have permission to use it on. That&apos;s the only warning you&apos;re getting, scroll on.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What is eBPF, actually&lt;/h2&gt;
&lt;p&gt;Well that is an interesting question. The BPF part was originally &lt;strong&gt;B&lt;/strong&gt;erkley &lt;strong&gt;P&lt;/strong&gt;acket &lt;strong&gt;F&lt;/strong&gt;ilter which got extended to eBFP (guess what the e stood for). Now eBPF does much much more than simple packet filtering so they&apos;ve decided eBFP doesn&apos;t stand for &lt;a href=&quot;https://ebpf.io/what-is-ebpf/#what-do-ebpf-and-bpf-stand-for&quot;&gt;anything&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Okay so what is it? eBPF continually evolves for our purposes it is a way to write code that runs sandboxed in the kernel.&lt;/p&gt;
&lt;p&gt;The idea is straight forward. You write a tiny program. The kernel runs it through a verifier that proves your program will halt, won&apos;t read uninitialized memory, won&apos;t dereference arbitrary pointers, and won&apos;t loop forever. Once the verifier is happy, the kernel JITs your program to native instructions and attaches it to the events you request. Almost any event, a syscall, an incoming packet, a function entry, a kernel tracepoint. When that event fires, your code runs. In kernel context. Just where we want to be.&lt;/p&gt;
&lt;p&gt;The eBPF site has nice &lt;a href=&quot;https://ebpf.io/what-is-ebpf/#what-is-ebpfio&quot;&gt;diagram&lt;/a&gt; of &quot;observability&quot; and &quot;networking&quot; and &quot;security.&lt;/p&gt;
&lt;p&gt;What it leaves out, and what we are here to talk about, is that eBPF programs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;See every syscall the box makes.&lt;/li&gt;
&lt;li&gt;Can rewrite userspace memory on the way out of a syscall (&lt;code&gt;bpf_probe_write_user&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Can decide what happens to a packet before the network stack has touched it (XDP).&lt;/li&gt;
&lt;li&gt;Can redirect a connection to a socket the attacker has stashed (&lt;code&gt;sk_lookup&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Persist after the program that loaded them exits (pinning to &lt;code&gt;bpffs&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Are basically invisible to people who only know about &lt;code&gt;lsmod&lt;/code&gt;, kernel modules, and &lt;code&gt;/proc/modules&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Astute readers like yourself may notice that this is a really good list if you happen to be building a rootkit.&lt;/p&gt;
&lt;h2&gt;Why a red teamer cares&lt;/h2&gt;
&lt;p&gt;A kernel module would do most of this too. Kernel modules have problems though. Distro kernels increasingly require signed modules. Lockdown LSM blocks unsigned loads. &lt;code&gt;lsmod&lt;/code&gt; will rat you out. The build dance is a pain in the ass and version-specific. You also need a compiler toolchain on the box, or you cross-compile and hope the kernel headers match.&lt;/p&gt;
&lt;p&gt;eBPF sidesteps almost all of it. The toolchain ships with the kernel. Programs are portable across kernel versions (kind of, we&apos;ll get there). You don&apos;t need &lt;code&gt;CONFIG_MODULES&lt;/code&gt;. Lockdown affects some helpers but not the program loading itself. And &lt;code&gt;lsmod&lt;/code&gt; doesn&apos;t list a single thing.&lt;/p&gt;
&lt;p&gt;You do need &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt; (or &lt;code&gt;CAP_BPF&lt;/code&gt; on newer kernels) to load programs.&lt;/p&gt;
&lt;h2&gt;Why Rust, why aya&lt;/h2&gt;
&lt;p&gt;There are two ways to write eBPF in 2026. Either you use libbpf with C, or you use &lt;a href=&quot;https://aya-rs.dev/&quot;&gt;aya&lt;/a&gt; with Rust.&lt;/p&gt;
&lt;p&gt;I picked aya, for a few reasons that matter for offensive work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single binary&lt;/strong&gt;. aya compiles the eBPF program to an ELF object at build time and embeds it in the userspace binary with &lt;code&gt;include_bytes_aligned!&lt;/code&gt;. Eventually one static &lt;code&gt;musl&lt;/code&gt;-linked binary, no &lt;code&gt;bpftool&lt;/code&gt; on the target, no &lt;code&gt;.o&lt;/code&gt; files lying around.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Type sharing&lt;/strong&gt;. The kernel-side program and the userspace loader are both Rust crates in the same workspace. The struct your eBPF code writes into a map is &lt;em&gt;literally the same struct&lt;/em&gt; the userspace reads back. No header juggling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&apos;s Rust&lt;/strong&gt;. Typical stuff about rust, ecosystem, borrow checker, type checking yada yada. I also don&apos;t want to deal with toolchain headaches and setting all of that up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s fine if you&apos;ve never written eBPF or Rust before. We&apos;ll move slow and I&apos;ll show what does what.&lt;/p&gt;
&lt;h2&gt;Hello, kernel&lt;/h2&gt;
&lt;p&gt;Let&apos;s write a program. The goal: count every &lt;code&gt;execve&lt;/code&gt; on the box and print the count once a second. It&apos;s the eBPF equivalent of &quot;hello world,&quot; and it actually does something — &lt;code&gt;execve&lt;/code&gt; is what fires when anything launches a process, so we get to watch the box&apos;s process churn in real time.&lt;/p&gt;
&lt;p&gt;The repo lives at &lt;code&gt;code/abusing-ebpf-part-1/&lt;/code&gt; in the &lt;a href=&quot;https://github.com/offensivecontext/blog&quot;&gt;blog source&lt;/a&gt;. Clone it if you want to follow along.&lt;/p&gt;
&lt;h3&gt;The workspace&lt;/h3&gt;
&lt;p&gt;We need two crates: one that compiles to a BPF ELF and runs in the kernel, and one that compiles to a regular x86 binary and runs in userspace.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;abusing-ebpf-part-1/
├── Cargo.toml                       # workspace
├── rust-toolchain.toml              # nightly
├── execve-counter-ebpf/             # kernel side
│   ├── Cargo.toml
│   └── src/main.rs
└── execve-counter/                  # userspace side
    ├── Cargo.toml
    ├── build.rs                     # compiles the ebpf crate
    └── src/main.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The workspace &lt;code&gt;Cargo.toml&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[workspace]
resolver = &quot;2&quot;
members = [&quot;execve-counter&quot;, &quot;execve-counter-ebpf&quot;]
default-members = [&quot;execve-counter&quot;]

[workspace.dependencies]
aya = { version = &quot;0.13&quot;, default-features = false }
aya-build = { version = &quot;0.1&quot;, default-features = false }
aya-ebpf = { version = &quot;0.1&quot;, default-features = false }
anyhow = &quot;1&quot;
cargo_metadata = &quot;0.23&quot;
env_logger = &quot;0.11&quot;
libc = &quot;0.2&quot;
log = &quot;0.4&quot;
tokio = { version = &quot;1&quot;, features = [&quot;macros&quot;, &quot;rt-multi-thread&quot;, &quot;signal&quot;, &quot;time&quot;] }

[profile.release]
lto = true
strip = true
codegen-units = 1
panic = &quot;abort&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;default-members = [&quot;execve-counter&quot;]&lt;/code&gt; matters. The eBPF crate doesn&apos;t build for your host target — it builds for &lt;code&gt;bpfel-unknown-none&lt;/code&gt;. If you &lt;code&gt;cargo build&lt;/code&gt; from the workspace root without setting a default, cargo gets confused. We let &lt;code&gt;build.rs&lt;/code&gt; in the userspace crate drive the eBPF build.&lt;/p&gt;
&lt;p&gt;You also need nightly Rust and &lt;code&gt;bpf-linker&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustup install nightly
cargo install bpf-linker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;rust-toolchain.toml&lt;/code&gt; in the repo pins all that, so once you &lt;code&gt;cd&lt;/code&gt; in, you&apos;re set.&lt;/p&gt;
&lt;h3&gt;The eBPF side&lt;/h3&gt;
&lt;p&gt;Here the eBPF crate&apos;s &lt;code&gt;Cargo.toml&lt;/code&gt; names the binary &lt;code&gt;counter-bpf&lt;/code&gt;. This fixes a naming collision, if we name the bin the same thing as the package, &lt;code&gt;aya-build&lt;/code&gt; ends up trying to copy the compiled BPF object on top of its own intermediate target directory and you get a confusing &quot;Is a directory&quot; error.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;execve-counter-ebpf/Cargo.toml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package]
name = &quot;execve-counter-ebpf&quot;
version = &quot;0.1.0&quot;
edition = &quot;2021&quot;
publish = false

[dependencies]
aya-ebpf = { workspace = true }

[[bin]]
name = &quot;counter-bpf&quot;
path = &quot;src/main.rs&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;execve-counter-ebpf/src/main.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use aya_ebpf::{
    macros::{map, tracepoint},
    maps::Array,
    programs::TracePointContext,
};

#[map]
static COUNT: Array&amp;lt;u64&amp;gt; = Array::with_max_entries(1, 0);

#[tracepoint(category = &quot;syscalls&quot;, name = &quot;sys_enter_execve&quot;)]
pub fn on_execve(_ctx: TracePointContext) -&amp;gt; u32 {
    if let Some(ptr) = COUNT.get_ptr_mut(0) {
        unsafe { *ptr += 1 };
    }
    0
}

#[panic_handler]
fn panic(_info: &amp;amp;core::panic::PanicInfo) -&amp;gt; ! {
    unsafe { core::hint::unreachable_unchecked() }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s the whole thing. Let&apos;s break it down.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#![no_std]&lt;/code&gt; and &lt;code&gt;#![no_main]&lt;/code&gt; — there&apos;s no standard library in the eBPF runtime. No &lt;code&gt;std::println!&lt;/code&gt;, no allocator, no threads. Anything you want has to come from &lt;code&gt;aya_ebpf&lt;/code&gt; or &lt;code&gt;core&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;#[map]&lt;/code&gt; attribute declares an eBPF map. Maps are the universal communication primitive the kernel side reads and writes them, the userspace side reads and writes them, and the kernel makes sure neither side blows up the other. Our map is a single 64-bit counter &lt;code&gt;Array&amp;lt;u64&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;#[tracepoint(category = &quot;syscalls&quot;, name = &quot;sys_enter_execve&quot;)]&lt;/code&gt; attribute is the interesting bit. It tells aya &quot;this function should run every time the &lt;code&gt;sys_enter_execve&lt;/code&gt; tracepoint fires.&quot; Tracepoints are stable hook points in the kernel, they don&apos;t move between kernel versions the way function symbols do, which makes them ideal for portability.&lt;/p&gt;
&lt;p&gt;The function body does the simplest possible thing: grab a pointer to slot 0 of the map, add one. The &lt;code&gt;unsafe&lt;/code&gt; block is required because &lt;code&gt;*ptr += 1&lt;/code&gt; is not atomic — two CPUs incrementing at once will lose updates. For a real counter you&apos;d use a &lt;code&gt;PerCpuArray&lt;/code&gt; and sum at read time, or use atomic helpers. We are deliberately keeping it minimal. If your dev box does 200 execs/second and we lose three, this post will still work.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;#[panic_handler]&lt;/code&gt; is required boilerplate. eBPF programs can&apos;t actually panic, the verifier won&apos;t let them, but Rust insists you provide a handler. We give it &lt;code&gt;unreachable_unchecked()&lt;/code&gt;, which compiles to nothing.&lt;/p&gt;
&lt;h3&gt;The userspace side&lt;/h3&gt;
&lt;p&gt;The userspace side has a few jobs. Build the eBPF crate at compile time and embed the result. At runtime, load that bytecode into the kernel, attach the program, and poll the map.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;execve-counter/build.rs&lt;/code&gt; handles the build-time half:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use anyhow::{anyhow, Context as _};
use aya_build::{Package, Toolchain};

fn main() -&amp;gt; anyhow::Result&amp;lt;()&amp;gt; {
    let metadata = cargo_metadata::MetadataCommand::new()
        .no_deps()
        .exec()
        .context(&quot;cargo metadata&quot;)?;

    let ebpf = metadata
        .packages
        .into_iter()
        .find(|p| p.name.as_str() == &quot;execve-counter-ebpf&quot;)
        .ok_or_else(|| anyhow!(&quot;execve-counter-ebpf package not found&quot;))?;

    let root_dir = ebpf
        .manifest_path
        .parent()
        .ok_or_else(|| anyhow!(&quot;no parent for {}&quot;, ebpf.manifest_path))?;

    let package = Package {
        name: ebpf.name.as_str(),
        root_dir: root_dir.as_str(),
        ..Default::default()
    };

    aya_build::build_ebpf([package], Toolchain::default())?;
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cargo_metadata&lt;/code&gt; runs &lt;code&gt;cargo metadata&lt;/code&gt; for the workspace and gives us back the packages. We grab the one we care about, build an
&lt;code&gt;aya_build::Package&lt;/code&gt; pointing at its directory, and hand it to &lt;code&gt;aya_build::build_ebpf&lt;/code&gt; along with a default toolchain. aya-buil
d shells out to nightly cargo with the BPF target and linker, produces an ELF, and drops it in &lt;code&gt;OUT_DIR&lt;/code&gt;. We pick it up in &lt;code&gt;main .rs&lt;/code&gt; with &lt;code&gt;include_bytes_aligned!&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You&apos;ll need build-dependencies for this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build-dependencies]
anyhow = { workspace = true }
aya-build = { workspace = true }
cargo_metadata = { workspace = true }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;execve-counter/src/main.rs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::time::Duration;

use aya::{maps::Array, programs::TracePoint, Ebpf};
use log::{info, warn};

const EBPF_OBJ: &amp;amp;[u8] =
    aya::include_bytes_aligned!(concat!(env!(&quot;OUT_DIR&quot;), &quot;/counter-ebpf&quot;));

#[tokio::main]
async fn main() -&amp;gt; anyhow::Result&amp;lt;()&amp;gt; {
    env_logger::init();
    bump_memlock_rlimit();

    let mut ebpf = Ebpf::load(EBPF_OBJ)?;

    let program: &amp;amp;mut TracePoint = ebpf
        .program_mut(&quot;on_execve&quot;)
        .ok_or_else(|| anyhow::anyhow!(&quot;program not found&quot;))?
        .try_into()?;
    program.load()?;
    program.attach(&quot;syscalls&quot;, &quot;sys_enter_execve&quot;)?;
    info!(&quot;attached. counting execve calls — Ctrl+C to stop.&quot;);

    let map = Array::&amp;lt;_, u64&amp;gt;::try_from(
        ebpf.map(&quot;COUNT&quot;).ok_or_else(|| anyhow::anyhow!(&quot;map missing&quot;))?,
    )?;

    let mut ticker = tokio::time::interval(Duration::from_secs(1));
    loop {
        tokio::select! {
            _ = ticker.tick() =&amp;gt; {
                let n = map.get(&amp;amp;0, 0).unwrap_or(0);
                info!(&quot;execve count = {n}&quot;);
            }
            _ = tokio::signal::ctrl_c() =&amp;gt; {
                info!(&quot;shutting down.&quot;);
                break;
            }
        }
    }
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The shape: &lt;code&gt;Ebpf::load&lt;/code&gt; parses the ELF, finds the programs and maps. &lt;code&gt;program_mut(&quot;on_execve&quot;)&lt;/code&gt; grabs our tracepoint by name. &lt;code&gt;load()&lt;/code&gt; runs the verifier (this is the moment of truth on real programs — your program either passes or you have a bad day). &lt;code&gt;attach(&quot;syscalls&quot;, &quot;sys_enter_execve&quot;)&lt;/code&gt; hooks it up to the tracepoint.&lt;/p&gt;
&lt;p&gt;After that, we wrap the &lt;code&gt;COUNT&lt;/code&gt; map as an &lt;code&gt;Array&amp;lt;_, u64&amp;gt;&lt;/code&gt; and read slot 0 once a second. The actual &lt;code&gt;*ptr += 1&lt;/code&gt; is happening in the kernel on every exec; we&apos;re just reading the result.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bump_memlock_rlimit&lt;/code&gt; is one of those rite-of-passage details. eBPF maps used to count against the calling process&apos;s &lt;code&gt;RLIMIT_MEMLOCK&lt;/code&gt;, and the default rlimit is usually 64KB which is nothing. Newer kernels use a different accounting model and this is no longer strictly required, but it&apos;s still polite. The function in the repo just calls &lt;code&gt;setrlimit&lt;/code&gt; to infinity.&lt;/p&gt;
&lt;h3&gt;Run it&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cargo build --release
sudo RUST_LOG=info ./target/release/execve-counter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO  execve_counter] attached. counting execve calls — Ctrl+C to stop.
[INFO  execve_counter] execve count = 0
[INFO  execve_counter] execve count = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now open another terminal and run something. Anything.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls
cat /etc/hostname
bash -c &apos;for i in $(seq 1 100); do true; done&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Watch the first terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO  execve_counter] execve count = 4
[INFO  execve_counter] execve count = 5
[INFO  execve_counter] execve count = 107
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hello, kernel.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you get a &quot;permission denied&quot; or a verifier error on load, you probably aren&apos;t root. eBPF needs &lt;code&gt;CAP_BPF&lt;/code&gt; (newer kernels) or &lt;code&gt;CAP_SYS_ADMIN&lt;/code&gt;. &lt;code&gt;sudo&lt;/code&gt; is the easy path.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What we just did&lt;/h2&gt;
&lt;p&gt;That tiny program is doing something genuinely surprising if you stop and look at it. We compiled a Rust function to a custom bytecode, the kernel verified it terminates and is memory-safe, JITed it to native, and started running it inside the kernel on every &lt;code&gt;execve&lt;/code&gt; syscall on the box. We did not load a module. We did not patch a kernel function. We did not even need root in a really privileged sense — &lt;code&gt;CAP_BPF&lt;/code&gt; alone gets you here.&lt;/p&gt;
&lt;p&gt;Now imagine that instead of incrementing a counter, your tracepoint handler walked the syscall&apos;s arguments, decided that this &lt;code&gt;execve&lt;/code&gt; was running &lt;code&gt;ps&lt;/code&gt;, and rewrote the buffer it would return so that certain PIDs vanished.&lt;/p&gt;
&lt;p&gt;That&apos;s where we&apos;re going. Next post: we hide PIDs from &lt;code&gt;ls /proc&lt;/code&gt; by hooking &lt;code&gt;getdents64&lt;/code&gt; and rewriting the directory entries. &lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, and &lt;code&gt;pgrep&lt;/code&gt; all lose their minds. It is very fun.&lt;/p&gt;
&lt;p&gt;Until then, the code is here &lt;a href=&quot;https://github.com/syndrowm/abusing-ebfp&quot;&gt;https://github.com/syndrowm/abusing-ebfp&lt;/a&gt;. Ping me on &lt;a href=&quot;https://x.com/syndrowm&quot;&gt;X&lt;/a&gt;or &lt;a href=&quot;https://www.linkedin.com/in/syndrowm/&quot;&gt;LinkedIn&lt;/a&gt; if I messed something up, and subscribe if you want Part 2 in your inbox when it drops.&lt;/p&gt;
</content:encoded><category>ebpf</category><category>linux</category><category>rust</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 7: Who is in charge here?</title><link>https://offensivecontext.com/posts/abusing-dns-part-7-who-is-in-charge-here/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-7-who-is-in-charge-here/</guid><description>The NS records that make a subdomain yours, and the delegation that lets any resolver on the internet find your server.</description><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;How does your DNS resolver know how to find the DNS server responsible for resolving your DNS address? Why DNS of course. Pretty simple actually, the DNS client asks the DNS resolver for the address the resolver asks its own DNS resolver  who asks the DNS server who to ask for the DNS address.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-7-who-is-in-charge-here/image.webp&quot; alt=&quot;Still from Inception of two characters talking across a table in a dim bar&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Ask the DNS server for the DNS address of the DNS server to ask for a DNS address&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is where the &lt;code&gt;NS&lt;/code&gt;  or &lt;code&gt;Name Server&lt;/code&gt; record comes in. The &lt;code&gt;NS&lt;/code&gt; record is used to identify the &lt;code&gt;authoritative&lt;/code&gt; name server for the domain. AKA who is actually in charge.&lt;/p&gt;
&lt;p&gt;For this example let&apos;s setup a sub domain of &lt;code&gt;offensivecontext.com&lt;/code&gt; so that any request for that sub domain will be forwarded to our DNS server. Lets be very creative with the naming and use &lt;code&gt;sub.offensivecontext.com&lt;/code&gt; as our sub domain. Meaning we want any query to &lt;code&gt;*.sub.offensivecontext.com&lt;/code&gt; to hit our server.&lt;/p&gt;
&lt;p&gt;We need two records to setup this up, a &lt;code&gt;NS&lt;/code&gt; record and an &lt;code&gt;A&lt;/code&gt; record. For example if I wanted all requests to be forwarded to &lt;code&gt;1.2.3.4&lt;/code&gt; I would setup the following.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NS&lt;/td&gt;
&lt;td&gt;sub.offensivecontext.com&lt;/td&gt;
&lt;td&gt;namserver.offensivecontext.com&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;nameserver.offensivecontext.com&lt;/td&gt;
&lt;td&gt;1.2.3.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Visually that would look like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-7-who-is-in-charge-here/screenshot-2025-04-03-at-9-30-44-am.webp&quot; alt=&quot;Diagram of the delegation records: an NS record for sub.offensivecontext.com points at nameserver.offensivecontext.com, whose A record points at 1.2.3.4&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Specifics of setting this up is going to depend on your domain registrar, once you have a the above records configured, any of the query types for &lt;code&gt;*.sub.offensivecontext.com&lt;/code&gt; will be forwarded to my server at &lt;code&gt;1.2.3.4&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;Getting our system wired into DNS is pretty straight forward, having this setup is going to add some new complications. Next week we will look at adding some more  processing in order to deduplicate requests and make the tooling more robust.&lt;/p&gt;
</content:encoded><category>dns</category><category>networking</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 6: It&apos;s all about who you know</title><link>https://offensivecontext.com/posts/abusing-dns-part-6-2/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-6-2/</guid><description>Routing queries through the host&apos;s own resolver instead of straight at our server, so the traffic looks like everyone else&apos;s.</description><pubDate>Thu, 27 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;One of the benefits we saw for using DNS in &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part 1&lt;/a&gt; was the ability to bounce our traffic through different systems that we don&apos;t control. Up until now we have been going directly to the DNS server we control.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-6-2/image-10.webp&quot; alt=&quot;Hand-drawn diagram of the host querying our DNS server directly, which reads key/value pairs from a database&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Talking directly to our server&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When your host computers doesn&apos;t know how to resolve a DNS address it will ask the DNS resolver it has been configured to use. This resolver is often called a &lt;code&gt;nameserver&lt;/code&gt;.  That &lt;code&gt;nameserver&lt;/code&gt; knows who to ask if it doesn&apos;t know the answer and so on until the question gets to your server. What we really want is something like this.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-6-2/screenshot-2025-03-26-at-10-52-43-am.webp&quot; alt=&quot;Hand-drawn diagram of a key request relayed from the host through 192.168.0.1 and 1.1.1.1 before reaching our server, with the value returned back down the chain&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What we really want&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Awesome we just need to ask the &lt;code&gt;nameserver&lt;/code&gt; So far we have used &lt;code&gt;localhost&lt;/code&gt; Okay sooo... we just give the client the &lt;code&gt;nameservers&lt;/code&gt; IP...? That is kind of lame, you don&apos;t specify an IP in your browser, and &lt;code&gt;dig&lt;/code&gt; doesn&apos;t take an IP by default. Turns out your system is configured with a &lt;code&gt;nameserver&lt;/code&gt;  look at the network configuration file and we are good to go right... right&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-6-2/screenshot-2025-03-26-at-10-21-34-am-1.webp&quot; alt=&quot;Anakin and Padmé four-panel meme: &apos;Find the system resolver&apos; / &apos;Read the config file?&apos; / Anakin stares back with a Windows logo over his face / &apos;Read the config file right?&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Windows is a special case&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Not so fast my dear reader. While finding the configured DNS resolver for most unix systems is relatively straight forward, to have some real fun we are going to be making our client support both linux and windows. Thats right this post is actually about cross platform support and conditional compilation! 😎&lt;/p&gt;
&lt;p&gt;Lets take advantage of some more rust features. Thankfully the rust compiler &lt;code&gt;rustc&lt;/code&gt; supports multiple &lt;a href=&quot;https://doc.rust-lang.org/nightly/rustc/platform-support.html&quot;&gt;platforms&lt;/a&gt; and it lets you &lt;a href=&quot;https://doc.rust-lang.org/nightly/rustc/check-cfg.html&quot;&gt;configure&lt;/a&gt; what your code does based on options at compile time.&lt;/p&gt;
&lt;h2&gt;Linux&lt;/h2&gt;
&lt;p&gt;Linux and most unixes have been nice enough to standardize on &lt;code&gt;/etc/resolv.conf&lt;/code&gt; as a place to find the systems configured resolver. Lets parse &lt;code&gt;resolv.conf&lt;/code&gt; and find the name server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(target_os = &quot;linux&quot;)]
fn find_dns_resolvers() -&amp;gt; Result&amp;lt;Vec&amp;lt;String&amp;gt;&amp;gt; {
    let mut servers = vec![];

    // Read /etc/resolv.conf to get DNS servers
    if let Ok(content) = std::fs::read_to_string(&quot;/etc/resolv.conf&quot;) {
        for line in content.lines() {
            let line = line.trim();
            if line.starts_with(&quot;nameserver&quot;) {
                if let Some(entry) = line.split_whitespace().nth(1) {
                    servers.push(format!(&quot;{}:53&quot;, entry));
                }
            }
        }
    }
    Ok(servers)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;find the resolvers for linux&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The magic here is the very first line &lt;code&gt;#[cfg(target_os = &quot;linux&quot;)]&lt;/code&gt; The comThis tells the rust compiler to build this version of &lt;code&gt;find_dns_resolvers&lt;/code&gt; when building for linux.&lt;/p&gt;
&lt;h2&gt;Windows&lt;/h2&gt;
&lt;p&gt;Now we just need to write the same function for windows ensure the function name and return types are the same and we should be good to go.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(target_os = &quot;windows&quot;)]
fn find_dns_resolvers(domain: String) -&amp;gt; Result&amp;lt;String&amp;gt; {
    let adapters = ipconfig::get_adapters()?;
    let mut servers = vec![];

    for dns_server in adapters
        .iter()
        .flat_map(|adapter| adapter.dns_servers().iter())
    {
        servers.push(format!(&quot;{}:53&quot;, dns_server));
    }
    Ok(servers)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we use the &lt;a href=&quot;https://github.com/liranringel/ipconfig&quot;&gt;ipconfig&lt;/a&gt; crate which makes it very simple to find the DNS server  for each of the host systems network adapters. To use ipconfig, we just need to add a conditional dependency to our &lt;code&gt;Cargo.toml&lt;/code&gt; and we are all set.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[target.&apos;cfg(windows)&apos;.dependencies]
ipconfig = &quot;0.3.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Call it&lt;/h2&gt;
&lt;p&gt;Finally we need to modify the code to call our new function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let server = match matches.get_one::&amp;lt;String&amp;gt;(&quot;server&quot;) {
    Some(s) =&amp;gt; s,
    None =&amp;gt; {
        let resolvers = find_dns_resolvers()?;
        if resolvers.is_empty() {
            return Err(&quot;Unable to find nameserver!&quot;.into());
        } else {
            &amp;amp;resolvers[0].to_owned()
        }
    }
};
tracing::debug!(&quot;found: {server}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While verifying the command line arguments instead of having the default DNS server be &lt;code&gt;127.0.0.1&lt;/code&gt; we will now call out to &lt;code&gt;find_dns_resolvers&lt;/code&gt; in order to lookup what the client system is configured to use as a resolver. When you compile this code for linux you get a version of the binary that looks at &lt;code&gt;/etc/resolv.conf&lt;/code&gt; and when you compile it for windows you get the version that looks at what is configured for network adapters.&lt;/p&gt;
&lt;h2&gt;Wrap up&lt;/h2&gt;
&lt;p&gt;There we have it folks, another one. You may have noticed I didn&apos;t cover actually compiling for linux and windows. You can either compile the client on both a linux and a windows machine... or you can do what is called cross compiling (let me know if I should cover cross compiling). Take note, this version will blow up after finding your DNS server but that is okay, next week we will look at how to get DNS traffic to actually route to our DNS server on the internet.&lt;/p&gt;
</content:encoded><category>dns</category><category>rust</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 5: Client says what?</title><link>https://offensivecontext.com/posts/abusing-dns-part-5-client-says-what/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-5-client-says-what/</guid><description>Sending arbitrary data the other way, packed into the 63 characters a single DNS label will carry.</description><pubDate>Thu, 20 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Now that our client has the ability to read arbitrary data from the server, lets update the client to be able it to send arbitrary data to the sever. (Psssst, if you are new start with &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part1&lt;/a&gt;)&lt;/p&gt;
&lt;h2&gt;The problem&lt;/h2&gt;
&lt;p&gt;As we learned in &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-4-let-the-fun-begin/&quot;&gt;Part4&lt;/a&gt; DNS can only transfer very small chunks of data mostly from the server to the client. Worse yet there is nothing like a &lt;code&gt;TXT&lt;/code&gt; record that lets us send bigger records. There is good news, we can ask the server about any domain name we would like.&lt;/p&gt;
&lt;h3&gt;Domain names&lt;/h3&gt;
&lt;p&gt;In DNS the maximum length of a domain is 253 characters. That is a good chunk of data, but there is another limitation. A domain looks something like this &lt;code&gt;www.google.com&lt;/code&gt;. Each parts in between the &lt;code&gt;.&lt;/code&gt;&apos;s are called &lt;code&gt;labels&lt;/code&gt;. Sadly, if we want to get to something works on the wider internet, we don&apos;t control all of the labels. For our purposes we are going to use two labels (more on this below). Which leaves us a maximum of 63 characters to work with. There is one more problem but we will talk about that soon.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-5-client-says-what/image-6.webp&quot; alt=&quot;Jackie Chan confused-face meme captioned &apos;Wait what????&apos;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The answer&lt;/h2&gt;
&lt;p&gt;Once again we are going to take advantage of being able to ask as many questions as we want, and this time we are also going to take advantage of being able to ask multiple types of questions. To send data we are going to encode our data and chunk it in &lt;code&gt;AAAA&lt;/code&gt; record queries until we are done. Once the data is all sent we will send an &lt;code&gt;A&lt;/code&gt; record query to let the server know we are done. All the server needs to do is append anything it gets as an &lt;code&gt;AAAA&lt;/code&gt; record to the value and when it gets the &lt;code&gt;A&lt;/code&gt; query it knows the value is ready.&lt;/p&gt;
&lt;p&gt;Wait a minute, we only have 63 bytes of data to work with, how can we send an arbitrary key &lt;em&gt;and&lt;/em&gt; value? If we just send &lt;code&gt;AAAA&lt;/code&gt; records how will the server know what key to append the value to?&lt;/p&gt;
&lt;p&gt;Well, smarty pants, I&apos;m glad you asked. We are going take advantage of labels to make a &lt;code&gt;session&lt;/code&gt;. Our DNS requests will be for a domain with two labels that looks like this: &lt;code&gt;{ENCODED_DATA}.{SESSION_ID}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Then all we have to do is encode the key and the value into the data and we are good to go. Thankfully rust&apos;s type system and encoding is here to save us.&lt;/p&gt;
&lt;h2&gt;The answer redux&lt;/h2&gt;
&lt;p&gt;Okay that was a little hard to write, I have to imagine it is pretty hard to read. To rephrase it we are going to update our strategy as follows.&lt;/p&gt;
&lt;h3&gt;Client&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Pick a random &lt;code&gt;u16&lt;/code&gt; value as our &lt;code&gt;session id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Encode our data and send &lt;code&gt;AAAA&lt;/code&gt; queries with a domain made of chunks of data&lt;/li&gt;
&lt;li&gt;Send a &lt;code&gt;A&lt;/code&gt; query for the &lt;code&gt;session id&lt;/code&gt; when we are done&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Server&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Append all &lt;code&gt;AAAA&lt;/code&gt; requests to the &lt;code&gt;session&lt;/code&gt; value&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;A&lt;/code&gt; request means the client is done. Decode the message in the &lt;code&gt;session&lt;/code&gt; and save the resulting message under the defined key.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Code updates&lt;/h2&gt;
&lt;p&gt;Okay up first, lets update the &lt;code&gt;Message&lt;/code&gt; structure to have both the key and the value. In &lt;code&gt;src/lib.rs&lt;/code&gt; update as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Message {
  key: String,
  value: String
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Structures do stuff&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Client Updates&lt;/h2&gt;
&lt;p&gt;Now on the &lt;code&gt;src/client.rs &lt;/code&gt;... we are going to need to update the CLI parsing to take a new &lt;code&gt;--set&lt;/code&gt; value which can parse the &lt;code&gt;KEY&lt;/code&gt; and &lt;code&gt;VALUE&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
.arg(
    Arg::new(&quot;set&quot;)
        .long(&quot;set&quot;)
        .num_args(2)
        .value_names([&quot;KEY&quot;, &quot;VALUE&quot;])
        .help(&quot;Set the KEY to VALUE&quot;),
)
.get_matches();
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;argument parsing&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client --set SETME &quot;This is the value to set&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;now our cli client can do that ^&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Have I mentioned that &lt;a href=&quot;https://docs.rs/clap/latest/clap/&quot;&gt;Clap&lt;/a&gt; makes parsing arguments pretty easy? Now let&apos;s define a new function.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn set_value(key: String, value: String){
 todo!()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Our new function&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;set_value&lt;/code&gt; function will take the key/value that the user wants to set and handle most of the logic to send data. Up first...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let id: u16 = rand::rng().random();
let domain = format!(&quot;.{id:x}&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;create a session id&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here &lt;code&gt;set_value&lt;/code&gt; makes up a random session value and it does that using &lt;a href=&quot;https://docs.rs/rand/latest/rand/&quot;&gt;rand&lt;/a&gt;. It will use the hex representation of the &lt;code&gt;session&lt;/code&gt; as the domain. Remember the request will look something like &lt;code&gt;{ENCODED_DATA}.{SESSION_ID}&lt;/code&gt; .&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; let message = Message {
     key: key.to_string(),
     value: value.to_string(),
 };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;create our simple message&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Then &lt;code&gt;set_value&lt;/code&gt; creates a new message with  the key/value from the user. Followed by..&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let blob = bincode::serialize(&amp;amp;message)?;
let encoded = BASE32_NOPAD.encode(&amp;amp;blob);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;serialize/encode the message&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Once again rust&apos;s ability to serialize and encode the messages to the rescue. In those simple two lines the &lt;code&gt;Message&lt;/code&gt; structure is converted to a binary blog and then encoded in DNS domain safe BASE32, perfect for the next step. With the encoded data we can now chunk it up.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-5-client-says-what/image-7.webp&quot; alt=&quot;Chunk from The Goonies pulling a face behind a picket fence&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Chunk!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Okay not that chunk, we need to break the data up in to blocks of data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for chunk in encoded.as_bytes().chunks(MAX_LABEL) {
    let chunk = String::from_utf8(chunk.to_vec()).unwrap();
    let fqdn = format!(&quot;{}{}&quot;, chunk, domain);
    let query = aaaa_query_record(&amp;amp;fqdn)?;
    sock.send_to(&amp;amp;query, server).await?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;chunk it up and send it off&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here &lt;code&gt;set_value&lt;/code&gt; converts the encoded ASCII back into bytes with &lt;a href=&quot;https://doc.rust-lang.org/std/string/struct.String.html#method.as_bytes&quot;&gt;&lt;code&gt;as_bytes&lt;/code&gt;&lt;/a&gt;, &lt;a href=&quot;https://doc.rust-lang.org/std/slice/struct.Chunks.html&quot;&gt;chunks&lt;/a&gt; those bytes into fun size pieces ready to send to the server as &lt;code&gt;AAAA&lt;/code&gt; records. The chunk size is the &lt;code&gt;MAX_LABEL&lt;/code&gt; accounting for the maximum label length of a DNS request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let query = a_query_record(&amp;amp;format!(&quot;{id:x}&quot;))?;
sock.send_to(&amp;amp;query, server).await?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;let the server know the client is done&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally &lt;code&gt;set_value&lt;/code&gt; will send an &lt;code&gt;A&lt;/code&gt; query to the server for the just the session id letting the server know that message is done.&lt;/p&gt;
&lt;h2&gt;Server Updates&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;fn append_value(key: String, value: String) {
    let mut db = DATABASE
        .get()
        .expect(&quot;Database not initialized&quot;)
        .lock()
        .expect(&quot;Failed to lock database&quot;);
    let mut current_value = db.remove(&amp;amp;key).unwrap_or_default();
    current_value.push_str(&amp;amp;value);
    tracing::debug!(&quot;{} {}&quot;, key.clone(), current_value.clone());
    db.insert(key, current_value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;append values&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;append_value&lt;/code&gt; function takes a key and value. It appends whatever is passed as &lt;code&gt;value&lt;/code&gt; to the &lt;code&gt;value&lt;/code&gt; already in the database. Which makes the rest of the changes pretty simple. In our &lt;code&gt;AAAA&lt;/code&gt; parser we add:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
let name = q.qname.to_string().to_uppercase();
let parts: Vec&amp;lt;&amp;amp;str&amp;gt; = name.split(&quot;.&quot;).collect();
if parts.len() &amp;lt; 2 {
    tracing::debug!(&quot;Not enough parts!&quot;);
    // TODO: handle some errors
    return Err(&quot;That did not work&quot;.into());
}

append_value(parts[1].to_string(), parts[0].to_string());
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This takes the query name, splits it by the &lt;code&gt;.&lt;/code&gt; and uses the two values as arguments to the &lt;code&gt;append_value&lt;/code&gt; function. Take note that the query looks like this &lt;code&gt;VALUE.KEY&lt;/code&gt; and the split is 0 indexed (the only way I don&apos;t care what lua says) so &lt;code&gt;part[1]&lt;/code&gt; is passed as the key and &lt;code&gt;part[0]&lt;/code&gt; is passed as the value.&lt;/p&gt;
&lt;p&gt;Now we just need to update &lt;code&gt;parse_a_query&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
let key = q.qname.to_string().to_uppercase();
tracing::info!(&quot;new a query: {key}&quot;);
let value = get_value(&amp;amp;key).unwrap();

let decoded = BASE32_NOPAD.decode(value.as_bytes())?;
let msg: Message = bincode::deserialize(&amp;amp;decoded)?;

set_value(msg.key.clone().to_uppsercase(), value);
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server knows when it receives the &lt;code&gt;A&lt;/code&gt; query the client is finished... Which means it can take the session from the query name, decode the data and store the value by the actual key name.&lt;/p&gt;
&lt;p&gt;There is one gotcha to call out. You may have noticed when we lookup the query name we uppercase the value. That is because DNS is case &lt;a href=&quot;https://en.wikipedia.org/wiki/Case_sensitivity&quot;&gt;insensitive&lt;/a&gt; and queries will often be dorked with by the DNS server chains. You may ask for &lt;code&gt;whatthef.example.com&lt;/code&gt; but by the time it gets to the &lt;code&gt;example.com&lt;/code&gt; servers the messages is something like &lt;code&gt;wHaTTheF.eXamPle.com&lt;/code&gt;. That is one of the other reasons we are using &lt;a href=&quot;https://en.wikipedia.org/wiki/Base32&quot;&gt;&lt;code&gt;BASE32_NOPAD&lt;/code&gt;&lt;/a&gt; to encode the data.&lt;/p&gt;
&lt;h2&gt;Have Fun!&lt;/h2&gt;
&lt;p&gt;That is it. We can now send arbitrary key/value data to the DNS server and retrieve it with the client! As with the other parts run &lt;code&gt;cargo run --bin server&lt;/code&gt; in one terminal and we can have fun with the client in another terminal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-5-client-says-what/screenshot-2025-03-19-at-2-25-57-pm.webp&quot; alt=&quot;Terminal session setting the key SUBSCRIBE to &apos;See protocols can be a lot of fun.&apos; and reading the same value back with --get&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now in the client run&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# set the value
cargo run -q --bin client -- --set SUBSCRIBE &quot;See protocols can be a lot of fun.&quot;

# get the value
cargo run -q --bin client -- --get SUBSCRIBE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Thanks for reading. If you are enjoying this please share, subscribe, yell, scream laugh. And of course I&apos;m on &lt;a href=&quot;https://bsky.app/profile/syndrowm.com&quot;&gt;bluesky&lt;/a&gt; and &lt;a href=&quot;https://www.linkedin.com/in/syndrowm/&quot;&gt;linkedin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In the next installment we will finally set this thing up on the internet and get recursive queries happening. See you then.&lt;/p&gt;
</content:encoded><category>dns</category><category>rust</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 4: Let the fun begin?</title><link>https://offensivecontext.com/posts/abusing-dns-part-4-let-the-fun-begin/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-4-let-the-fun-begin/</guid><description>Getting real data out of a protocol built for tiny answers, by chunking it across repeated TXT queries.</description><pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Hello again friends. This is exciting we are starting to actually get somewhere.... Let&apos;s get after it, first a quick recap. Remember we are building a key/value store with DNS as our communication mechanism to explore how hackers get data out of networks. &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part 1&lt;/a&gt; was an intro to the concepts we are going to use, &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-2-2/&quot;&gt;Part 2&lt;/a&gt; we built a basic server, &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-3-client/&quot;&gt;Part 3&lt;/a&gt; we built a simple client. Oh and the code is &lt;a href=&quot;https://github.com/syndrowm/dns-kv/tree/part4&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For this post we are going to transfer data. This might sound pretty easy as DNS is designed to answer questions with data... Not so fast, DNS is designed to answer questions, short questions and the original DNS RFC (&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc883&quot;&gt;883&lt;/a&gt;) is from 1983 not a lot of data back then. Which technically makes DNS a millennial.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-4-let-the-fun-begin/image-5.webp&quot; alt=&quot;A young Bill Gates reclining across a desk beside 1980s PCs running Microsoft software&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Data was different back then&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Getting Data&lt;/h2&gt;
&lt;p&gt;This is part relatively easy. DNS has a very handy &lt;code&gt;TXT&lt;/code&gt; record (hey the client we built was asking for those records?!?!). Sadly there are some limitations most notably, you can only return text annnd you are limited to 255 characters segments. What if I wanted to get the contents of &lt;code&gt;/etc/passwd&lt;/code&gt; for... reasons. I don&apos;t have to tell you that 255 characters is not a lot of room. Thankfully we have roughly unlimited times to ask.&lt;/p&gt;
&lt;p&gt;There are a few ways to accomplish this, for this example we will us an &lt;code&gt;A&lt;/code&gt; request tell the DNS server what &lt;code&gt;TXT&lt;/code&gt; record we are going to query and then query that &lt;code&gt;TXT&lt;/code&gt; record repeatedly until we have all the data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-4-let-the-fun-begin/screenshot-2025-03-12-at-8-53-15-pm.webp&quot; alt=&quot;Hand-drawn diagram: the host sends an A query naming a key, then queries TXT records repeatedly to pull the data back, while the server reads key/value pairs from a database&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Server changes&lt;/h2&gt;
&lt;p&gt;First we need a database to store the key we are going to be looking for. Here we are using the &lt;a href=&quot;https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html&quot;&gt;OnceLock&lt;/a&gt; struct to make a global &lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.Mutex.html&quot;&gt;Mutex&lt;/a&gt; locked &lt;a href=&quot;https://doc.rust-lang.org/stable/std/collections/struct.HashMap.html&quot;&gt;HashMap&lt;/a&gt;.  Which is a really fancy way to store a key and a value in a memory and thread safe way.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Database = HashMap&amp;lt;String, String&amp;gt;;

static DATABASE: OnceLock&amp;lt;Mutex&amp;lt;Database&amp;gt;&amp;gt; = OnceLock::new();

fn get_value(key: &amp;amp;String) -&amp;gt; Option&amp;lt;String&amp;gt; {
    let mut db = DATABASE
        .get()
        .expect(&quot;Database not initialized&quot;)
        .lock()
        .expect(&quot;Failed to lock database&quot;);
    db.remove(key)
}

fn set_value(key: String, value: String) {
    let mut db = DATABASE
        .get()
        .expect(&quot;Database not initialized&quot;)
        .lock()
        .expect(&quot;Failed to lock database&quot;);
    db.insert(key, value);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The&lt;code&gt;set_value&lt;/code&gt; function does what you would expect (set a value). The &lt;code&gt;get_value&lt;/code&gt; function retrieves a value &lt;em&gt;and&lt;/em&gt; removes it from the database, you will see why in a second.&lt;/p&gt;
&lt;p&gt;Then all we have to do is make sure we initialize the database in main.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASE.get_or_init(|| Mutex::new(HashMap::new()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we will parse the A query and add the request value to the database.&lt;/p&gt;
&lt;p&gt;First in a new file &lt;code&gt;src/lib.rs&lt;/code&gt; we created a new structure that we will use to pass the message. This is overkill for the example but will make more sense in later parts of this series.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct Message {
    pub value: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next we define a function to parse the A record.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn parse_a_query(q: Question&amp;lt;&apos;_&amp;gt;) -&amp;gt; Result&amp;lt;ResourceRecord&amp;lt;&apos;_&amp;gt;&amp;gt; {
    let name = q.qname.to_string().to_uppercase();
    tracing::info!(&quot;new a query: {name}&quot;);

    let value = fs::read_to_string(&quot;/etc/passwd&quot;).await?;
    let data = bincode::serialize(&amp;amp;Message { value })?;
    let encoded = BASE32_NOPAD.encode(&amp;amp;data);

    set_value(name.clone(), encoded);

    tracing::info!(&quot;Set value {}&quot;, name);
    Ok(ResourceRecord::new(
        q.qname,
        CLASS::IN,
        2,
        RData::A(A {
            address: u32::from_be_bytes([41, 41, 41, 41]),
        }),
    ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;parse_a_query&lt;/code&gt; starts out by pulling the &lt;code&gt;name&lt;/code&gt; out of the record. Next it reads in the data the client will be requesting, in this case &lt;code&gt;/etc/passwd&lt;/code&gt;. Then we start to see some real magic, serializing the buffer into something we can pass to the client.  Step 1 of serializing is utilizing &lt;a href=&quot;https://docs.rs/bincode/latest/bincode/&quot;&gt;bincode&lt;/a&gt; to turn our structure into bytes, then it uses &lt;a href=&quot;https://docs.rs/data-encoding/latest/data_encoding/&quot;&gt;data_encoding&lt;/a&gt; to turn those bytes into ASCII which is safe to transfer. After encoding the value it uses &lt;code&gt;set_value&lt;/code&gt; to store the value with &lt;code&gt;name&lt;/code&gt; as the key. This encoding step is also a little overkill for this example and will come in handy in later versions of the code. Finally &lt;code&gt;set_value&lt;/code&gt; responds with a generic &lt;code&gt;A&lt;/code&gt; record so the client knows the server was successful.&lt;/p&gt;
&lt;p&gt;Now we need to handle the &lt;code&gt;TXT&lt;/code&gt; queries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn parse_txt_query(q: Question&amp;lt;&apos;_&amp;gt;) -&amp;gt; Result&amp;lt;ResourceRecord&amp;lt;&apos;_&amp;gt;&amp;gt; {
    let name = q.qname.to_string().to_uppercase();
    tracing::info!(&quot;Lookup {}&quot;, name);

    let value = get_value(&amp;amp;name).unwrap_or(&quot;AAAA&quot;.to_string());

    let len = value.clone().len().min(255);
    let (txt, remainder) = value.split_at(len);

    tracing::info!(&quot;Got value {}&quot;, value);

    if !remainder.is_empty() {
        set_value(name.clone(), remainder.to_string());
    } else {
        tracing::info!(&quot;No remaining info&quot;);
    };

    tracing::info!(&quot;returning {}&quot;, &amp;amp;value);

    let mut data = TXT::new();
    data.add_char_string(txt.to_string().try_into()?);
    Ok(ResourceRecord::new(
        q.qname.clone(),
        CLASS::IN,
        2,
        RData::TXT(data),
    ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;parse_txt_query&lt;/code&gt; function uses &lt;code&gt;get_value&lt;/code&gt; to lookup the value based on the &lt;code&gt;name&lt;/code&gt; value from the query. It then uses &lt;code&gt;split_at&lt;/code&gt; to chunk up the data into 255 byte values. It then stores the remainder back in the database for the next time the client queries. (Thus the delete in &lt;code&gt;get_value&lt;/code&gt;). Finally this chunk of the data is returned as a response to the &lt;code&gt;TXT&lt;/code&gt; request.&lt;/p&gt;
&lt;h2&gt;Client updates&lt;/h2&gt;
&lt;p&gt;With our server updated, now the client needs to know what to do. First the client will make an A query with the name of the key we want from the database. Then it will make TXT queries until it has all of the data.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn a_query_record(domain: &amp;amp;str) -&amp;gt; Result&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt; {
    let mut pkt = Packet::new_query(1);
    let q = Question::new(
        Name::new_unchecked(domain),
        TYPE::A.into(),
        CLASS::IN.into(),
        false,
    );
    pkt.set_flags(PacketFlag::RECURSION_DESIRED);
    pkt.questions.push(q);
    Ok(pkt.build_bytes_vec()?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;a_query_record&lt;/code&gt; should look familiar, it is basically &lt;code&gt;txt_query_record&lt;/code&gt; but for A&apos;s.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn parse_txt_response(data: Vec&amp;lt;u8&amp;gt;) -&amp;gt; Result&amp;lt;String&amp;gt; {
    let mut rv = String::new();
    let packet = Packet::parse(&amp;amp;data)?;
    let answer = packet.answers[0].clone();
    if let RData::TXT(val) = answer.rdata {
        for (k, _) in val.attributes() {
            rv += &amp;amp;k;
        }
    }
    Ok(rv)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;parse_txt_response&lt;/code&gt; takes a buffer, parses it into a packet and extracts the relevant data from the answers attributes. Finally it returns the value as a &lt;a href=&quot;https://doc.rust-lang.org/std/string/struct.String.html&quot;&gt;String&lt;/a&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let query = a_query_record(domain)?;

let mut buf = [0; 4096];
let (_size, _) = sock.recv_from(&amp;amp;mut buf).await?;
// let data = buf[..size].to_vec();

// TODO: How bout you check for some errors?
// let packet = Packet::parse(&amp;amp;data)?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First the client makes the &lt;code&gt;A&lt;/code&gt; query. Notably here is a good chance to check for errors, I have skipped that as this post is already getting pretty long.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
let query = txt_query_record(domain)?;
let mut incoming = String::new();
loop {
    sock.send_to(&amp;amp;query, server).await?;
    let mut buf = [0; 4096];
    let (size, _) = sock.recv_from(&amp;amp;mut buf).await?;
    let data = buf[..size].to_vec();
    let data = parse_txt_response(data)?;
    incoming += &amp;amp;data;
    if data.len() &amp;lt; 255 {
        break;
    }
}

let decoded = BASE32_NOPAD.decode(incoming.as_bytes())?;
let message: Message = bincode::deserialize(&amp;amp;decoded)?;
tracing::info!(&quot;value:\n{}&quot;, message.value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now for the magic on the client side. First we create a txt_query for our magic domain. Then drop into a loop and make the same txt request until we get back a buffer that didn&apos;t use the whole 255 bytes.&lt;/p&gt;
&lt;p&gt;Then it is just a matter of running the serialization steps the server took, but in reverse. &lt;code&gt;BASE32_NOPAD&lt;/code&gt; decode the data and &lt;code&gt;bincode::deserialize.&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Run it!&lt;/h2&gt;
&lt;p&gt;It is finally that point. Run the server in one terminal and the client in another and watch the magic happen.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-4-let-the-fun-begin/screenshot-2025-03-12-at-6-51-25-pm.webp&quot; alt=&quot;Two terminals side by side: the server logs base32-encoded TXT responses while the client runs --get test and prints the contents of /etc/passwd&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Phew that was a lot. Congrats for making it this far. If you made it this far make sure you subscribe, come yell at my on &lt;a href=&quot;https://bsky.app/profile/syndrowm.com&quot;&gt;bluesky&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/in/syndrowm/&quot;&gt;linkedin&lt;/a&gt;. Next week, we will update the client to send data to our &lt;a href=&quot;https://github.com/syndrowm/dns-kv&quot;&gt;dns-kv&lt;/a&gt; server.&lt;/p&gt;
</content:encoded><category>dns</category><category>rust</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Brain explode: AI is getting a little scary</title><link>https://offensivecontext.com/posts/brain-explode-ai-is-getting-a-little-scary/</link><guid isPermaLink="true">https://offensivecontext.com/posts/brain-explode-ai-is-getting-a-little-scary/</guid><description>A PE checksum problem, three AI tools, and the one that went and read the docs instead of inventing functions.</description><pubDate>Mon, 10 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Okay I kind of just had my mind blown.&lt;/p&gt;
&lt;p&gt;I am working on an interesting project in which I need to modify a PE file. The problem with modifying PE files is there is a checksum that windows defender looks at and flags you as being a mean bad jerk and puts your executable in executable jail. Soooo I need to fix that header.&lt;/p&gt;
&lt;p&gt;First I asked chatGPT how I could accomplish this with rust.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/brain-explode-ai-is-getting-a-little-scary/image-3.webp&quot; alt=&quot;ChatGPT 4o answering with a pelite Cargo.toml dependency and Rust code to fix a PE checksum&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Looks plausible. I copy the code into a new rust project... well the code was close, but chatGPT made up some functions and thus the logic of the code won&apos;t work.&lt;/p&gt;
&lt;p&gt;A little frustrated I asked google how to check the header with python...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/brain-explode-ai-is-getting-a-little-scary/screenshot-2025-03-06-at-3-06-44-pm.webp&quot; alt=&quot;Google search for &apos;python check the pe header checksum&apos; showing an AI Overview with a pefile code snippet&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Cool, Google AI gave me a snippet... I made a quick virtualenv, pip installed &lt;code&gt;pefile&lt;/code&gt; and I had a working example that could validate the header checksum for me.&lt;/p&gt;
&lt;p&gt;Next I loaded up cursor and pointed it at the rust project I had made from chatGPT. Not even thinking about the python snippet and venv in that folder. (This is called foreshadowing, It is my understanding that it is good for story telling)&lt;/p&gt;
&lt;p&gt;I ask the cursor agent (which is claude-sonnet 3.7) the following:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/brain-explode-ai-is-getting-a-little-scary/screenshot-2025-03-06-at-3-07-58-pm.webp&quot; alt=&quot;Cursor&apos;s chat panel: a request to fix a PE file&apos;s optional header checksum, with the agent naming two missing methods and searching the web and codebase&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Cursor did it&apos;s thing. Offered up some changes and it works.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;x-pe$ cargo run
   Compiling x-pe v0.1.0 (/home/evan/repos/x-pe)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/x-pe`
Old Checksum: 0x692460
New Checksum: 0x683B86
Checksum fixed successfully!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looking at what cursor did. First it searched for the library and functions to fix the header. Didn&apos;t find anything... It also recognized that the library functions from the chatGPT code didn&apos;t exist. Then with no prompting at all found the python snippet, the algorithm in the virtualenv code and re-implemented the algorithm in rust... 🤯&lt;/p&gt;
&lt;p&gt;And it worked the first time I ran it. Most importantly now defender doesn&apos;t hate my binaries.&lt;/p&gt;
&lt;p&gt;I don&apos;t know what it means exactly, but that was pretty cool.&lt;/p&gt;
</content:encoded><category>ai</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 3: What do you want?</title><link>https://offensivecontext.com/posts/abusing-dns-part-3-client/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-3-client/</guid><description>The client side: a Rust CLI that asks our own DNS server for records, so we stop leaning on dig to test it.</description><pubDate>Thu, 06 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Now that we have a server, it is time to build the client. In &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-2-2/&quot;&gt;Part 2&lt;/a&gt; we laid out the ground work of our project and built a server that could respond to various DNS queries (check out &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part 1&lt;/a&gt; if that is gobbledygook). As usual you can jump right to the source code on github TODO.&lt;/p&gt;
&lt;p&gt;As a recap here is where we left our client:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo run --bin client
   Compiling dns-kv v0.1.0 (/home/evan/dns-kv)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/client`
Hello, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s fix that and make a command line tool that is able to talk to our new DNS server and ask it for a TXT record. Sounds like fun doesn&apos;t it?&lt;/p&gt;
&lt;p&gt;First we are going to need another library that will assist us in parsing out command line argument (&lt;a href=&quot;https://docs.rs/clap/latest/clap/&quot;&gt;clap&lt;/a&gt;). Clap is awesome and helps us with most the things we need to make a fancy CLI app. Sound good? Right about now you are probably asking...&lt;/p&gt;
&lt;h2&gt;How do I get the clap?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-3-client/image.webp&quot; alt=&quot;Futurama&apos;s Fry squinting suspiciously, captioned &apos;Wait a minute…&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What did you just say?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Lucky for us rust, more specifically cargo makes that very simple for us. From your project folder run the following command and you are good to go.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo add clap -F cargo
    Updating crates.io index
      Adding clap v4.5.31 to dependencies
             Features:
             + cargo
             + color
...
    Updating crates.io index
     Locking 13 packages to latest compatible versions
      Adding anstream v0.6.18
...
dns-kv$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is it, now we have a very powerful tool to enable us to create CLI applications. For this example we are going to use the &lt;a href=&quot;https://docs.rs/clap/latest/clap/_tutorial/index.html&quot;&gt;builder pattern&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Parse away&lt;/h2&gt;
&lt;p&gt;Just like &lt;code&gt;dig&lt;/code&gt; or &lt;code&gt;nslookup&lt;/code&gt; our CLI is going to need to know what domain we are looking up and what server to talk to... Let&apos;s parse some arguments.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let matches = command!() // requires `cargo` feature
  .arg(arg!([server] &quot;DNS Server&quot;).default_value(&quot;127.0.0.1:5353&quot;))
  .arg(arg!(-g --get &amp;lt;NAME&amp;gt; &quot;Get the domain&quot;).required(true))
  .get_matches();

let domain = matches
  .get_one::&amp;lt;String&amp;gt;(&quot;get&quot;).expect(&quot;domain is required&quot;);

let server = matches
  .get_one::&amp;lt;String&amp;gt;(&quot;server&quot;).expect(&quot;server has default&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cool, what does that do?
First we use the &lt;code&gt;command&lt;/code&gt; macro to create our Command struct we then add our arguments. &lt;code&gt;server&lt;/code&gt; which is the DNS server to query and will default to &lt;code&gt;127.0.0.1:5353&lt;/code&gt; which is perfect as that is where our server currently is. Next we add the argument for the domain we would like to get which for now is &lt;code&gt;required&lt;/code&gt; ... you know what let me just show you.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo run --bin client -- -h
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/client -h`
Usage: client --get &amp;lt;NAME&amp;gt; [server]

Arguments:
  [server]  DNS Server to query [default: 127.0.0.1:5353]

Options:
  -g, --get &amp;lt;NAME&amp;gt;  Get the domain
  -h, --help        Print help
  -V, --version     Print version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Oh that is right, one of the great things about having clap in your project is you get built in help. Okay thats enough of that I promise.&lt;/p&gt;
&lt;h2&gt;What do you want?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-3-client/image-1.webp&quot; alt=&quot;Two-panel still from The Notebook: &apos;What… do you want?&apos; answered with &apos;It&apos;s not that simple.&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Now the CLI knows the domain and server we would like to talk to, it just has to ask. For now the CLI will assume we would like a TXT record.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn txt_query_record(domain: &amp;amp;str) -&amp;gt; Result&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt; {
    let mut pkt = Packet::new_query(1);
    let q = Question::new(
        Name::new_unchecked(domain),
        TYPE::TXT.into(),
        CLASS::IN.into(),
        false,
    );
    pkt.set_flags(PacketFlag::RECURSION_DESIRED);
    pkt.questions.push(q);
    Ok(pkt.build_bytes_vec()?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;txt_query_record&lt;/code&gt; creates a DNS query packet that is asking for a &lt;code&gt;TXT&lt;/code&gt; record for the domain we specified.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    let query = txt_query_record(domain)?;

    let sock = UdpSocket::bind(&quot;0.0.0.0:0&quot;).await?;
    sock.send_to(&amp;amp;query, server).await?;

    let mut buf = [0; 4096];
    let (size, _) = sock.recv_from(&amp;amp;mut buf).await?;
    let data = buf[..size].to_vec();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that query in hand the CLI binds to an open UDP port &lt;code&gt;0.0.0.0:0&lt;/code&gt; let&apos;s the kernel pick the port we will send data from. Sends the query packet to the server and reads the response.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let packet = Packet::parse(&amp;amp;data)?;
tracing::info!(&quot;{:#?}&quot;, packet.answers);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally the cli parses the response from the packet and prints out the response.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo run --bin client -- -g domain.com
   Compiling dns-kv v0.1.0 (/home/evan/repos/dns-kv)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.59s
     Running `target/debug/client -g domain.com`
2025-03-05T22:19:50.479792Z  INFO client: [
    ResourceRecord {
        name: Name(
            &quot;domain.com&quot;,
            &quot;12&quot;,
        ),
        class: IN,
        ttl: 2,
        rdata: TXT(
            TXT {
                strings: [
                    CharacterString {
                        data: &quot;AAAA&quot;,
                    },
                ],
                size: 5,
            },
        ),
        cache_flush: false,
    },
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lets go&lt;/h2&gt;
&lt;p&gt;Well that is coming along nicely if I don&apos;t say so myself. If you made it this far, you might was well click that subscribe button over there. Come yell at my on &lt;a href=&quot;https://bsky.app/profile/syndrowm.com&quot;&gt;bluesky&lt;/a&gt; or &lt;a href=&quot;https://www.linkedin.com/in/syndrowm/&quot;&gt;linkedin&lt;/a&gt;. Next week, we will start turning &lt;a href=&quot;https://github.com/syndrowm/dns-kv&quot;&gt;dns-kv&lt;/a&gt; into a tool that will let us transfer data.&lt;/p&gt;
</content:encoded><category>dns</category><category>rust</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 2: Serving up some fun</title><link>https://offensivecontext.com/posts/abusing-dns-part-2-2/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-2-2/</guid><description>Building a DNS server in Rust that answers real queries — the first working piece of a key-value store that talks over DNS.</description><pubDate>Thu, 27 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;As we continue to explore how hackers take advantage of DNS, our goal for part 2 is to get into some rust code and build a server that has enough functionality that it will be able to respond to simple DNS requests. If you missed it, check out &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part 1&lt;/a&gt; for an overview and refresher on the protocol. Follow along or jump to the source code &lt;a href=&quot;https://github.com/syndrowm/dns-kv/tree/part2&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Let&apos;s write a server!&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-2-2/screenshot-2025-02-26-at-9-52-13-am.webp&quot; alt=&quot;The same DNS relay diagram with the middle resolver, 192.168.0.1, circled in red&quot; /&gt;&lt;/p&gt;
&lt;p&gt;I&apos;m going to assume you have &lt;a href=&quot;https://rustup.rs/&quot;&gt;rust&lt;/a&gt; installed. Up first let&apos;s create our project and add some dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cargo new dns-kv &amp;amp;&amp;amp; cd dns-kv
$ cargo add tokio -F full
$ cargo add simple-dns
$ cargo add tracing, tracing-subscriber, bincode, data-encoding
$ cargo add tracing
$ cargo add tracing-encoding
$ cargo add tracing-subscriber
$ cargo add bincode
$ cargo add data-encoding
$ cargo run
...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/dns-kv`
Hello, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 I cut a bunch of fluff out of the above, don&apos;t be worried if you see a bunch of stuff in your shell.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;And a client!?!?!&lt;/h2&gt;
&lt;p&gt;Just kidding, Part 3 will be devoted to the client. For now though, lets setup our project so that we can have a server and a client.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ mkdir src/bin
dns-kv$ mv src/main src/bin/server.rs
dns-kv$ cp src/bin/server.rs src/bin/client.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we have two binaries in our project. You run them like this.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo run --bin server
   Compiling dns-kv v0.1.0 (/home/evan/dns-kv)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/server`
Hello, world!
dns-kv$ cargo run --bin client
   Compiling dns-kv v0.1.0 (/home/evan/dns-kv)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/client`
Hello, world!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;For real lets write a server&lt;/h2&gt;
&lt;p&gt;Rust forces you to think about errors and error handling from the start. If your rust code is confusing or painful... You aren&apos;t doing errors right. Luckily we can start off pretty simple and handle more complicated errors as we run into trouble.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Easy mode error handling.
type Result&amp;lt;T&amp;gt; = core::result::Result&amp;lt;T, Error&amp;gt;;
type Error = Box&amp;lt;dyn std::error::Error&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is one of the best videos on rust error handling I&apos;ve found.&lt;/p&gt;

&lt;p&gt;Now you can update your main function in &lt;code&gt;src/bin/server.rs&lt;/code&gt; with the following.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    // everybody love logging
    use tracing_subscriber::{fmt, prelude::*, EnvFilter};
    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(EnvFilter::new(std::env::var(&quot;RUST_LOG&quot;).unwrap_or_else(
            |_| format!(&quot;{}=debug&quot;, env!(&quot;CARGO_CRATE_NAME&quot;)),
        )))
        .try_init()?;

    // create teh server socket and listen on 5353
    let socket = UdpSocket::bind(&quot;0.0.0.0:5353&quot;).await?;
    tracing::info!(&quot;listening on {}&quot;, socket.local_addr().unwrap());

    let socket = std::sync::Arc::new(socket);
    let mut buf = vec![0u8; 2048]; // max dns packet is 512

    loop {
        let socket = socket.clone();
        let (size, peer) = socket.recv_from(&amp;amp;mut buf).await?;
        let data = buf[..size].to_vec();
        // Spawn a new task for each incoming datagram
        tokio::spawn(async move {
            tracing::debug!(&quot;received {} bytes from {}&quot;, size, peer);
            let rv = handle_dns_query(socket, data, peer).await;
            if let Err(e) = rv {
                tracing::debug!(&quot;{e:?}&quot;);
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let&apos;s break that down. &lt;code&gt;#[tokio::main]&lt;/code&gt; is a macro from the asynchronous &lt;a href=&quot;https://tokio.rs/&quot;&gt;tokio&lt;/a&gt; project. That is right, we are using async code so our server will be blazingly fast.&lt;/p&gt;
&lt;p&gt;Our &lt;code&gt;main&lt;/code&gt; function returns &lt;code&gt;Result&amp;lt;()&amp;gt;&lt;/code&gt; this magic can happen because we defined above.&lt;/p&gt;
&lt;p&gt;Next up we setup tracing. Because everybody loves logging.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-2-2/everybody-loves-1.webp&quot; alt=&quot;The Everybody Loves Raymond cast photo with &apos;Raymond&apos; crossed out and replaced by &apos;Logging&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Finally in the loop we listen for connections and spawn tasks to read from the socket and handle the requests in &lt;code&gt;handle_dns_query&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Handling the request&lt;/h2&gt;
&lt;p&gt;Let&apos;s take a look at &lt;code&gt;handle_dns_query&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn handle_dns_query(socket: Arc&amp;lt;UdpSocket&amp;gt;, data: Vec&amp;lt;u8&amp;gt;, peer: SocketAddr) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let packet = Packet::parse(&amp;amp;data)?;

    let mut response = packet.clone().into_reply();

    for q in packet.questions {
        let answer = match q.qtype {
            QTYPE::TYPE(TYPE::A) =&amp;gt; Ok(parse_a_query(q).await?),
            QTYPE::TYPE(TYPE::AAAA) =&amp;gt; Ok(parse_aaaa_query(q).await?),
            QTYPE::TYPE(TYPE::TXT) =&amp;gt; Ok(parse_txt_query(q).await?),
            _ =&amp;gt; Err(&quot;invalid type&quot;),
        }?;

        response.answers.push(answer);
    }

    let rd = response.build_bytes_vec()?;
    socket.send_to(&amp;amp;rd, peer).await?;

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function takes a copy of the UDPSocket, the data to process and information about the &lt;code&gt;peer&lt;/code&gt; aka who made the request. It then parses the &lt;code&gt;data&lt;/code&gt; into a DNS packet and prepares a response. Next it iterates over the questions in the packet and hands them off to a function that handles the specific response.&lt;/p&gt;
&lt;p&gt;Let&apos;s look at the first query handler &lt;code&gt;parse_a_query&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn parse_a_query(q: Question&amp;lt;&apos;_&amp;gt;) -&amp;gt; Result&amp;lt;ResourceRecord&amp;lt;&apos;_&amp;gt;&amp;gt; {
    Ok(ResourceRecord::new(
        q.qname,
        CLASS::IN,
        2,
        RData::A(A {
            address: u32::from_be_bytes([41, 41, 41, 41]),
        }),
    ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Like the name implies, this function is handling an &lt;code&gt;A&lt;/code&gt; query. For now it isn&apos;t doing much, just responding with a static response. As saw last week and IP address is just a funky way of encoding a number.&lt;/p&gt;
&lt;p&gt;For fun, this is what that conversion looks like in python.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; socket.inet_ntoa(struct.pack(&apos;&amp;gt;BBBB&apos;, 41,41,41,41))
&apos;41.41.41.41&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Wrap it up&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-2-2/image.webp&quot; alt=&quot;Office Space meme: Bill Lumbergh saying &apos;If we could just wrap it up anytime now… that would be great&apos;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We are in the home stretch, lets run the server and test it out with dig. In one shell run the server.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ cargo run --bin server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And in another shell test it out.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns-kv$ dig +short @127.0.0.1 -p 5353 A test.com
41.41.41.41
dns-kv$ dig +short @127.0.0.1 -p 5353 AAAA test.com
::41.41.41.41
dns-kv$ dig +short @127.0.0.1 -p 5353 TXT test.com
&quot;AAAA&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Amazing! We&apos;ve done it. In the first command we use dig to ask our server on &lt;code&gt;127.0.0.1&lt;/code&gt; port &lt;code&gt;5353&lt;/code&gt; what the &lt;code&gt;A&lt;/code&gt; record is for &lt;code&gt;test.com&lt;/code&gt; and as we expected, we get back &lt;code&gt;41.41.41.41&lt;/code&gt;. We then do the same to check that the &lt;code&gt;AAAA&lt;/code&gt; and &lt;code&gt;TXT&lt;/code&gt; records are working as we expect.&lt;/p&gt;
&lt;h2&gt;Call to action&lt;/h2&gt;
&lt;p&gt;Check out &lt;a href=&quot;https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/&quot;&gt;Part 1&lt;/a&gt; to see where this all started, subscribe if you want to see more, follow me up on the &lt;a href=&quot;https://bsky.app/profile/syndrowm.com&quot;&gt;butterfly&lt;/a&gt; site if you want some more memes or to make fun of my non-sense. See you next week in Part 3 where we will be building the CLI client with &lt;a href=&quot;https://docs.rs/clap/latest/clap/&quot;&gt;clap&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>dns</category><category>networking</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Abusing DNS, Part 1: How does DNS do what it do?</title><link>https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/</link><guid isPermaLink="true">https://offensivecontext.com/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/</guid><description>How DNS resolution actually works, and why a protocol every network lets out is such a useful place to hide data.</description><pubDate>Thu, 20 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Domain_Name_System&quot;&gt;DNS&lt;/a&gt; is one of the protocols that make the internet possible. In many ways DNS is the phone book of the internet. DNS turns the domain you are trying to get to into the IP address your computer needs to get there. DNS is robust network of inter connected systems used to translate human readable domains into the IP address, but it can be much more.&lt;/p&gt;
&lt;p&gt;In this series we will use rust to build a key-value store that uses DNS to transfer data between our custom DNS server and client. Future code will be available on &lt;a href=&quot;https://github.com/syndrowm/dns-kv&quot;&gt;Github&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let&apos;s start by looking at the protocol, we will then build a simple DNS server/client application and finally take advantage of what we know to build a key value store.&lt;/p&gt;
&lt;h2&gt;DNS requests, how do they work?&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/magnets-c.webp&quot; alt=&quot;Miracles&amp;quot; / Fucking Magnets, How Do They Work? | Know Your Meme&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Clown doesn&apos;t know how DNS do&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It is important to know that computers don&apos;t think in strings like people (www.google.com) they think in binary &lt;code&gt;10001110111110100100100000100100&lt;/code&gt;. Humans are really bad at thinking in binary so we make computers to backflips to manage all of that for us.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dig +short @1.1.1.1 www.google.com
142.250.69.228
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Astute readers like yourself probably notice that isn&apos;t binary. We humans are so bad at binary we have computers &lt;code&gt;encode&lt;/code&gt; the numbers (we wont remember) into more human readable forms, thus &lt;code&gt;142.250.69.228&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; socket.gethostbyname(&apos;www.google.com&apos;)
&amp;gt;&amp;gt;&amp;gt; socket.inet_aton(&apos;142.250.69.228&apos;)
b&apos;\x8e\xfaE\xe4&apos;
&amp;gt;&amp;gt;&amp;gt; struct.unpack(&apos;&amp;gt;I&apos;, b&apos;\x8e\xfaE\xe4&apos;)
(2398767140,)
&amp;gt;&amp;gt;&amp;gt; bin(2398767140)
&apos;0b10001110111110100100100000100100&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will utilize data encodings to our advantage later in this series. Back to DNS...&lt;/p&gt;
&lt;p&gt;What is happening when you make a DNS request?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ dig www.google.com

; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.18.28-1~deb12u2-Debian &amp;lt;&amp;lt;&amp;gt;&amp;gt; www.google.com
;; global options: +cmd
;; Got answer:
;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NOERROR, id: 5248
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.google.com.                        IN      A

;; ANSWER SECTION:
www.google.com.         300     IN      A       142.250.72.36

;; Query time: 28 msec
;; SERVER: 192.168.0.1#53(192.168.0.1) (UDP)
;; WHEN: Thu Feb 20 09:10:07 MST 2025
;; MSG SIZE  rcvd: 59
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The above &lt;code&gt;dig&lt;/code&gt; command asked &quot;what is the IPv4 address for www.google.com&quot;.&lt;/p&gt;
&lt;p&gt;While a very simple command on its face, a very complex set of things just happened.&lt;/p&gt;
&lt;p&gt;First my system looked at it&apos;s local resolver settings and found &lt;code&gt;192.168.0.1&lt;/code&gt; as my local DNS server. Next a UDP request with an &lt;code&gt;A&lt;/code&gt; question was sent to &lt;code&gt;192.168.0.1&lt;/code&gt;. From here the server checked to see if it knew an IP for &lt;code&gt;www.google.com&lt;/code&gt; and if that IP address was still valid by checking the &lt;code&gt;TTL&lt;/code&gt;.  If &lt;code&gt;192.168.0.1&lt;/code&gt; did not know the answer it then looked for it&apos;s own resolver and sent a request asking the same question. I&apos;m simplifying here, but that continues until we get an answer or don&apos;t have any other server to ask. Finally the answers are sent all the way back and my system has the IP.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://offensivecontext.com/images/posts/abusing-dns-part-1-how-does-dns-do-what-it-do/screenshot-2025-02-20-at-9-44-41-am.webp&quot; alt=&quot;Hand-drawn diagram: a host asks 192.168.0.1 for www.google.com, that resolver asks 1.1.1.1, and the answer 142.250.72.36 comes back down the chain&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Well that is pretty cool. We ask a server we don&apos;t control to send requests for us and get responses from our own servers... Wait, what else can we ask for?&lt;/p&gt;
&lt;p&gt;Glad you asked! There are many different questions you can ask. The server responds with &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_DNS_record_types&quot;&gt;records&lt;/a&gt;. We already saw the most common &lt;code&gt;A&lt;/code&gt; query which will return an &lt;code&gt;A&lt;/code&gt; record. The &lt;code&gt;A&lt;/code&gt; query is so common, in fact, &lt;code&gt;dig&lt;/code&gt; assumes that is what you meant. If you want to be more explicit you can specify the record type. Let&apos;s look at a few different query types.&lt;/p&gt;
&lt;p&gt;Later in the series we will be going further into query and record types, for now, know that there are multiple record types that contain different data types. &lt;code&gt;A&lt;/code&gt; records are IPv4, &lt;code&gt;AAAA&lt;/code&gt; (quad A) records contain IPv6 addresses. &lt;code&gt;TXT&lt;/code&gt; records have text data. &lt;code&gt;MX&lt;/code&gt; (&lt;strong&gt;M&lt;/strong&gt;ail &lt;strong&gt;E&lt;/strong&gt;xchanger) have the mail server. These are the most common, there are a whole bunch (go read the wiki pages and donate to wiki foundation while you are there).&lt;/p&gt;
&lt;p&gt;That is it for now. Up next we will start our rust project and build a UDP DNS server that can respond to various query types with our own DNS records.&lt;/p&gt;
&lt;p&gt;Subscribe, follow, etc if you want to see more.&lt;/p&gt;
</content:encoded><category>dns</category><category>networking</category><author>contact@offensivecontext.com (Evan Anderson)</author></item><item><title>Spooky simple Python tricks</title><link>https://offensivecontext.com/posts/simple-python-tricks/</link><guid isPermaLink="true">https://offensivecontext.com/posts/simple-python-tricks/</guid><description>The handful of Python tricks I reach for almost daily to reshape, encode and explore data.</description><pubDate>Mon, 21 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In infosec, being able to explore and manipulate data is a super power. After years (decades) of using python, these are some of the things I lean on almost every day to make the hax and explore.&lt;/p&gt;
&lt;p&gt;Python is an amazing programming language. Python is simple, concise and flexible. It is especially useful to quickly solve real problems. There is a reason that Python is the language of ML and AI and data sciences.&lt;/p&gt;
&lt;p&gt;Here are some beginner python tricks to quickly format data, encode/decode various data types, and quickly automate exploring &lt;a href=&quot;https://www.cisa.gov/known-exploited-vulnerabilities-catalog&quot;&gt;CISA KEV&lt;/a&gt; list.&lt;/p&gt;
&lt;h2&gt;REPL&lt;/h2&gt;
&lt;p&gt;For starters, interactivity is key for exploring data and quickly iterating on ideas. The Python &lt;a href=&quot;https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop&quot;&gt;REPL&lt;/a&gt; makes exploring and playing with data a breeze.&lt;/p&gt;
&lt;p&gt;With Python &lt;a href=&quot;https://wiki.python.org/moin/BeginnersGuide/Download&quot;&gt;installed&lt;/a&gt; on most systems using the REPL is as simple as typing &lt;code&gt;python&lt;/code&gt; into your terminal.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ python3
Python 3.11.8 (main, Feb 13 2024, 09:03:56) [GCC 12.2.0] on linux
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once in the REPL you are executing python. Just type the python and hit enter. It couldn&apos;t be more simple&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; 1+1
2
&amp;gt;&amp;gt;&amp;gt; print(&quot;hello world&quot;)
hello world
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HELP!&lt;/h3&gt;
&lt;p&gt;Don&apos;t worry Python is here to help. The &lt;code&gt;help()&lt;/code&gt; function will show you documentation and be a useful reminder if you forget any of the syntax.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; help()
Welcome to Python 3.11&apos;s help utility! If this is your first time using
Python, you should definitely check out the tutorial at
https://docs.python.org/3.11/tutorial/.

Enter the name of any module, keyword, or topic to get help on writing
Python programs and using Python modules.  To get a list of available
modules, keywords, symbols, or topics, enter &quot;modules&quot;, &quot;keywords&quot;,
&quot;symbols&quot;, or &quot;topics&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Virtual environments&lt;/h3&gt;
&lt;p&gt;You will likely need to install some additional packages that can make your life easier. Don&apos;t over think this we are hacking and exploring data. VirtualEnvs are a perfect solution for experimenting and quickly installing packages in a partially isolated environment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install requests rich
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pretty Printing&lt;/h3&gt;
&lt;p&gt;If you are going to be looking at data in the shell... you need to make the output pretty. Thankfully we have the amazing &lt;a href=&quot;https://github.com/Textualize/rich&quot;&gt;rich&lt;/a&gt; library. Rich has many features to make your script output beautiful. Luckily for us rich has the &lt;code&gt;pretty&lt;/code&gt; module. Once installed in the REPL Python data structures will automatically be pretty printed with syntax highlighting.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from rich import pretty
&amp;gt;&amp;gt;&amp;gt; pretty.install()
&amp;gt;&amp;gt;&amp;gt; x = {&apos;a&apos;: 1, &apos;b&apos;: 2}
{&apos;a&apos;: 1, &apos;b&apos;: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;HTTP Client&lt;/h3&gt;
&lt;p&gt;The world runs on HTTP. Being able to make quick simple HTTP requests and parse the data is its own super power. Maybe you would like to explore the CISA Known Exploited Vulnerabilities (KEV) list.&lt;/p&gt;
&lt;p&gt;The python &lt;a href=&quot;https://docs.python-requests.org/en/latest/index.html&quot;&gt;requests&lt;/a&gt; module is designed to make simple HTTP requests well... simple. The requests tag line is &lt;code&gt;Built for human beings.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here we make an HTTP request to download the CISA KEV list and parse the response as JSON with the &lt;code&gt;.json()&lt;/code&gt; call. In Python JSON is translated to the built-in &lt;a href=&quot;https://docs.python.org/3/tutorial/datastructures.html#dictionaries&quot;&gt;dictionary&lt;/a&gt; data type. For our purposes a dictionary is a set of key: value pairs. The &lt;code&gt;.keys()&lt;/code&gt; method on a dictionary shows us the top level keys. We then get the values for the &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;dateReleased&lt;/code&gt; keys.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import requests
&amp;gt;&amp;gt;&amp;gt; r = requests.get(
 &quot;https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json&quot;
 ).json()
&amp;gt;&amp;gt;&amp;gt; r.keys()
dict_keys([&apos;title&apos;, &apos;catalogVersion&apos;, &apos;dateReleased&apos;, &apos;count&apos;, &apos;vulnerabilities&apos;])
&amp;gt;&amp;gt;&amp;gt; r[&apos;title&apos;], r[&apos;dateReleased&apos;]
(&apos;CISA Catalog of Known Exploited Vulnerabilities&apos;, &apos;2024-03-18T17:37:00.9876Z&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; r[&apos;vulnerabilities&apos;][-1]
{
    &apos;cveID&apos;: &apos;CVE-2024-27198&apos;,
    &apos;vendorProject&apos;: &apos;JetBrains&apos;,
    &apos;product&apos;: &apos;TeamCity&apos;,
    &apos;vulnerabilityName&apos;: &apos;JetBrains TeamCity Authentication Bypass Vulnerability&apos;,
    &apos;dateAdded&apos;: &apos;2024-03-07&apos;,
    &apos;shortDescription&apos;: &apos;JetBrains TeamCity contains an authentication bypass vulnerability that allows an attacker to perform admin actions.&apos;,
    &apos;requiredAction&apos;: &apos;Apply mitigations per vendor instructions or discontinue use of the product if mitigations are unavailable.&apos;,
    &apos;dueDate&apos;: &apos;2024-03-28&apos;,
    &apos;knownRansomwareCampaignUse&apos;: &apos;Unknown&apos;,
    &apos;notes&apos;: &apos;https://www.jetbrains.com/help/teamcity/teamcity-2023-11-4-release-notes.html&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally lets build a list of all of the CVEs on the CISA KEV list, get a count of how many cves we found and look at the first 5 entries to sanity check.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; cves = [i[&apos;cveID&apos;] for i in r[&apos;vulnerabilities&apos;]]
&amp;gt;&amp;gt;&amp;gt; len(cves)
1089
&amp;gt;&amp;gt;&amp;gt; cves[0:5]
[&apos;CVE-2021-27104&apos;, &apos;CVE-2021-27102&apos;, &apos;CVE-2021-27101&apos;, &apos;CVE-2021-27103&apos;, &apos;CVE-2021-21017&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Encoding and decoding data&lt;/h3&gt;
&lt;p&gt;Data can be represented in many ways and thus comes in all shapes and sizes. Being able to convert between data types is essential for your new super power. Let&apos;s look at the very basics.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[WARNING] encoding is not encryption&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&quot;bWFuaXB1bGF0aW5nIGRhdGEgaXMga2V5&quot;

&quot;6d616e6970756c6174696e672064617461206973206b6579&quot;

[
    &apos;0b1101101&apos;, &apos;0b1100001&apos;, &apos;0b1101110&apos;, &apos;0b1101001&apos;, &apos;0b1110000&apos;, &apos;0b1110101&apos;,
    &apos;0b1101100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1101001&apos;, &apos;0b1101110&apos;, &apos;0b1100111&apos;,
    &apos;0b100000&apos;, &apos;0b1100100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1100001&apos;, &apos;0b100000&apos;,
    &apos;0b1101001&apos;, &apos;0b1110011&apos;, &apos;0b100000&apos;, &apos;0b1101011&apos;, &apos;0b1100101&apos;, &apos;0b1111001&apos;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;base64&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Base64&quot;&gt;base64&lt;/a&gt; is one of the most common schemes allowing you to encode binary data into printable characters. Base64 is so common it is built into the Python standard library, the docs are &lt;a href=&quot;https://docs.python.org/3/library/base64.html&quot;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Lets take a base64 value and convert it to binary and then back to base64. Take note in Python the underscore &lt;code&gt;_&lt;/code&gt; is used to represent the output from the previous line. This is super useful if you forget to assign the output of your command.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from base64 import b64encode, b64decode
&amp;gt;&amp;gt;&amp;gt; x = &quot;bWFuaXB1bGF0aW5nIGRhdGEgaXMga2V5&quot;
&amp;gt;&amp;gt;&amp;gt; b64decode(x)
b&apos;manipulating data is key&apos;
&amp;gt;&amp;gt;&amp;gt; b64encode(_)
b&apos;bWFuaXB1bGF0aW5nIGRhdGEgaXMga2V5&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;hex&lt;/h3&gt;
&lt;p&gt;Another way to represent binary data as printable characters is &lt;a href=&quot;https://en.wikipedia.org/wiki/Hexadecimal&quot;&gt;hexidecimal&lt;/a&gt;. This type of representation is often seen used in tools like &lt;code&gt;hexdump&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here is how you can convert from a hexidecimal encoding to binary and back.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; from binascii import hexlify, unhexlify
&amp;gt;&amp;gt;&amp;gt; unhexlify(&quot;6d616e6970756c6174696e672064617461206973206b6579&quot;)
b&apos;manipulating data is key&apos;
&amp;gt;&amp;gt;&amp;gt; hexlify(_)
b&apos;6d616e6970756c6174696e672064617461206973206b6579&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;binary&lt;/h3&gt;
&lt;p&gt;With all this talk of binary, lets look at how you can use a list comprehension to see what the world in binary.&lt;/p&gt;
&lt;p&gt;First we create a list of binary values called &lt;code&gt;x&lt;/code&gt;. On the next line we convert each item in the list to an integer with the call to &lt;code&gt;int()&lt;/code&gt; and convert that integer into a character with the &lt;code&gt;char()&lt;/code&gt; function. Finally we join all the characters into a single string.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; x = [
...     &apos;0b1101101&apos;, &apos;0b1100001&apos;, &apos;0b1101110&apos;, &apos;0b1101001&apos;, &apos;0b1110000&apos;, &apos;0b1110101&apos;,
...     &apos;0b1101100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1101001&apos;, &apos;0b1101110&apos;, &apos;0b1100111&apos;,
...     &apos;0b100000&apos;, &apos;0b1100100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1100001&apos;, &apos;0b100000&apos;,
...     &apos;0b1101001&apos;, &apos;0b1110011&apos;, &apos;0b100000&apos;, &apos;0b1101011&apos;, &apos;0b1100101&apos;, &apos;0b1111001&apos;
... ]
&amp;gt;&amp;gt;&amp;gt; &apos;&apos;.join([chr(int(i, 2)) for i in x])
&apos;manipulating data is key&apos;
&amp;gt;&amp;gt;&amp;gt; [bin(ord(i)) for i in _]
&amp;gt;&amp;gt;&amp;gt;  [&apos;0b1101101&apos;, &apos;0b1100001&apos;, &apos;0b1101110&apos;, &apos;0b1101001&apos;, &apos;0b1110000&apos;, &apos;0b1110101&apos;,
     &apos;0b1101100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1101001&apos;, &apos;0b1101110&apos;, &apos;0b1100111&apos;,
     &apos;0b100000&apos;, &apos;0b1100100&apos;, &apos;0b1100001&apos;, &apos;0b1110100&apos;, &apos;0b1100001&apos;, &apos;0b100000&apos;,
     &apos;0b1101001&apos;, &apos;0b1110011&apos;, &apos;0b100000&apos;, &apos;0b1101011&apos;, &apos;0b1100101&apos;, &apos;0b1111001&apos;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Data Objects&lt;/h3&gt;
&lt;p&gt;As we saw earlier a simple way to interact with JSON objects in Python is to convert them to the &lt;a href=&quot;https://docs.python.org/3/tutorial/datastructures.html#dictionaries&quot;&gt;dictionary&lt;/a&gt; data structure. What if you want to save the dictionary locally instead of reading it from a HTTP request?&lt;/p&gt;
&lt;h3&gt;json&lt;/h3&gt;
&lt;p&gt;Here we save or &lt;code&gt;dump&lt;/code&gt; a Python dictionary as a JSON string into the file &lt;code&gt;example.json&lt;/code&gt; and then read or &lt;code&gt;load&lt;/code&gt; that same JSON string from a file converting it back to a dictionary.&lt;/p&gt;
&lt;p&gt;The magic here is the call to the &lt;code&gt;open()&lt;/code&gt; function which creates a readable stream the &lt;code&gt;json&lt;/code&gt; module can then use to write or read the file contents.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import json
&amp;gt;&amp;gt;&amp;gt; json.dump({&apos;a&apos;: 1, &apos;b&apos;: 2}, open(&apos;example.json&apos;, &apos;w&apos;))
&amp;gt;&amp;gt;&amp;gt; json.load(open(&apos;example.json&apos;))
{&apos;a&apos;: 1, &apos;b&apos;: 2}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;yaml&lt;/h3&gt;
&lt;p&gt;Yet another markup language is &lt;a href=&quot;https://en.wikipedia.org/wiki/YAML&quot;&gt;YAML&lt;/a&gt;. As it is designed to be human readable YAML is often used for configuration files and is the defacto language of DevOps. Thanks to the &lt;a href=&quot;https://pyyaml.org/&quot;&gt;PyYaml Framework&lt;/a&gt; it is easy convert YAML to Python dictionaries and store the objects as files.&lt;/p&gt;
&lt;p&gt;First we need to install the PyYaml module into the &lt;code&gt;virtualenv&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pyyaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once installed we can interact with YAML pretty much the exact same syntax as the &lt;code&gt;json&lt;/code&gt; module.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import yaml
&amp;gt;&amp;gt;&amp;gt; yaml.dump({&apos;a&apos;: 1, &apos;b&apos;: 2}, open(&apos;example.yaml&apos;, &apos;w&apos;))
&amp;gt;&amp;gt;&amp;gt; yaml.safe_load(open(&apos;example.yaml&apos;))
{&apos;a&apos;: 1, &apos;b&apos;: 2}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><category>python</category><author>contact@offensivecontext.com (Evan Anderson)</author></item></channel></rss>