<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>※ hackd</title><description>hackd - security &amp; risk</description><link>https://hackd.net/</link><language>en-us</language><item><title>Choices and responsibility</title><link>https://hackd.net/posts/choice-responsibility/</link><guid isPermaLink="true">https://hackd.net/posts/choice-responsibility/</guid><description>Brief notes on tools we choose to use, their impacts and risks, and who bears the cost.</description><pubDate>Thu, 12 Feb 2026 18:13:53 GMT</pubDate><content:encoded>&lt;p&gt;Every time new technology comes out and becomes widely available, there is a
difficult balance to be found between the exuberance of early adopters—keen to
drive broad usage and land early wins unlocked by the new thing—&lt;sup&gt;&lt;a href=&quot;#user-content-fn-em&quot; id=&quot;user-content-user-content-fnref-em&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;and those
who would seek to better understand the downsides before going too deep. There
are essential (and obvious) risks none disagree on, but these are few. The real
challenge is in negotiating the middle-ground, all the second+ order effects.
It&apos;s fair that neither side prevails unchallenged&lt;sup&gt;&lt;a href=&quot;#user-content-fn-balance&quot; id=&quot;user-content-user-content-fnref-balance&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;My primary perspective comes from doing Security Engineering work across
companies of different sizes (and cultures), some defense but mostly offense. On
either side though, I&apos;ve largely thought about what the new technology amplifies
in terms of risk. Today, of course, it&apos;s LLMs, but the problem space is fractal:
we run into variants&lt;sup&gt;&lt;a href=&quot;#user-content-fn-inject&quot; id=&quot;user-content-user-content-fnref-inject&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;3&lt;/a&gt;&lt;/sup&gt; of the same meta issues all the time in security,
because that&apos;s the nature of our work. Something shiny, often useful, almost
never designed with safety&lt;sup&gt;&lt;a href=&quot;#user-content-fn-safety&quot; id=&quot;user-content-user-content-fnref-safety&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;4&lt;/a&gt;&lt;/sup&gt; as a primary property.&lt;/p&gt;
&lt;p&gt;Yet, in accepting that we cannot wait for good-enough guardrails around new
technology within certain time constraints, we also &lt;strong&gt;do not get to abdicate our
responsibility&lt;/strong&gt; for the choice of using such tech. We might not be aware of all
the specific second+ order effects, but we can be sure they exist&lt;sup&gt;&lt;a href=&quot;#user-content-fn-macro&quot; id=&quot;user-content-user-content-fnref-macro&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;5&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;So it is with LLMs and all they&apos;ve recently unlocked in terms of working with
software and systems (e.g., code assistance, vulnerability discovery, semi- or
fully- autonomous personal agents). Broadly, we&apos;ve been able to convert A LOT of
ideas into running code, and got some pretty funny and clever things out, too.
Quite fast, generally. Yet, all of this software remains the responsibility of
whatever human actor is ultimately at the top of the pyramid, and we should
neither pretend otherwise, nor enable unaccountability in this regard.&lt;/p&gt;
&lt;p&gt;There are situations where and simply using whatever the LLM generated for code
is fine without paying it too much attention: prototypes, one-off scripts, even
tools that would only ever impact that human if there was a problem (i.e., skin
in the game), etc.&lt;/p&gt;
&lt;p&gt;Yet for code that&apos;s to be shared with other people it remains the human&apos;s
responsibility to review it, ensure its quality, and do so both in terms of
respect, and empathy, before sharing it in the first place. That human is still
responsible.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;I really enjoyed reading (and recommend) both Russ Cox&apos;s
&lt;a href=&quot;https://groups.google.com/g/golang-dev/c/4Li4Ovd_ehE/m/8L9s_jq4BAAJ&quot;&gt;post in &lt;code&gt;golang-dev@&lt;/code&gt;&lt;/a&gt;
on suggestions for how to approach aspects of this issue in that project, and
the &lt;a href=&quot;https://rfd.shared.oxide.computer/rfd/0576&quot;&gt;Oxide RFD (576)&lt;/a&gt; he mentions.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;user-content-footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-em&quot;&gt;
&lt;p&gt;I happen to know how to use em-dashes; this article is 100% organic. &lt;a href=&quot;#user-content-fnref-em&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-balance&quot;&gt;
&lt;p&gt;Even this concession is uncomfortable for some cultures, which would much
rather build up a high degree of confidence/containment before allowing
anything. This moves into a different discussion on risk, and while that
conversation is interesting and worth iterating on, it is not how things
currently work in practice. &lt;a href=&quot;#user-content-fnref-balance&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-inject&quot;&gt;
&lt;p&gt;Injections of all kinds are as old as computing, as is the unsafe mixing of
code and data. &lt;a href=&quot;#user-content-fnref-inject&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 3&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-safety&quot;&gt;
&lt;p&gt;I think of safety in general, not just security. They are deeply
intertwined, yet the actual, meaningful property we should strive to deliver
upon has to be safety; security is a critical and related, but not
identical, attribute. &lt;a href=&quot;#user-content-fnref-safety&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 4&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-macro&quot;&gt;
&lt;p&gt;At a macro scale this gets complex, and we see plenty of imbalances in who
pays for these externalities; it&apos;s why we have regulations for some things,
frequently the result of socialized negative outcomes we want to avoid
repeating. &lt;a href=&quot;#user-content-fnref-macro&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 5&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><author>roguesys</author></item><item><title>Mimestream Private Push</title><link>https://mimestream.com/trust/private-push</link><guid isPermaLink="true">https://mimestream.com/trust/private-push</guid><pubDate>Sat, 07 Feb 2026 02:19:10 GMT</pubDate><content:encoded>&lt;p&gt;A nice bit of technical detail on how to achieve a privacy-preserving
implementation for email push. I hope many people would care about such details,
though realistically I&apos;m unsure of it. It&apos;s one of those things that can be done
quickly, easily, and poorly; or with a little more effort and attention, quite
well.&lt;/p&gt;</content:encoded><author>roguesys</author></item><item><title>Capabilities for agent delegation</title><link>https://niyikiza.com/posts/capability-delegation/</link><guid isPermaLink="true">https://niyikiza.com/posts/capability-delegation/</guid><pubDate>Fri, 09 Jan 2026 00:55:52 GMT</pubDate><content:encoded>&lt;p&gt;This is one of the more interesting ideas I&apos;ve come across in terms of securing
agentic workloads.&lt;/p&gt;</content:encoded><author>roguesys</author></item><item><title>Atuin and Tailscale and containers and 1Password</title><link>https://hackd.net/posts/atuin-and-tailscale-and-containers-and-1password/</link><guid isPermaLink="true">https://hackd.net/posts/atuin-and-tailscale-and-containers-and-1password/</guid><description>Self-hosting an Atuin sync server on macOS (feat. Tailscale, Podman containers, 1Password)</description><pubDate>Thu, 19 Jun 2025 17:47:24 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;m not sure how it took me this long to discover&lt;sup&gt;&lt;a href=&quot;#user-content-fn-1&quot; id=&quot;user-content-user-content-fnref-1&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; &lt;a href=&quot;https://atuin.sh&quot;&gt;atuin&lt;/a&gt; but I&apos;ve been
missing out for sure. Atuin is essentially (much) &quot;better shell history&quot; in a
bunch of ways, as it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;uses a database to enable more kinds of queries on the history&lt;/li&gt;
&lt;li&gt;has per-directory (or workspace) contextual filters, to better limit what is
shown&lt;/li&gt;
&lt;li&gt;syncs your shell history across devices, via an end-to-end encrypted sync
server&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That last feature was one of the things I was most excited about, although 2 is
really great as well in practice. While there is a public sync server available,
I thought I should run and host my own, given that&apos;s an option and I&apos;m that sort
of person.&lt;/p&gt;
&lt;p&gt;So, we&apos;re setting up a self-hosted Atuin sync server reachable via Tailscale,
running in containers (Podman, but Docker would work), with 1Password providing
secrets management, running on macOS. The completed configuration is available
&lt;a href=&quot;https://github.com/axtl/atuin-self-server&quot;&gt;on GitHub&lt;/a&gt;, if you want to skip ahead. The documentation from Atuin is
pretty thorough, but I did have to figure out a bit more glue, hence this post.&lt;/p&gt;
&lt;h2&gt;Containers (Podman, Docker etc.)&lt;/h2&gt;
&lt;p&gt;I use Podman as most container workloads don&apos;t need root. In this particular
setup, we&apos;ll deploy 3 containers via &lt;code&gt;podman-compose&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Atuin server&lt;/li&gt;
&lt;li&gt;the Postgres database for the Atuin server&lt;/li&gt;
&lt;li&gt;Tailscale routing so we can reach the Atuin server from our tailnet&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Setting up the Postgres and Atuin containers is pretty well-covered in the
&lt;a href=&quot;https://docs.atuin.sh/self-hosting/docker/#docker-compose&quot;&gt;official self-hosting docs&lt;/a&gt;, though I did have to figure out a
small thing and ended up submitting a &lt;a href=&quot;https://github.com/atuinsh/docs/pull/87&quot;&gt;PR&lt;/a&gt; to fix the upstream docs.&lt;/p&gt;
&lt;h3&gt;Atuin server&lt;/h3&gt;
&lt;p&gt;Annotated container config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;atuin:
  image: ghcr.io/atuinsh/atuin:latest
  # start after the DB and Tailscale
  depends_on:
    - db
    - tailscale
  restart: always
  command: server start
  # Make config persistent
  volumes:
    - &apos;./config:/config&apos;
  # Map the ports
  ports:
    - 8888:8888
  environment:
    # Listen an all interfaces.
    ATUIN_HOST: &apos;0.0.0.0&apos;
    # same port as above
    ATUIN_PORT: 8888
    # I don&apos;t keep registration open generally, but you probably need to set
    # this to &quot;true&quot; once to set up a first account.
    ATUIN_OPEN_REGISTRATION: &apos;false&apos;
    # These env vars will be injected later. Make sure that `db` matches your
    # database container&apos;s name!
    ATUIN_DB_URI: postgres://${ATUIN_DB_USERNAME}:${ATUIN_DB_PASSWORD}@db/${ATUIN_DB_NAME}
    RUST_LOG: info,atuin_server=debug
  network_mode: service:tailscale
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main trouble I had was the DB container name not matching the &lt;code&gt;ATUIN_DB_URI&lt;/code&gt;
string. Now that the upstream docs are fixed, it&apos;s unlikely you&apos;d run into this
issue.&lt;/p&gt;
&lt;p&gt;Given that we&apos;re using Tailscale to access the server, I don&apos;t worry about
setting up TLS.&lt;/p&gt;
&lt;h3&gt;Postgres database&lt;/h3&gt;
&lt;p&gt;Annotated container config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;db:
  image: postgres:17
  depends_on:
    - tailscale
  restart: unless-stopped
  volumes:
    # Don&apos;t remove permanent storage for index database files!
    - &apos;./database:/var/lib/postgresql/data/&apos;
    # If the WAL ever gets corrupt (e.g., due to abrupt container stops), fix it with:
    # docker run -it -v ./database:/var/lib/postgresql/data/ postgres:17 /bin/bash
    # su postgres &amp;#x26;&amp;#x26; cd /var/lib/postgresql/data &amp;#x26;&amp;#x26; pg_resetwal
  environment:
    POSTGRES_USER: ${ATUIN_DB_USERNAME}
    POSTGRES_PASSWORD: ${ATUIN_DB_PASSWORD}
    POSTGRES_DB: ${ATUIN_DB_NAME}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Tailscale&lt;/h3&gt;
&lt;p&gt;We want our Tailscale container to be able to route to and from our Atuin
server, while being able to independently authenticate to the network so that we
don&apos;t lose connectivity periodically. Tailscale docs are pretty good on &lt;a href=&quot;https://tailscale.com/blog/docker-tailscale-guide&quot;&gt;how to
set up containers&lt;/a&gt;, including OAuth configuration.&lt;/p&gt;
&lt;p&gt;Annotated container config:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;tailscale:
  image: ghcr.io/tailscale/tailscale:latest
  restart: always
  # TS_AUTHKEY from the OAuth config
  environment:
    TS_AUTHKEY: ${TS_AUTHKEY}
    TS_HOSTNAME: atuin
    TS_STATE_DIR: /var/lib/tailscale
    TS_EXTRA_ARGS: --advertise-tags=tag:container
  volumes:
    - &apos;./tailscale:/var/lib/tailscale/&apos;
  devices:
    - /dev/net/tun:/dev/net/tun
  cap_add:
    - net_admin
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1Password&lt;/h2&gt;
&lt;p&gt;I&apos;ve known for a while that 1Password has a CLI integration, allowing secrets
access from the command line, in scripts etc. This seemed like a good spot to
try that. My approach was to reference the secrets as environment variables (via
&lt;a href=&quot;http://mise.jdx.dev&quot;&gt;mise&lt;/a&gt;), have their values be paths into a 1Password vault, and start my
cluster by having &lt;code&gt;op&lt;/code&gt; inject the values at start-up time.&lt;/p&gt;
&lt;p&gt;So, for example, to set the database password for Atuin, in &lt;code&gt;mise.toml&lt;/code&gt; I have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-toml&quot;&gt;[env]
ATUIN_DB_PASSWORD = &quot;op://Personal/atuin/password&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;All together now (hopefully)&lt;/h2&gt;
&lt;p&gt;With the finished &lt;code&gt;compose.yaml&lt;/code&gt; for the cluster, and having set up the
respective secrets in 1Password, all that&apos;s left to do is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;op run -- podman compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This should prompt for authentication with 1Password, and then bring up all the
containers and have everything ready to go! As mentioned, you may need to allow
for registration once, to set up one account that you&apos;ll use. Or keep it on if
you&apos;re doing this for a team, friend group, polycule, etc. Given the server has
to be reached over Tailscale, it won&apos;t matter too much if you leave this on.&lt;/p&gt;
&lt;p&gt;If you have issues, check the logs for clues. I&apos;ve had the occasional issue with
the DB WAL.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;podman compose logs -n -t -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can also configure 1Password integration (and &lt;code&gt;mise&lt;/code&gt; populating the env) is
working as expected by printing the environment unmasked:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;op run --no-masking -- printenv # might want to pipe into grep ATUIN
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Atuin client&lt;/h2&gt;
&lt;p&gt;Get Atuin (the client) from the &lt;a href=&quot;https://atuin.sh&quot;&gt;official place&lt;/a&gt; or use &lt;code&gt;homebrew&lt;/code&gt; /
another package manager of your choice. You&apos;ll need a little configuration to
tell your client which is the right server to use.&lt;/p&gt;
&lt;p&gt;Here&apos;s my (annotated) configuration as an example, but only the &lt;code&gt;sync_address&lt;/code&gt;
is really needed. For the rest, find what works for you, and make sure to look
at the &lt;a href=&quot;https://docs.atuin.sh/self-hosting/docker/#docker-compose&quot;&gt;Atuin docs&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;# Tailnet address and correct port; remember this is HTTP.
sync_address = &quot;http://atuin.&amp;#x3C;your-tailnet&gt;.ts.net:8888&quot;
# Workspaces use per-git-repository history, rather than directory-only
workspaces = true
# Prefer to see the workspace history by default.
filter_mode_shell_up_key_binding = &quot;workspace&quot; # or global, host, directory, etc
# Run commands directly; tab to edit.
enter_accept = true
# Use Ctrl-0 .. Ctrl-9 instead of Alt-0 .. Alt-9 UI shortcuts; better on macOS.
ctrl_n_shortcuts = true
# Invert window?
invert = false
# Window style
style = &quot;auto&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point you should be able to run &lt;code&gt;atuin status&lt;/code&gt; and see you&apos;re green and
connecting to the server you&apos;ve just configured!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;user-content-footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-1&quot;&gt;
&lt;p&gt;It was probably
&lt;a href=&quot;https://hachyderm.io/@b0rk@jvns.ca/114083615859693297&quot;&gt;this comic&lt;/a&gt; by Julia
Evans that finally made me look. &lt;a href=&quot;#user-content-fnref-1&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><author>roguesys</author></item><item><title>macOS reflective code loading analysis</title><link>https://hackd.net/posts/macos-reflective-code-loading-analysis/</link><guid isPermaLink="true">https://hackd.net/posts/macos-reflective-code-loading-analysis/</guid><description>Fileless, artifact-free, in-memory code execution… not quite.</description><pubDate>Tue, 08 Feb 2022 02:45:24 GMT</pubDate><content:encoded>&lt;h2&gt;Background&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://attack.mitre.org/techniques/T1620/&quot;&gt;Reflective code loading&lt;/a&gt; is an interesting attack technique, useful when
there&apos;s a desire to conceal or protect the code that&apos;s being executed on a
system. This is primarily accomplished by avoiding the creation of any files on
disk (e.g., downloading the binary that&apos;s then going to run) or other execution
artifacts that would become indicators of behavior. Instead, code can be
downloaded and executed directly in the memory of an otherwise benign process.
While not completely interchangeable, sometimes we see this technique also
referred to as &quot;fileless&quot;, &quot;artifact-free&quot;, and/or &quot;in-memory (only)&quot; as those
are desirable characteristics.&lt;/p&gt;
&lt;p&gt;Probably the best known implementation of this technique on macOS relies on
functionality exposed by &lt;code&gt;dyld&lt;/code&gt;, and first documented in &lt;a href=&quot;https://www.wiley.com/en-us/The+Mac+Hacker%27s+Handbook-p-9780470395363&quot;&gt;&quot;The Mac Hacker&apos;s
Handbook&quot;&lt;/a&gt; by Dino Dai Zovi and Charlie Miller in 2009, though it&apos;s been
also discussed/popularized by &lt;a href=&quot;https://www.blackhat.com/docs/us-15/materials/us-15-Wardle-Writing-Bad-A-Malware-For-OS-X.pdf&quot;&gt;Patrick Wardle at BlackHat 2015&lt;/a&gt;, and by
&lt;a href=&quot;https://vimeo.com/215195101&quot;&gt;Stephanie Archibald at INFILTRATE &apos;17&lt;/a&gt;. The high-level approach is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;receive code over a socket&lt;/li&gt;
&lt;li&gt;if it&apos;s a binary, change a field in the Mach-O header from &lt;code&gt;MH_EXEC&lt;/code&gt; to
&lt;code&gt;MH_BUNDLE&lt;/code&gt; - needed for the subsequent steps as the necessary APIs expect a
bundle&lt;sup&gt;&lt;a href=&quot;#user-content-fn-bundle&quot; id=&quot;user-content-user-content-fnref-bundle&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;NSCreateObjectFileImageFromMemory&lt;/code&gt; to, well, create an object file from
the memory region that contains the binary&lt;/li&gt;
&lt;li&gt;use &lt;code&gt;NSLinkModule&lt;/code&gt; to link in the necessary shared libraries&lt;/li&gt;
&lt;li&gt;call functions or pass execution to the loaded code (after figuring out the
entry point, in the case it was actually a binary)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This technique has been used by malware authors rather recently, notably the
&lt;a href=&quot;https://objective-see.com/blog/blog_0x51.html&quot;&gt;Lazarus Group in the 2019 version of Apple Jeus&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There&apos;s just one thing: this technique hasn&apos;t been truly fileless for… some
time.&lt;/p&gt;
&lt;h2&gt;How it (really) works today&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://twitter.com/bluec0re&quot;&gt;@bluec0re&lt;/a&gt; initially got me looking into this in
depth, after noticing during a debug run that artifacts &lt;strong&gt;are&lt;/strong&gt;, in fact,
created when these specific functions are called. Given the public perception
around this in-memory loading technique, this was a bit of a surprise, so I
spent some time doing a bit more research and putting it together for this
analysis.&lt;/p&gt;
&lt;p&gt;I used &lt;a href=&quot;https://twitter.com/its_a_feature_&quot;&gt;@its_a_feature&lt;/a&gt;&apos;s simple
&lt;a href=&quot;https://github.com/its-a-feature/macos_execute_from_memory&quot;&gt;macos_execute_from_memory&lt;/a&gt; PoC for testing (there&apos;s also Archibald&apos;s
&lt;a href=&quot;https://github.com/CylanceVulnResearch/osx_runbin&quot;&gt;PoC&lt;/a&gt;, for historical reference). For convenience/ease of
iteration, these PoCs do actually load a target binary from disk, rather than
over the network, but the execution is meant to be in-memory only.&lt;/p&gt;
&lt;p&gt;To figure out what&apos;s going on, a good starting place is to run the code while
printing anything that has to do with &lt;code&gt;dyld&lt;/code&gt;, given that&apos;s where the two
functions are. There are some helpful environment variables to set before
running the code:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;clang -g -o main main.c
DYLD_PRINT_APIS=1 DYLD_PRINT_LIBRARIES=1 ./main
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;// some output ellided
dyld[80184]: NSCreateObjectFileImageFromMemory(0x105010000, 0x00008258)
dyld[80184]: NSCreateObjectFileImageFromMemory() copy 0x105010000 to 0x10501c000
dyld[80184]: NSLinkModule(0x6000029d4270, module)
dyld[80184]: dlopen(&quot;/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-VTd6S38q&quot;, 0x80000080)
dyld[80184]: &amp;#x3C;DDBBB7CE-78F7-3E78-AD09-2C79ED030E2A&gt; /private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-VTd6S38q
dyld[80184]:       dlopen(NSCreateObjectFileImageFromMemory-VTd6S38q) =&gt; 0x20a24d4a0
dyld[80184]: NSLinkModule(0x6000029d4270, module) =&gt; 0x20a24d4a0
dyld[80184]: NSLookupSymbolInModule(0x20a24d4a0, _execute)
dyld[80184]: NSLookupSymbolInModule(0x20a24d4a0, _execute) =&gt; 0x105037f84
old timey mode: Executed!
dyld[80184]: NSUnLinkModule(0x20a24d4a0)
dyld[80184]: dlclose(0x20a24d4a0)
dyld[80184]: NSDestroyObjectFileImage(0x6000029d4270)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks like &lt;code&gt;NSLinkModule&lt;/code&gt; uses &lt;code&gt;dlopen&lt;/code&gt; to load the
&lt;code&gt;NSCreateObjectFileImageFromMemory-VTd6S38q&lt;/code&gt; file from a temporary location. To
confirm and get a bit more detail, I monitored the execution of &lt;code&gt;./main&lt;/code&gt; using
Patrick Wardle&apos;s &lt;a href=&quot;https://objective-see.com/products/utilities.html&quot;&gt;File Monitor&lt;/a&gt; to see what files are being accessed. To
narrow the output, I&apos;m piping this through &lt;code&gt;jq&lt;/code&gt; to filter out for events related
to the PoC, and extracting only the type and the file destination for each
event:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;sudo /Applications/FileMonitor.app/Contents/MacOS/FileMonitor | \
jq  &apos;.
   | select(.file.process.path | contains(&quot;macos_execute_from_memory/main&quot;))
   | {event: .event, dest: .file.destination}&apos; | \
tee filemon-filtered.json # also saving for futher analysis
# start ./main in another terminal, Ctrl-C the FileMonitor process when done
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_OPEN&quot;,
  &quot;dest&quot;: &quot;…/macos_execute_from_memory/test.bundle&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_CLOSE&quot;,
  &quot;dest&quot;: &quot;…/macos_execute_from_memory/test.bundle&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_CREATE&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_WRITE&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_CLOSE&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_OPEN&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_CLOSE&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
// open-close twice more
{
  &quot;event&quot;: &quot;ES_EVENT_TYPE_NOTIFY_UNLINK&quot;,
  &quot;dest&quot;: &quot;/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-7zEgh32K&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;During the execution of &lt;code&gt;./main&lt;/code&gt; a file is created, accessed a few times, then
deleted. The name changes slightly with every run (the &lt;code&gt;7zEgh32K&lt;/code&gt; above,
compared to &lt;code&gt;VTd6S38q&lt;/code&gt; previously) and based on the path it&apos;s fairly obvious
this is a temporary file. I wanted to understand more about when it&apos;s created,
and if its presence means this particular technique is no longer as interesting
as we once thought: it can be replaced with a much less brittle combo of
&lt;code&gt;dlopen&lt;/code&gt; and &lt;code&gt;dlsym&lt;/code&gt;, but it&apos;s also not artifact-free.&lt;/p&gt;
&lt;p&gt;So next I ran &lt;code&gt;./main&lt;/code&gt; in &lt;code&gt;lldb&lt;/code&gt; with a breakpoint on
&lt;code&gt;NSCreateObjectFileImageFromMemory&lt;/code&gt;, stepping through instructions to figure out
what&apos;s going on:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;(lldb) b NSCreateObjectFileImageFromMemory
Breakpoint 1: where = libdyld.dylib`NSCreateObjectFileImageFromMemory, address = 0x0000000180314fa0
(lldb) r
Process 94604 launched: &apos;macos_execute_from_memory/main&apos; (arm64)
Process 94604 stopped
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = breakpoint 1.1
    frame #0: 0x000000018e3b8fa0 libdyld.dylib`NSCreateObjectFileImageFromMemory
libdyld.dylib`NSCreateObjectFileImageFromMemory:
-&gt;  0x18e3b8fa0 &amp;#x3C;+0&gt;:  mov    x3, x2
    0x18e3b8fa4 &amp;#x3C;+4&gt;:  mov    x2, x1
    0x18e3b8fa8 &amp;#x3C;+8&gt;:  mov    x1, x0
    0x18e3b8fac &amp;#x3C;+12&gt;: adrp   x8, 366208
Target 0: (main) stopped.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After for a while we eventually get to
&lt;code&gt;dyld4::APIs::NSCreateObjectFileImageFromMemory(void const*, unsigned long, __NSObjectFileImage**)&lt;/code&gt;
which superficially looks like the function called in &lt;code&gt;main.c&lt;/code&gt; but, as we&apos;ll see
a bit later on, is not.&lt;/p&gt;
&lt;p&gt;Continuing on we reach first
&lt;code&gt;libdyld.dylib\&lt;/code&gt;NSCreateObjectFileImageFromMemory&lt;code&gt;and then&lt;/code&gt;dyld`dyld4::APIs::NSLinkModule(__NSObjectFileImage*,
char const*, unsigned
int)&lt;code&gt;in a similar fashion. Here we&apos;ll see a call to&lt;/code&gt;mkstemp` that&apos;s a give-away
we&apos;re likely creating a temporary file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;(lldb)
Process 97220 stopped
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = instruction step into
    frame #0: 0x000000018e3bdea8 libdyld.dylib`dyld4::LibSystemHelpers::getenv(char const*) const
libdyld.dylib`dyld4::LibSystemHelpers::getenv:
-&gt;  0x18e3bdea8 &amp;#x3C;+0&gt;: mov    x0, x1
    0x18e3bdeac &amp;#x3C;+4&gt;: b      0x18e3c329c               ; symbol stub for: getenv

libdyld.dylib`dyld4::LibSystemHelpers::mkstemp:
    0x18e3bdeb0 &amp;#x3C;+0&gt;: mov    x0, x1
    0x18e3bdeb4 &amp;#x3C;+4&gt;: b      0x18e3c336c               ; symbol stub for: mkstemp
Target 0: (main) stopped.
(lldb)
Process 97220 stopped
* thread #1, queue = &apos;com.apple.main-thread&apos;, stop reason = instruction step into
    frame #0: 0x000000018e3bdeac libdyld.dylib`dyld4::LibSystemHelpers::getenv(char const*) const + 4
libdyld.dylib`dyld4::LibSystemHelpers::getenv:
-&gt;  0x18e3bdeac &amp;#x3C;+4&gt;: b      0x18e3c329c               ; symbol stub for: getenv

libdyld.dylib`dyld4::LibSystemHelpers::mkstemp:
    0x18e3bdeb0 &amp;#x3C;+0&gt;: mov    x0, x1
    0x18e3bdeb4 &amp;#x3C;+4&gt;: b      0x18e3c336c               ; symbol stub for: mkstemp

libdyld.dylib`dyld4::LibSystemHelpers::getTLVGetAddrFunc:
    0x18e3bdeb8 &amp;#x3C;+0&gt;: adrp   x16, -5
Target 0: (main) stopped.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Right after the return from &lt;code&gt;mkstemp&lt;/code&gt;, I checked the temporary file location to
see a file created there:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;fd NSCreateObjectFileImageFromMemory /private/var/folders/
/private/var/folders/q8/28dylf2973q_22bqzy2_lplr0000gn/T/NSCreateObjectFileImageFromMemory-wySJJ7WJ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SHA-256 checksum of this temp file matched the SHA-256 checksum of the file
that was loaded in-memory by the PoC, so it&apos;s pretty clear at this point that
&lt;code&gt;NSLinkModule&lt;/code&gt; writes the file out, then uses &lt;code&gt;dlopen&lt;/code&gt; to load it back to do the
rest. &lt;code&gt;dlopen&lt;/code&gt; is part of a stable API, unlike the long-deprecated
&lt;code&gt;NSCreateObjectFileImageFromMemory&lt;/code&gt; and &lt;code&gt;NSLinkModule&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Tracing the source code&lt;/h2&gt;
&lt;p&gt;Apple publishes &lt;a href=&quot;https://github.com/apple-oss-distributions/dyld&quot;&gt;the source code to dyld&lt;/a&gt;, though with some delay
after the corresponding OS release that introduces a new version. For example,
the latest available source is &lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/tree/rel/dyld-852&quot;&gt;852&lt;/a&gt;, which corresponds to macOS 11, but
the current macOS 12.2 version is &lt;code&gt;dyld-941.5&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-fish&quot;&gt;strings /usr/lib/dyld | rg &quot;dyld-\b\d\d\d\b&quot; # also /System/Library/dyld/dyld_shared_cache_arm64e
@(#)PROGRAM:dyld  PROJECT:dyld-941.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An interesting fact is that the function we saw earlier as
&lt;code&gt;dyld4::APIs::NSCreateObjectFileImageFromMemory&lt;/code&gt; is from the &lt;code&gt;dyld4&lt;/code&gt; namespace,
which is not present in any of the source code releases (there&apos;s only &lt;code&gt;dyld&lt;/code&gt; and
&lt;code&gt;dyld3&lt;/code&gt; respectively.) During debugging I saw a few functions called that are
part of the &lt;code&gt;dyld3&lt;/code&gt; namespace, so my guess is that all of these will coexist, at
least for a while. For the purposes of this analysis, I&apos;ll focus on the
&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/tree/rel/dyld-852&quot;&gt;852&lt;/a&gt; version of &lt;code&gt;dyld&lt;/code&gt;, but I&apos;ll keep an eye out for whenever 941 drops,
to check out &lt;code&gt;dyld4&lt;/code&gt; there.&lt;/p&gt;
&lt;p&gt;There are a few implementations of &lt;code&gt;NSCreateObjectFileImageFromMemory&lt;/code&gt; in the
source for &lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/tree/rel/dyld-852&quot;&gt;852&lt;/a&gt;, in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/blob/rel/dyld-852/src/dyldAPIs.cpp#L904&quot;&gt;&lt;code&gt;src/dyldAPIs.cpp&lt;/code&gt;&lt;/a&gt;
added with the &lt;code&gt;dyld-43&lt;/code&gt; release, about 17 years ago.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/blob/rel/dyld-852/src/dyldAPIsInLibSystem.cpp#L778&quot;&gt;&lt;code&gt;src/dyldAPIsInLibSystem.cpp&lt;/code&gt;&lt;/a&gt;
added by &lt;code&gt;dyld-519.2.1&lt;/code&gt; in 2017&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/blob/rel/dyld-852/dyld3/APIs_macOS.cpp#L94&quot;&gt;&lt;code&gt;dyld3/APIs_macOS.cpp&lt;/code&gt;&lt;/a&gt;
where the relevant portion was also added with &lt;code&gt;dyld-519.2.1&lt;/code&gt; in 2017.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same is true for &lt;code&gt;NSLinkModule&lt;/code&gt;, just a few lines below in those same files.&lt;/p&gt;
&lt;p&gt;It looks like &lt;code&gt;gUseDyld3&lt;/code&gt; controls whether the version that&apos;s being used is the
one in the &lt;code&gt;dyld3&lt;/code&gt; namespace, versus the &quot;original&quot; from the &lt;code&gt;dyld-43&lt;/code&gt; release.
There are a few places this is assigned to throughout the code base, though only
one spot where it&apos;s set to &lt;code&gt;true&lt;/code&gt;, in
&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/blob/rel/dyld-852/dyld3/libdyldEntryVector.cpp#L61-L82&quot;&gt;&lt;code&gt;libdyldEntryVector.cpp&lt;/code&gt;&lt;/a&gt;.
Following the path further through the build config files in Xcode and the
source itself, I believe the entry vector is a dependency to building
&lt;code&gt;libdyld.dylib&lt;/code&gt;. So that&apos;s how we get to the code path that uses the new
(&lt;code&gt;dyld3&lt;/code&gt;) implementation of &lt;code&gt;NSCreateObjectFileImageFromMemory&lt;/code&gt; and
&lt;code&gt;NSLinkModule&lt;/code&gt;, the latter of which creates a temp file and loads it via
&lt;code&gt;dlopen&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;How long has this been the case?&lt;/h2&gt;
&lt;p&gt;I admit that, while researching, I went through a few different theories as to
when this behavior changed. Given I hadn&apos;t seen anyone discuss it, seemed like a
recent change. But when I began tracing through the &lt;code&gt;dyld&lt;/code&gt; source, I figured
maybe this changed around 2017 when the initial &lt;code&gt;dyld3&lt;/code&gt; code was added, and
somehow nobody noticed. I even went to search VirusTotal for files named
according to this pattern, and found quite a few, dating back to 2018.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./rcl-vti-tmp.png&quot; alt=&quot;VirusTotal search results for name:NSCreateObjectFileImageFromMemory*&quot;&gt;&lt;/p&gt;
&lt;p&gt;Somehow though, nobody noticing just… didn&apos;t seem right?&lt;/p&gt;
&lt;h3&gt;&quot;Carbon&quot; dating &lt;code&gt;dyld&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;While version 941.5 isn&apos;t out, I was curious to check out how execution of this
code path looks now, so I opened my local copy of &lt;code&gt;dyld&lt;/code&gt; in Ghidra. Searching
for &lt;code&gt;NSLinkModule&lt;/code&gt; returned a single location, in the &lt;code&gt;dyld4&lt;/code&gt; namespace as we
expected. The decompiled&lt;sup&gt;&lt;a href=&quot;#user-content-fn-demangler&quot; id=&quot;user-content-user-content-fnref-demangler&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; code does bear resemblance to the source in
version
&lt;a href=&quot;https://github.com/apple-oss-distributions/dyld/blob/rel/dyld-852/dyld3/APIs_macOS.cpp#L147-L214&quot;&gt;852&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;void dyld4::APIs::NSLinkModule(__NSObjectFileImage*, char const*, unsigned int)

{
  int iVar1;
  char *pcVar2;
  size_t sVar3;
  undefined8 uVar4;
  long *plVar5;
  char acStack1096 [1024];
  long local_48;

  local_48 = ___stack_chk_guard;
  if (*(char *)(param_1[1] + 0x90) != &apos;\0&apos;) {
    dyld4::RuntimeState::log(param_1,&quot;NSLinkModule(%p, %s)\n&quot;);
  }
  if (param_2[1] == (char *)0x0) {
    uVar4 = 0;
  }
  else {
    *param_2 = (char *)0x0;
    pcVar2 = (char *)(**(code **)(*(long *)param_1[0xd] + 0x80))((long *)param_1[0xd],&quot;TMPDIR&quot;);
    if ((pcVar2 == (char *)0x0) || (sVar3 = _strlen(pcVar2), sVar3 &amp;#x3C; 3)) {
      _strlcpy(acStack1096,&quot;/tmp/&quot;,0x400);
    }
    else {
      _strlcpy(acStack1096,pcVar2,0x400);
      sVar3 = _strlen(pcVar2);
      if (pcVar2[sVar3 - 1] != &apos;/&apos;) {
        _strlcat(acStack1096,&quot;/&quot;,0x400);
      }
    }
    _strlcat(acStack1096,&quot;NSCreateObjectFileImageFromMemory-XXXXXXXX&quot;,0x400);
    iVar1 = (**(code **)(*(long *)param_1[0xd] + 0x88))((long *)param_1[0xd],acStack1096);
    if (iVar1 != -1) {
      pcVar2 = (char *)_pwrite(iVar1,param_2[1],(size_t)param_2[2],0);
      if (pcVar2 == param_2[2]) {
        plVar5 = (long *)param_1[0xd];
        sVar3 = _strlen(acStack1096);
        pcVar2 = (char *)(**(code **)(*plVar5 + 8))(plVar5,sVar3 + 1);
        *param_2 = pcVar2;
        _strcpy(pcVar2,acStack1096);
      }
      _close(iVar1);
    }
    uVar4 = 0x80000080;
  }
  if (*param_2 != (char *)0x0) {
    pcVar2 = (char *)(**(code **)(*param_1 + 0x70))(param_1,*param_2,uVar4);
    param_2[4] = pcVar2;
    if (pcVar2 != (char *)0x0) {
      pcVar2 = (char *)dyld4::Loader::loadAddress(dyld4::RuntimeState&amp;#x26; pcVar2 &gt;&gt; 1,param_1);
      param_2[3] = pcVar2;
      if (param_2[1] != (char *)0x0) {
        _unlink(*param_2);
      }
      if (*(char *)(param_1[1] + 0x90) != &apos;\0&apos;) {
        dyld4::RuntimeState::log(param_1,&quot;NSLinkModule(%p, %s) =&gt; %p\n&quot;);
      }
      pcVar2 = param_2[4];
      goto LAB_00029c40;
    }
    if (*(char *)(param_1[1] + 0x90) != &apos;\0&apos;) {
      (**(code **)(*param_1 + 0x80))(param_1);
      dyld4::RuntimeState::log(param_1,&quot;NSLinkModule(%p, %s) =&gt; NULL (%s)\n&quot;);
    }
  }
  pcVar2 = (char *)0x0;
LAB_00029c40:
  if (___stack_chk_guard != local_48) {
                    /* WARNING: Subroutine does not return */
    ___stack_chk_fail(pcVar2);
  }
  return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Line 32 confirms the template for the temporary name is the same as what we saw
during execution.&lt;/p&gt;
&lt;p&gt;Line 51 would be around where we expect a &lt;code&gt;dlopen&lt;/code&gt; call to happen, although in
this case there&apos;s an indirection to resolve an address for a function from a
jump table. Unfortunately Ghidra isn&apos;t able to resolve that table, so we&apos;re left
speculating if that&apos;ll end up being &lt;code&gt;dlopen&lt;/code&gt; or not. But I&apos;m pretty sure that&apos;s
where we end up eventually, given what we&apos;ve observed during live debugging.&lt;/p&gt;
&lt;p&gt;I wanted to compare this version with some of the older ones, but lacking at the
moment any non-Monterey installs, I had to resort to grabbing copies of
&lt;code&gt;/usr/lib/dyld&lt;/code&gt; from VMs and, eventually, download them from VirusTotal. I was
quite surprised to see that every one of them, as far back as 551, and including
852, do in fact use a non-&lt;code&gt;dlopen&lt;/code&gt; version of the code, basically the true
fileless implementation. I can&apos;t rule out that maybe all of these versions are
off VMs, where maybe something is different with &lt;code&gt;dyld&lt;/code&gt;… I think it&apos;s rather
curious that I didn&apos;t find any that use the &lt;code&gt;dyld3&lt;/code&gt; implementation.&lt;/p&gt;
&lt;p&gt;The thing that throws me off the most—if this behavior really was only enabled
in Monterey—is that so many files matching the &lt;code&gt;mkstemp&lt;/code&gt; pattern of
&lt;code&gt;/private/var/tmp/NSCreateObjectFileImageFromMemory-XXXXXX&lt;/code&gt; show up in
VirusTotal since 2018. I&apos;m not convinced either way just yet.&lt;/p&gt;
&lt;h2&gt;Future work&lt;/h2&gt;
&lt;p&gt;The predictable file name and location means XDR can easily grab these files
whenever they&apos;re created, as it would be exactly the code to run that&apos;s written
out (so any obfuscation would happen before, most likely). There are some benign
uses of these APIs, certainly, but there are bound to be some cool findings too.
I haven&apos;t spent any time looking through such files on VirusTotal to see if
anything good is there, but it&apos;s certainly tempting.&lt;/p&gt;
&lt;p&gt;Once the code for the latest version of &lt;code&gt;dyld&lt;/code&gt; is out, I hope to better
understand the specifics of how the &lt;code&gt;dyld4&lt;/code&gt; namespace is enabled for use. Of
course, I also want to see what direction Apple is taking for these APIs, given
the new namespace.&lt;/p&gt;
&lt;p&gt;On the offensive tooling side, I&apos;m curious if it&apos;s possible to develop a pure
in-memory (true fileless) way to execute code on macOS. Mostly that seems to
require writing a Mach-O loader, and I bet significant parts of the logic can be
borrowed from both the deprecated code, and the &lt;code&gt;dlopen&lt;/code&gt; implementation.&lt;/p&gt;
&lt;p&gt;If you happen to come across any interesting implementations of true in-memory
loaders for macOS, have figured out some of the missing parts I didn&apos;t get to in
this analysis, or found any issues/mistakes etc. please let me know via
&lt;a href=&quot;https://twitter.com/roguesys&quot;&gt;@roguesys&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thanks for reading!&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;user-content-footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-bundle&quot;&gt;
&lt;p&gt;If we&apos;re to go by Apple&apos;s old documentation, the motivation for this API is
to allow loading of plug-ins in applications that might want them. Bundles
(in the Mach-O sense) fit this purpose rather well, though since it&apos;s
trivial to turn a binary into a bundle as shown &lt;a href=&quot;#user-content-fnref-bundle&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-demangler&quot;&gt;
&lt;p&gt;The GNU demangler that&apos;s part of Ghidra threw a bunch of errors and did not
demangle any names, so I ran the decompiled code through &lt;code&gt;c++filt&lt;/code&gt; manually
and adjusted as necessary. Still, I may have made some errors re:
parameters, which was fine for my purposes but may not be for yours. &lt;a href=&quot;#user-content-fnref-demangler&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><author>roguesys</author></item><item><title>Reordering Architecture Headers in a Universal Mach-O Binary</title><link>https://hackd.net/posts/reorder-arch-headers-universal-macho/</link><guid isPermaLink="true">https://hackd.net/posts/reorder-arch-headers-universal-macho/</guid><description>A simple way to re-order platform architectures in universal Mach-O binaries.</description><pubDate>Thu, 08 Jul 2021 16:24:49 GMT</pubDate><content:encoded>&lt;p&gt;When I started trialling VS Code and ended up
&lt;a href=&quot;/posts/vscode-licensing-ext/&quot;&gt;distracted reverse-engineering extensions&lt;/a&gt;, my
goal was to document a simple mechanism to re-order the way architectures are
presented to the loader in universal Mach-O binaries. This is that post.&lt;/p&gt;
&lt;p&gt;Why would this be necessary? For me, it was the need to test a custom Mach-O
loader, to make sure it can handle certain special cases. This exercise ended up
being a straightforward, practical introduction to the Mach-O file format, which
in some ways was more valuable, any why I decided to share the process.&lt;/p&gt;
&lt;h2&gt;Intro to universal Mach-O binaries&lt;/h2&gt;
&lt;p&gt;This post focuses on universal (a.k.a. &quot;fat&quot;) Mach-O binaries, unless otherwise
noted. Specifically, those containing object files for &lt;code&gt;x86_64&lt;/code&gt; (Intel) and
&lt;code&gt;arm64&lt;/code&gt;&lt;sup&gt;&lt;a href=&quot;#user-content-fn-arm64&quot; id=&quot;user-content-user-content-fnref-arm64&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; (Apple Silicon) architectures. There are other potential
architectures inside such binaries, since Apple also used the format when moving
from PowerPC to Intel. As we&apos;ll see, the specific architectures don&apos;t really
matter when it comes to reordering, but since I did not test with older
binaries, I won&apos;t claim this works as-is for those combinations.&lt;/p&gt;
&lt;p&gt;The header describing universal binaries is defined in
&lt;a href=&quot;https://github.com/apple/darwin-xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h&quot;&gt;&lt;code&gt;mach-o/fat.h&lt;/code&gt;&lt;/a&gt;,
and the essential information is rather short:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c++&quot;&gt;#define FAT_MAGIC       0xcafebabe
#define FAT_CIGAM       0xbebafeca      /* NXSwapLong(FAT_MAGIC) */

struct fat_header {
        uint32_t        magic;          /* FAT_MAGIC */
        uint32_t        nfat_arch;      /* number of structs that follow */
};

struct fat_arch {
        cpu_type_t      cputype;        /* cpu specifier (int) */
        cpu_subtype_t   cpusubtype;     /* machine specifier (int) */
        uint32_t        offset;         /* file offset to this object file */
        uint32_t        size;           /* size of this object file */
        uint32_t        align;          /* alignment as a power of 2 */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each universal binary starts with a header that is always stored in
&lt;a href=&quot;https://github.com/apple/darwin-xnu/blob/main/EXTERNAL_HEADERS/mach-o/fat.h#L36-L37&quot;&gt;big-endian&lt;/a&gt;
format on disk, and which contains the magic number (&lt;code&gt;0xcafebabe&lt;/code&gt;) and the total
number of architectures the binary contains. Headers for each architecture then
follow, specifying the CPU, its subtype, an absolute offset into the binary for
the object file corresponding to the architecture, that object file&apos;s size, and
an alignment.&lt;/p&gt;
&lt;p&gt;To list architectures in a binary on macOS, either the &lt;code&gt;file&lt;/code&gt; utility, or
&lt;code&gt;lipo -archs&lt;/code&gt; can be used; the output is different, but both parse and display
the headers, in the order they&apos;re present in the binary.&lt;/p&gt;
&lt;h2&gt;Reordering architecture headers&lt;/h2&gt;
&lt;p&gt;The order as displayed is in fact entirely determined by how these headers are
laid out inside the binary. Think of the universal binary as a container for its
multiple architectures. Each one of those is a (non-universal) binary in its own
right. In fact, &lt;code&gt;lipo&lt;/code&gt; can be used to &quot;thin&quot; a binary to only a specified
architecture, which essentially parses these headers, identifies the one
corresponding to the architecture that was requested, skips to the offset
indicated in the header, and dumps the next &lt;code&gt;size&lt;/code&gt; bytes out.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌────────────────────────────┐
│ ┌──────────┐               │
│┌┤fat_header├──────────────┐│
││└──────────┘              ││
││┌────────────────────────┐││
│││magic                   │││
││├────────────────────────┤││
│││num archs               │││
││└────────────────────────┘││
│└──────────────────────────┘│
│ ┌────────────┐             │
│┌┤arch_headers├────────────┐│
││└────────────┘            ││
││┌──────────────────────┬─┐││
│││ cputype              │0│││
│││ cpusubtye            └─┤││
│││ offset                 │││
│││ size                   │││
│││ align                  │││
││├──────────────────────┬─┤││
│││ cputype              │1│││
│││ cpusubtye            └─┤││
│││ offset                 │││
│││ size                   │││
│││ align                  │││
││└──────┬───────┬─────────┘││
││       │   …   │          ││
││       └───────┘          ││
│└──────────────────────────┘│
└────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This means that, in order to reorder the way architectures in the binary are
processed, the simplest approach is to reorder the headers to get the desired
sequence. It is possible (but insufficient) to shift the actual architecture
code in the binary as well, but there&apos;s no benefit, except a (dubious)
performance claim that if the offset is earlier, there&apos;s less seeking necessary
to get to the relevant code. That&apos;s out of scope for this post, but certainly
something one can try.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lipo&lt;/code&gt; has a way to carve out architectures from a binary, and also supports
creating a universal binary from specific architecture binaries, but the
ordering is fixed, based on a sort by alignment, to save space&lt;sup&gt;&lt;a href=&quot;#user-content-fn-lipo-create&quot; id=&quot;user-content-user-content-fnref-lipo-create&quot; data-footnote-ref aria-describedby=&quot;user-content-footnote-label&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;, so
it&apos;s unsuitable for reordering. The best option is a small tool that can shift
the arch headers as needed. Fortunately, we don&apos;t need to &lt;em&gt;understand&lt;/em&gt; the arch
headers (not that it&apos;s hard) for this task, it&apos;s sufficient to figure out how
many there are, read 20 bytes for each, and save to a new file with a different
ordering.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;/samples/macho-header-swap.py&quot;&gt;Python code&lt;/a&gt; is fairly straightforward,
though it doesn&apos;t allow for anything fancy like specifying what order the
architectures should be output in etc. Simply shifts &quot;left&quot;, so for example for
the &lt;code&gt;file&lt;/code&gt; binary, which has the following 3 architectures on my macOS install,
the first shift would go from &lt;code&gt;0 1 2&lt;/code&gt; to &lt;code&gt;1 2 0&lt;/code&gt; etc. as indicated below:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;arch     | x86_64 arm64 arm64e -&gt; arm64 arm64e x86_64 -&gt; arm64e x86_64 arm64
position |    0     1      2   -&gt;   1      2      0   -&gt;    2      0     1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Obviously (and reassuringly) shifting all the way to the original yields the
same SHA256 hash for the binary.&lt;/p&gt;
&lt;p&gt;To begin, read in the &lt;code&gt;fat_header&lt;/code&gt; to confirm this has multiple architectures,
then read how many architectures we&apos;re dealing with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;offset = 0
magic = struct.unpack(&quot;&gt;I&quot;, inbin.read(4))[0]
offset += 4
inbin.seek(offset)

if magic != MAGIC:
    print(&quot;not a universal binary&quot;)
    return 2

# next value tells us how many archs the binary contains
narchs = struct.unpack(&quot;&gt;I&quot;, inbin.read(4))[0]
offset += 4
inbin.seek(offset)

if narchs &amp;#x3C; 2:
    print(&quot;not enough archs: %d&quot;, narchs)
    return 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we know how many architectures are in the binary, we can simply read their
headers, 20 bytes at the time, but we do not need to parse them. This would be
different if we also had to reorder the object files inside the binary, since
offsets would need to be adjusted, but that&apos;s not the case here.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;headers = []
for _ in range(narchs):
    headers.append(inbin.read(20))
    offset += 20  # promise
    inbin.seek(offset)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then write to the output binary, maintaining the &lt;code&gt;fat_header&lt;/code&gt; and using a simple
shift in the loop to write the arch headers themselves. Finally, copy the
remainder of the file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# write back magic
outbin.write(struct.pack(&quot;&gt;I&quot;, magic))
# write out how many narchs
outbin.write(struct.pack(&quot;&gt;I&quot;, narchs))
# put headers in, shifting &quot;left&quot;
for idx in range(1, narchs + 1):
    outbin.write(headers[idx % narchs])
# inneficiently copy the remaining bytes
outbin.write(inbin.read())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Final notes&lt;/h2&gt;
&lt;p&gt;There is not much to this once there&apos;s an understanding of how the headers in
the binary are laid out. This is somewhat documented in the open source code
Apple publishes, though resources like Jonathan Levin&apos;s books make for much
easier references to learn from.&lt;/p&gt;
&lt;p&gt;As for follow-up work (besides improved error checking) a better way to
understand the Mach-O format is to also enable object file reordering in the
binary, and/or have a way to specify the sequence for header reordering.&lt;/p&gt;
&lt;section data-footnotes class=&quot;footnotes&quot;&gt;&lt;h2 class=&quot;sr-only&quot; id=&quot;user-content-footnote-label&quot;&gt;Footnotes&lt;/h2&gt;
&lt;ol&gt;
&lt;li id=&quot;user-content-user-content-fn-arm64&quot;&gt;
&lt;p&gt;Frequently this will in fact be &lt;code&gt;arm64e&lt;/code&gt;, but in most cases discussed here,
it is not necessary to make the distinction. &lt;a href=&quot;#user-content-fnref-arm64&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 1&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id=&quot;user-content-user-content-fn-lipo-create&quot;&gt;
&lt;p&gt;Search for &lt;code&gt;* create_fat&lt;/code&gt; to find the function in
&lt;a href=&quot;https://opensource.apple.com/source/cctools/cctools-973.0.1/misc/lipo.c.auto.html&quot;&gt;lipo.c&lt;/a&gt; &lt;a href=&quot;#user-content-fnref-lipo-create&quot; data-footnote-backref=&quot;&quot; aria-label=&quot;Back to reference 2&quot; class=&quot;data-footnote-backref&quot;&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;</content:encoded><author>roguesys</author></item><item><title>Reverse Engineering License Validation in a VS Code Extension</title><link>https://hackd.net/posts/vscode-licensing-ext/</link><guid isPermaLink="true">https://hackd.net/posts/vscode-licensing-ext/</guid><description>A short example of reverse engineering the license check for a fairly compact VS Code extension.</description><pubDate>Mon, 31 May 2021 16:47:11 GMT</pubDate><content:encoded>&lt;p&gt;I recently switched on a trial-basis to VS Code, and spent some time tweaking a
few things by way of extensions. VS Code has most of the features I care about
by default, but there are a few things I wanted a bit different, which is how I
eventually came about this particular (unnamed) extension. After using it for a
while, the extension nags the user for a license. The interface to enter it
requires the user provides an email address and the license key provided by the
vendor upon payment. If valid, the extension unlocks permanently (on that
installation of VS Code (it does not appear the license state syncs between
connected VS Code instances).&lt;/p&gt;
&lt;p&gt;Since this all happens client-side, I wondered what kind of mechanisms are there
to do so securely, and if there are any APIs in VS Code that enable this sort of
thing. So here we are.&lt;/p&gt;
&lt;h2&gt;Disclaimer&lt;/h2&gt;
&lt;p&gt;I did this after purchasing the extension, for fun, out of curiosity, and to
check my assumptions about implementing such code to begin with (e.g., &quot;is there
a secure storage/settings API in VS Code?&quot;). As a newcomer to the editor and its
ecosystem, I took this opportunity to reverse engineer some extension code so I
can familiarize myself to implementation patterns, as well.&lt;/p&gt;
&lt;h2&gt;Walkthrough&lt;/h2&gt;
&lt;p&gt;While registering the extension with the license key, I did trace process and
file accesses using &lt;a href=&quot;https://objective-see.com/products/utilities.html#ProcessMonitor&quot;&gt;Process Monitor&lt;/a&gt; and &lt;a href=&quot;https://objective-see.com/products/utilities.html#FileMonitor&quot;&gt;File Monitor&lt;/a&gt; from
Objective-See. These didn&apos;t end up helping, but I think it&apos;s worth collecting
traces like that early on. Given that I had a valid license to begin with, I
figured these traces might help see which files etc. change during the
activation process.&lt;/p&gt;
&lt;p&gt;The source code for VS Code extensions (on macOS and Linux) is in
&lt;code&gt;$HOME/.vscode/extensions/&amp;#x3C;ext_name&gt;&lt;/code&gt;, and in particular for this extension the
two important files were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;js/main.js&lt;/code&gt; is the extension loader as required by VS Code;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;js/app.js&lt;/code&gt; contains the minified source code of the extension.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I opened the second file in a VS Code buffer and ran Prettier to make it
somewhat more readable. Since a lot of function and variable names get removed
or changed to nondescript ones during the minification process, it&apos;s still not
amazing to work with, but at least the code is not all on one line.&lt;/p&gt;
&lt;p&gt;One thing that stands out at the top of the file is the license text for an MD5
implementation in JavaScript. That&apos;s a pretty big hint we might see MD5 in use
somewhere.&lt;/p&gt;
&lt;p&gt;Looking for license-related strings in the code, the function that checks the
license comes up, included below. The comments are my own annotations as I was
making sense of the code.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;{
    key: &quot;isValidLicense&quot;,
    value: function () {
    var e = // email
        arguments.length &gt; 0 &amp;#x26;&amp;#x26; void 0 !== arguments[0]
            ? arguments[0]
            : &quot;&quot;,
        t = // token (the license key)
        arguments.length &gt; 1 &amp;#x26;&amp;#x26; void 0 !== arguments[1]
            ? arguments[1]
            : &quot;&quot;;
    if (!e || !t) return !1;
    // do something with UUID+email
    var o = s()(&quot;&quot;.concat(i.APP.UUID).concat(e)),
        // split into 5-char slices
        r = o.match(/.{1,5}/g),
        // join with dashes
        n = r.slice(0, 5).join(&quot;-&quot;);
    // check t(oken) matches n
    return t === n;
    },
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;i&lt;/code&gt; and &lt;code&gt;s()&lt;/code&gt; get defined elsewhere. Fortunately, &quot;Go to Definition&quot; worked for
&lt;code&gt;s()&lt;/code&gt; because there was exactly one declaration. Even without that, &lt;code&gt;UUID&lt;/code&gt; was
only present in one other location in the code, inside the &lt;code&gt;i&lt;/code&gt; object.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;// let&apos;s call this the `main` function given it declares APP etc.
function (e, t, o) {
&quot;use strict&quot;;
    var i = {
        APP: {
            NAME: &quot;[REDACTED]&quot;,
            UUID: &quot;[REDACTED]&quot;,
        },
    },
r = o(1),
s = o.n(r);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, it looked like I should figure out what gets passed as the &lt;code&gt;o&lt;/code&gt;
argument to this function, to then figure out what &lt;code&gt;s&lt;/code&gt; was. Since nothing had
names, I turned to &lt;a href=&quot;https://semgrep.dev&quot;&gt;semgrep&lt;/a&gt; to figure out where functions with 3 arguments get
called from. This would not return &lt;strong&gt;just&lt;/strong&gt; the call sites for the function I
care about, but it would cut the scope significantly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;❯ semgrep --lang=js -e &quot;\$FUNC(\$E, \$T, \$O)&quot; app.js
21:        o.o(e, t) || Object.defineProperty(e, t, { enumerable: !0, get: i });
--------------------------------------------------------------------------------
26:          Object.defineProperty(e, Symbol.toStringTag, { value: &quot;Module&quot; }),
--------------------------------------------------------------------------------
27:          Object.defineProperty(e, &quot;__esModule&quot;, { value: !0 });
--------------------------------------------------------------------------------
35:          Object.defineProperty(i, &quot;default&quot;, { enumerable: !0, value: e }),
--------------------------------------------------------------------------------
39:            o.d(
40:              i,
41:              r,
42:              function (t) {
43:                return e[t];
44:              }.bind(null, r)
45:            );
--------------------------------------------------------------------------------
57:        return o.d(t, &quot;a&quot;, t), t;
--------------------------------------------------------------------------------
1067:          const p = s.spawn(i, l, f);
--------------------------------------------------------------------------------
1145:            Object.defineProperty(e, i.key, i);
--------------------------------------------------------------------------------
1230:                e !== t.colorTheme &amp;#x26;&amp;#x26; i.update(&quot;colorTheme&quot;, e, !0),
--------------------------------------------------------------------------------
1233:                    i.update(&quot;iconTheme&quot;, o, !0),
--------------------------------------------------------------------------------
1326:            Object.defineProperty(e, i.key, i);
--------------------------------------------------------------------------------
1329:      o.d(t, &quot;default&quot;, function () {
1330:        return u;
1331:      });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(A couple more results elided for brevity and to avoid revealing which extension
this is.)&lt;/p&gt;
&lt;p&gt;Just as I was about to start trying to figure out which of these entries might
be the one calling my &quot;&lt;code&gt;main&lt;/code&gt;&quot; function, I thought on a whim to just assume
&lt;code&gt;s()&lt;/code&gt; is and MD5 implementation, and try it out. While I didn&apos;t think that would
work, well…&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;❯ echo -n &quot;&amp;#x3C;APP.UUID&gt;my-test-email@example.com&quot; | md5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Bingo! Well, this lacks the dashes at every 5 characters, but the rest matches
the license I received. At first I forgot to use &lt;code&gt;-n&lt;/code&gt;, but without it there&apos;s a
newline emitted that leads to an incorrect hash.&lt;/p&gt;
&lt;p&gt;I don&apos;t know if the UUID changes per installation/user or if it&apos;s global for the
extension, but at any rate, it should be possible to &quot;crack&quot; the extension once
installed locally, even without going through the entire flow described above.
Extracting the UUID from the un-minified file, using
&lt;a href=&quot;https://github.com/BurntSushi/ripgrep&quot;&gt;ripgrep&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;❯ rg -o &quot;UUID:\&quot;.*?\&quot;&quot; app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;A Better License Check?&lt;/h2&gt;
&lt;p&gt;It&apos;s necessary to consider both the threat model and the economics of
implementing a solution before deciding to iterate and improve the license
check. Investing resources into building up sufficient protections may not
always make rational sense.&lt;/p&gt;
&lt;p&gt;The threat model with licensing revolves primarily around piracy. As implemented
now, it is trivial to create a license key generator, as well as a &quot;nag
remover&quot;, patching out the validation code. Anther threat involves sharing of
license keys. In the threat model, the attacker is the end-user, which controls
the extension, the VS Code installation, and the operating system as a whole.&lt;/p&gt;
&lt;p&gt;The economics are less obvious to me since I don&apos;t have profit and revenue
numbers. There are certainly solutions available that might further limit the
likelihood of piracy, but at the risk of upsetting legitimate users (through
increasing complicated steps required to use the extension even if paid for) and
spending resources on a solution that may be disproportionate to the potential
additional revenue.&lt;/p&gt;
&lt;p&gt;While at first I was going to list a few ideas and their shortcomings, I think
there are lots of approaches with varying degrees of complexity to try, none
foolproof, all with trade-offs that I can&apos;t analyze with the partial information
I have.&lt;/p&gt;
&lt;p&gt;It&apos;s also true that folks that don&apos;t want to pay for this particular extension
have lots of alternatives. Some may value this particular extension and be fine
paying for it, and some may not. I do not think pirated copies equal lost sales,
but there is an emotional aspect to anti-piracy efforts, not just a
rational/economic one, and I can&apos;t speak to these developers&apos; state of mind on
that topic. Maybe they already considered everything I&apos;ve written about here,
and this is the result.&lt;/p&gt;
&lt;p&gt;It&apos;s rather common to assume, from the outside, that imperfect implementations
are the result of ignorance of security principles, when sometimes they are in
fact the result of a deliberate risk assessment, with its trade-offs.&lt;/p&gt;</content:encoded><author>roguesys</author></item></channel></rss>