Looking for a remote role · Jacksonville, FL

Walter Marshall

I build software end to end. The mobile app, the web app, and the backend behind both.

Right now that's Dealer Recon Systems, a dealer management platform for iOS, Android and web. I worked in dealerships for years before I wrote software for one, so it's built around how a shop really runs. Earlier on I did Android security work at Kik.

See DRS See the work Email me

Selected work

What I’ve built

Founder · Solo

Dealer Recon Systems

A dealer management system I built and run by myself.

Reconditioning, inventory, sales and CRM, service, parts and accounting, for car, RV, marine, motorcycle and body shop dealers. It's pre-launch, and the iOS and Android apps are in App Store and Google Play review.

  • Flutter
  • Dart
  • Firebase
  • Firestore
  • Cloud Functions
  • Node.js
  • Twilio

The problem

Before a used vehicle can be sold it has to be inspected, repaired, detailed, photographed and priced, and every day it sits waiting costs the dealership money. Most software for this is written by people who've never worked in a recon shop, and the screens don't follow the order the work happens in.

A pipeline with deadlines

Every vehicle moves through seven stages, and each stage has a target in days. Anything running late gets flagged on its own, so nobody has to walk the lot looking for it. A car also can't leave Photos until it has enough pictures for a listing.

  1. 01 Intake 1 day
  2. 02 Inspection 2 days
  3. 03 Mechanical 5 days
  4. 04 Detail 2 days
  5. 05 Photos 1 day Needs 5+ photos
  6. 06 Pricing 1 day
  7. 07 Lot ready For sale
Recon stages and the time target for each, as modeled in the app.

Multi-tenant by default

Every dealership's data is walled off by Firestore security rules that fail closed. If a dealership's account isn't active it can't read anything, and once a record is created its dealership can't be changed. On top of that there are about 40 permissions grouped into roles, with per-employee overrides, two-factor login and lockout after too many failed attempts.

Clients

  • iOS
  • Android
  • Web

One Flutter codebase, offline cache

Firebase

  • AuthMFA, lockout
  • Firestorerules scoped per dealership
  • Storagevehicle photos

Cloud Functions · 37

  • Scheduled backups
  • Twilio SMScampaigns, inbound
  • Export & CSV import
  • Audit log & restore
How DRS fits together.

Built for the lot

Techs are usually out on the lot where the signal is bad, so the app caches data locally and queues photo uploads until it's back online. VIN decoding, barcode and document scanning, and e-signatures are built in, and the audit log can roll any record back to an earlier version.

Everything else

There's no team. Besides the app, the backend and the website, I handle the infrastructure, networking and business setup myself. It hasn't launched yet, and the mobile apps are in App Store and Google Play review.

Software Engineer · Remote

Kik

Closing a fake-camera exploit without tipping off the people using it.

I worked on Kik's Android app, mostly on security, and designed the UI and UX for Kik X, a full redesign of the app meant to compete with Instagram and Snapchat.

  • Java
  • Smali
  • XMPP
  • XML
  • HTML
  • CSS

The exploit

Kik labeled photos taken with the in-app camera differently from ones picked from the gallery, which told the recipient a photo had just been taken. Modified builds of the app were sending gallery photos flagged as live camera shots, making it easy to catfish with someone else’s pictures.

Detecting a modified app

I wrote a function that generates a hash unique to each version of the app. The server stores the expected hash for every version and checks it at login, so a tampered build can't pass itself off as the real thing.

Failing quietly

If we'd just blocked modified apps, their authors would have known exactly what tripped. So they could still log in, but every photo they sent arrived as a gallery photo, even real camera shots, while their own app kept showing it as a camera photo. From their side the exploit still looked like it was working.

  1. 1

    Modified app → Kik server

    Logs in and sends its version hash

  2. 2

    Kik server

    Hash doesn't match this version. Login is allowed anyway. tampered

  3. 3

    Modified app → Kik server

    Sends a gallery photo, labeled as camera source: camera

  4. 4

    Kik server → Recipient

    Delivers it as a gallery photo source: gallery

  5. 5

    Modified app

    The sender's own chat still shows source: camera

What a tampered client saw versus what everyone else received.

Personal project · Solo

Mogadishu

Reverse engineering a 2003 game engine from its machine code, then testing what I found against the running game.

Delta Force: Black Hawk Down ships as compiled x86 with no source, no symbols and no documentation. I rebuilt a working model of what its engine does: movement, collision, blast damage, mission scripting. Then I wrote an overlay that draws all of it on screen while you play, so you can see the collision hulls, the invisible trigger volumes, the grenade arcs and the kill radii the game itself never shows you.

  • C
  • Win32
  • Direct3D 8
  • Python
  • Ghidra
  • x86 assembly

What the overlay draws

The game never tells you why you're stuck on nothing, where a mission is watching for you, or how far a grenade will reach. The overlay draws all of it while you play. The collision shot below is the clearest example: the artwork is an airplane, but what the engine thinks is there is a handful of rough convex shapes that barely resemble it.

A parked airplane in Black Hawk Down overlaid with brightly colored convex collision hulls that only loosely follow the shape of the aircraft
Collision geometry drawn over the artwork. Screenshots are of Delta Force: Black Hawk Down (NovaLogic, 2003), captured by the tool; the overlay and its drawings are mine.

The Steam build is encrypted

The Steam build is encrypted on disk. I measured it section by section: only 0.5% of the code section on disk matches what ends up in memory, and it's at maximum entropy. Ghidra can't open it at all. So I analyzed a different, unpacked build of the game, dumped the Steam version out of a running process, and rewrote its section headers so the dump could be read like a normal executable.

Carrying findings between builds

Addresses move between builds, and they don't all move by the same amount. I measured one function shifted by 0x960 and another a few hundred bytes later shifted by 0x9D0, so adding a fixed offset lands you in the wrong function. Instead I disassemble the bytes at a known address, wildcard only the fields that are allowed to change, and search the other build for that pattern. It either matches exactly once or fails loudly. Since the wildcarded slots get read back after a match, the function's global variables carry over with it.

Attaching without touching the game

The game imports d3d8.dll, and Windows looks in the application folder first, so a stand-in DLL sitting next to the executable gets loaded by the game itself. No injection, no debugger, no patched files. That's partly on principle and partly because I had to: the Steam wrapper checks the executable and refuses to start if a single byte is different.

Reloading code the game is holding open

A mission takes over thirty seconds to load, so restarting the game after every change wasn't an option. The proxy DLL holds only the permanent hooks and forwards through a pointer that's allowed to be null. The overlay itself is a second DLL, loaded from a shadow copy so a rebuild can overwrite the original while the game is running, and the new build is live in about a second. Keeping the hooks in the part that never unloads is what stops the swap from leaving the graphics driver pointing at freed memory.

Reading the game's own archives

Most of what the tool knows offline comes out of the game's packed archives, and none of the formats are documented. I decoded the archive container, the terrain heightmaps, the AI navigation meshes and the tactical map assets. That work also caught one of my own mistakes: I'd been sorting the navmeshes by file extension, and checking the actual file signature instead turned up dozens more saved under the wrong extension, including two named as images.

A grayscale terrain heightmap decoded from one of Black Hawk Down's archive files and rendered as an image
A mission heightmap, decoded straight out of the game archives.

Reading compiled mission scripts

Missions ship as compiled bytecode, and there are no opcode names anywhere in the game. It just switches on bare integers. The mission editor that shipped with the game has to turn those same numbers back into readable text for its own UI, so its jump tables work as a translation key, and scraping them recovered the vocabulary. The overlay now shows a mission's live logic in plain English: which zone is being watched, how big it is, how often the script checks it, and what happens when the rule fires.

An in-game panel titled Zone Explainer describing a mission trigger zone in plain English, listing each of the rule's conditions with a live yes or no and the consequences of it firing
Compiled 2003 mission bytecode, decompiled and rendered as English with live condition states.

Keeping track of what I know

Every claim in my analysis notes is marked one of three ways: read straight from the disassembly with an address, worked out with the math shown, or a guess. Right now that's 444 read, 150 worked out and 40 guesses. Thirty-one documents have corrections in them. A function two of them called the damage handler turned out to pick the death animation, and an early idea that low frame rates made the player faster got tested from 10 to 106 fps, turned out to be wrong, and was marked wrong instead of quietly deleted.

Checking it against the game

Everything I read out of the code gets checked against the running game. Walking speeds pulled from the animation data land within about 1% of the speeds I measured by driving the game with simulated keystrokes. A grenade at the player's feet was predicted to throw them at 7.69 m/s and measured at 7.7, with the damage exactly as calculated. If there's no number to find, nothing gets drawn. For example, the AI's line-of-sight check doesn't consider smoke at all, so the overlay doesn't draw a smoke radius.

A thrown grenade in mid-flight with its trajectory traced and two blast rings drawn around it, labeled with the blast radius and the damage it would do to the player at their current distance
A grenade mid-flight: traced arc, fuse counted in the engine’s own simulation ticks, and blast rings sized by the falloff formula read out of the machine code.

What it won't do

It's a single-player analysis tool and it's built to stay that way. It never touches networking, matchmaking or anti-cheat code, and every drawing is gated on a single-player check read from the game's own mission loader. If that flag can't be read, the overlay stays off. The source stays private because the game is DRM-protected, and the tool doesn't ship any game content.

The tool’s in-game menu, subtitled "single-player analysis", listing every active drawing with its color alongside one-click presets
The tool's in-game menu. It draws its own text and UI, and every overlay can be switched on and off.

Back lot

Open source

  • Hindsight reviewing Byrne vs. Fischer, 1956: 11.Bg5 marked as a blunder, the engine’s preferred Be2 drawn as an arrow, and the explanation beside the board

    Hindsight

    Free chess game review that runs on your own computer, the same kind of review Chess.com charges for. Load a PGN and Stockfish grades every move by how much it gave up, scores accuracy with the same curves Lichess uses, and explains each mistake by finding the actual pin, fork or hanging piece on the board. Version 0.1.0 is out with installers for Windows, macOS and Linux, and CI runs all 581 tests on all three. No account needed, and nothing leaves your computer.

    • TypeScript
    • Electron
    • Stockfish
  • The same PNG side by side: decoded by Pillow on the left, decoded by paeth on the right, pixel-identical

    paeth

    A PNG decoder in Rust, written from the spec: chunks, inflate, the five filter types and Adam7 interlacing. PngSuite is a set of 176 images made specifically to break decoders. paeth decodes all 162 valid ones pixel-for-pixel identical to an independent decoder, and rejects all 14 corrupt ones for the reason the suite gives. 153 tests, plus a fuzz pass that mutates headers to make sure nothing panics.

    • Rust
    • PNG
    • zlib
  • Loupe printing the headers and mitigations of dnsapi.dll in a terminal

    Loupe

    Shows what's inside an executable: headers, security mitigations, sections, imports and exports, for both PE and ELF files. It's read-only and never patches, writes or runs anything. Every offset in a binary can be controlled by an attacker, so every read goes through one bounds-checked function. It parses all 4,264 binaries in System32 under AddressSanitizer, UBSan and LeakSanitizer with no crashes, memory errors or leaks.

    • C
    • PE
    • ELF
  • unspool summarizing a capture: packet counts and a protocol breakdown as a bar chart in the terminal

    unspool

    Reads pcapng capture files and decodes what's in them: Ethernet, IP, TCP and UDP, DNS, TLS handshakes and HTTP. It only parses files, so there's no capture driver and no admin rights needed, and it installs anywhere. Checked frame by frame against tshark across all 562 public Wireshark sample captures, 1,290,282 packets. 172 tests.

    • Python
    • pcapng
    • DNS/TLS/HTTP
  • vinspect checking three VINs and then decoding one into its manufacturer, plant and model year

    vinspect

    Validates and decodes Vehicle Identification Numbers. The check digit is simple arithmetic, so it catches most typos before anything hits a database. The offline decoder gives you the manufacturer, model year and plant from a built-in snapshot of the NHTSA registry, and when the model year is ambiguous it returns both options instead of guessing. Tested against 6,516 real VINs from NHTSA crash-test records. 108 tests, one static binary, no network needed.

    • Go
    • ISO 3779
    • NHTSA vPIC

Mathematics

Some math I animated

A few proofs and results I like, animated. Each one runs on the actual math, so every number on screen is calculated live.

  • Group theory

    Everything is a 3-cycle

    A scramble is a permutation of the cube's 54 stickers, and solving it means building the reverse out of legal turns. Under the cube, each of its nine slices is drawn as a ring, three per axis. Every sticker sits in exactly two slices, so it lands where two rings cross, and the 54 stickers are the 54 crossings. A face turn slides the twelve stickers on its ring three spots along and rotates that face's grid around its center. The whole solve is one trick, repeated. R U R′ moves a handful of pieces, but only one of them is in the bottom layer. So do R U R′, turn D, undo R U R′, then undo the D turn. The two moves only overlap at that one spot, so the net result moves exactly three corners and leaves everything else alone. That's the commutator [R U R′, D]. R L′ F2 R′ L does the same thing for edges against U, and a setup turn moves any three pieces into those spots first and gets undone after. The arrows before each step show which stickers are about to move. Parity is the one thing a 3-cycle can't fix, but the cube handles it: a quarter turn is a 4-cycle of corners and a 4-cycle of edges, both odd, so the two parities always match and one quarter turn makes both even. Nothing in the solve is hard-coded. It's worked out when the site builds.

  • Geometry

    Shear, turn, shear

    Euclid's proof, acted out as three moves. Lay the hypotenuse flat with its square underneath. The altitude dropped from the right angle splits that square into two rectangles, one for each leg. Now take the square on one leg and shear it: slide its far edge along its own line, which keeps the base and the height the same, so the area stays the same. Then rotate it a quarter turn around the corner and shear it once more. It lands exactly on its rectangle, and none of those moves changed its area. The two areas in the readout are recalculated from the moving shapes every frame with the shoelace formula, so a bad move would show up as a number drifting.

  • Geometry

    Two thirds of a cylinder

    Archimedes asked for this on his tombstone. Slice a hemisphere and a cone at the same height and the cross-sections have areas π(r²−h²) and πh², which add up to πr², the cylinder's cross-section, at every height. Eudoxus had already proved the cone is a third of its cylinder, so the hemisphere has to be the other two thirds, which makes a sphere two thirds of the cylinder it fits in. The animation shows disks instead of widths on purpose, because it's the areas that add up, not the radii.

  • Geometry

    The volume of a donut

    Spin a shape around an axis it never crosses and the volume you get is the shape's area times the distance its centroid travels. Instead of just stating that, the animation cuts the donut open and straightens it out. The inside of the bend stretches exactly as much as the outside gets squeezed, so you end up with a plain cylinder of radius r and length 2πR, and everyone already knows how to get the volume of a cylinder.

  • Geometry

    Why π is there at all

    Roll a wheel through one full turn and it lays down a track exactly as long as its circumference. Measure that track in diameters and three fit, with a little left over. What the wheel can show you is that the leftover is the same for every wheel, whatever size you build it, since every circle is a scaled copy of every other. That's why the ratio gets its own name. What it can't show you is that no fraction ever hits it exactly. That was an open question until Lambert proved it in 1761. The marked point on the rim traces a cycloid along the way, and that curve has a couple of neat facts of its own: one arch is exactly 8r long and encloses exactly three times the area of the wheel that drew it.

  • Trigonometry

    Where the wave comes from

    Sine and cosine are really just coordinates. Move a point around a circle of radius 1: its height above the axis is the sine of the angle and its distance across is the cosine. Carry that height out to the right as the point moves and it draws the wave by itself. Watch the thick arc. Its length is the angle, which is all a radian is, and it's why the derivative of sine is cosine with nothing extra in front. Measure in degrees and you're stuck carrying a factor of π/180 around. The triangle inside the circle has legs cos and sin and a hypotenuse of 1, so sin² + cos² = 1 is just the Pythagorean theorem, and the readout holds it at 1.000000 while everything else moves.

  • Trigonometry

    Corners out of circles

    A square wave is just odd harmonics stacked up: (4/π)·Σ sin((2k−1)t)/(2k−1). Attach each term to the end of the last one as a spinning arm and the chain draws the wave on its own, flat tops and sharp edges out of nothing but circles. It never fully settles, though. The overshoot stays at about 9% of the jump no matter how many arms you add. The wave runs from −1 to 1, a jump of 2, so the partial sums keep peaking near 1.179 instead of 1. That's called the Gibbs phenomenon. It's part of the math, so the animation keeps it in.

  • Calculus

    The area and the slope

    The fundamental theorem of calculus in one sentence: the rate at which the area grows is the height of the curve. On top, the signed area under f fills in behind a sweep line, orange where the curve is above the axis and blue where it's below and the area is being given back. On the bottom, that running total is drawn as its own curve. It comes back to zero at 2π because the second hump is the first one flipped upside down and reversed, so they cancel exactly. The two numbers in the readout are worked out from opposite directions on purpose. The area is added up with the trapezoid rule from the top curve, and the slope is measured off the bottom curve by finite differences. Neither one just evaluates f and relabels it, since that would be assuming the thing being shown. Measured that way, the slope still matches the height of the curve to four decimal places. The small gap that's left is expected: a central difference of a trapezoid sum works out to (f₋₁ + 2f₀ + f₁)/4, which is a smoothing average and sits off from f by h²f″/4. At this step size that's about 1.4×10⁻⁵ at worst, which is what the readout shows.

  • Calculus

    Finite to fill, endless to paint

    Spin y = 1/x around the x-axis, starting at x = 1, and let it run forever. The volume adds up to exactly π. The surface area grows like 2π·ln x, which is slow but never stops, so this is a shape you could fill with a finite amount of paint but never finish painting. The paradox only works if you picture paint as a coat with a fixed thickness, and no coat that thick fits inside a tube that narrows to nothing. If you fill it, you've already coated it.

  • Number theory

    Cross out, never divide

    The sieve of Eratosthenes, still how you'd do it by hand. Write out the numbers, take the first one that isn't crossed off, cross out its multiples, and repeat. Whatever's left is prime, and you never had to divide anything. Two shortcuts make it fast, and both are safe. Crossing out starts at p², because every smaller multiple of p has a factor below p and already got crossed out. And you can stop as soon as p² passes the end of the list. The result is exact: 62 primes below 300. Next to it is N/ln N, the leading term of the prime number theorem, which guesses 52.6. That's 15% low, and since the error only shrinks like 1/ln N, it's still 7.8% low at a million.

  • Number theory

    It never closes

    z(θ) = e^iθ + e^iπθ. Two arms, the second attached to the tip of the first and spinning exactly π times as fast. The pen draws a rose whose petals all touch a circle of radius 2. For the curve to close, some whole number of laps N would have to leave the second arm on a whole number of turns too, which would make πN a whole number and π a fraction. It isn't, so the curve never closes. After exactly 7 laps the pen is 0.0556 from where it started (that's π ≈ 22/7), and after 113 laps it's 0.00019 away (π ≈ 355/113). The animation zooms in on the lap-7 near miss so you can watch the new line run right alongside the old one and miss it. Over two million laps, no lap ends closer than 2.4×10⁻⁷.

  • Number theory

    The angle no fraction gets near

    Same idea as It never closes, but with the golden ratio in place of π. Plant each seed a fixed angle around from the last one. Pick a simple fraction of a turn and every seed lands on one of a few spokes forever. Three eighths of a turn gives eight spokes, and no number of seeds will fill the gaps. The golden angle is harder to approximate with a fraction than any other angle, so its seeds never even come close to lining up into spokes, no matter how many you plant.

  • Probability

    π by throwing darts

    The quarter circle covers exactly π/4 of the square around it, so if you scatter points at random, the fraction that lands inside gets closer and closer to π/4. No formula and no calculus, just counting, and enough patience to count a lot. The random numbers come from a plain linear congruential generator. Pairs of its outputs fall on a set of parallel lines, which can cause problems with a bad multiplier, but with this one the lines are 1.4×10⁻⁵ apart, way finer than 2400 darts can pick up. Run across 600 different seeds, the estimate comes out unbiased.

  • Probability

    A bell curve, assembled by falling

    Twelve rows of pegs, a coin flip at every one, and the bin a ball lands in is just how many times it went right. So the bins fill in proportion to C(12,k)/4096, a binomial distribution built out of gravity and coin flips, with no statistics applied. The stepped outline is that exact binomial, scaled to however many balls have landed, not a curve fit to the bars afterward. The dashed line is the normal approximation, and the two are drawn separately on purpose: at the middle bin the binomial gives 0.225586 and the normal gives 0.230329, so the bell curve everyone uses is 2.1% high right where it looks the most convincing. More rows shrink that gap, but it never goes away completely.

  • Physics

    The quickest way down

    Three beads on three tracks, same start, same finish, released at the same time. The cycloid wins. That's the curve a rolling wheel's rim traces, the same one from the π card. It wins because it trades extra length for a steep drop right away and spends the rest of the trip moving fast. The straight ramp is the shortest path and takes 18.5% longer. A circular arc that starts by dropping straight down takes 2.5% longer. Both are timed against the cycloid. The weirder part: put beads at different heights on the same arch and they all reach the bottom at the same moment, in π√(a/g) no matter where they started, where a is the radius of the rolling wheel. Huygens built a clock around that.

  • Physics

    What a pulley actually buys you

    Count the rope segments holding up the moving block. That number is the whole trick. Four segments share a 600 N load, so you only pull with 150 N. But each of those four segments has to shorten by however far you want the load to rise, so you pull four meters of rope for one meter of lift. Force goes down by four, distance goes up by four, and the product is the same in every setup: 600 joules in, 600 joules out. An ideal pulley trades a smaller force for more rope, and the work stays exactly the same.

  • Physics

    Equal areas, equal times

    Kepler's second law. A planet on an ellipse sweeps out area at a constant rate, so it whips around the near side of its orbit and crawls along the far side. The eight wedges cover eight equal slices of time and look nothing alike, one short and wide, another long and thin. The readout measures each wedge's area off the drawing as it finishes and shows the spread across all eight, about 9×10⁻⁵. That small spread comes from the drawing: each wedge is built from 48 straight segments, and a straight segment cuts the corner off a curve. The law itself is exact. Underneath it is conservation of angular momentum: r²θ̇ stays constant, and the area swept per second is exactly half of it.

Chess hall

A game I won

Rapid game, playing Black. White opened quietly, then spent six moves walking the queen around the board. Once the position opened up I traded into an attack and finished it on the g-file. The board below replays the whole game. The moves are read from the PGN by a move generator I wrote, not a hardcoded list of squares.

Chess.com · rapid · 29 July 2026 · 0–1

White Guest5184350737 Black Guest6258940814 — me

  1. 1.e3e5
  2. 2.c3d5
  3. 3.Qa4+Bd7
  4. 4.Qb3Bc8
  5. 5.Nf3Nc6
  6. 6.Qa4e4
  7. 7.Nd4Bd7
  8. 8.Nxc6Bxc6
  9. 9.Bb5Ne7
  10. 10.Bxc6+Nxc6
  11. 11.b4a6
  12. 12.b5axb5
  13. 13.Qxb5Qc8
  14. 14.a4Ra5
  15. 15.Qb3Bd6
  16. 16.Ba3Bxa3
  17. 17.Rxa3O-O
  18. 18.O-ONe5
  19. 19.Qb4Nf3+
  20. 20.gxf3Ra6
  21. 21.fxe4Qg4+
  22. 22.Kh1Qf3+
  23. 23.Kg1Rg6#

The finish is worth a look. 19...Nf3+ is a knight sacrifice, and taking it with 20. gxf3 is close to forced. That pulls the pawn off g2 and opens the file the rook mates on. By 23...Rg6# the king has nowhere to go: f1, f2 and h2 are blocked by White's own pieces, and g2 and h1 are covered by the queen on f3.

Getting the game back

I played this on a guest account, and Chess.com only keeps game history for registered ones: a guest game is gone almost as soon as it ends. I closed the browser, couldn't stop thinking about the game, and decided to make a video walking through it. Five moves into setting it up on an analysis board, I realized I didn't remember what came next.

The game's address was still in my Firefox history, but opening it just started a new game, and the Wayback Machine had never captured it. The one copy left was the one my own browser had kept. While you play, Chess.com holds the game's moves in an encoded form, and that was still sitting in Firefox's cache. I searched the cache for the game ID, found the move data as raw hex, and decoded it back into notation. That's the PGN this board plays back. Then I made the video.

About

Background

I worked in dealerships for years before I ever wrote software for one. That's why DRS is laid out the way it is. It follows how a car, a tech and a manager actually move through a day, because I spent a long time doing that work myself.

Outside of work I like taking software apart to see how it was built: file formats, bytecode, network traffic, code loaded into programs that are already running. It's the same curiosity that got me into security work at Kik.

Away from the computer I play a lot of rapid chess and run on the beach.

Front desk

Hiring for a remote role?

I'm open to software engineering, technical support and QA roles, fully remote. Email's the fastest way to reach me, and I answer everything.