Introducing MobHunt: agentic mobile bug bounty hunting


tl;dr I built an agentic mobile security research system that runs the whole mobile bug bounty pipeline, from scoping a program to writing the report. I pointed it at a set of real programs a few months ago. It produced 9 reports. Two were accepted and paid. Seven were valid findings that got pushed back on, mostly on the grounds that I had “only performed static analysis”. I want to talk about the gap that points at. The whole thing is now open source: https://github.com/ivRodriguezCA/MobHunt.

Background

I have been doing mobile application security for over 12 years, and the two platforms have aged very differently.

On Android, the acquire step is essentially what it always was. Install the app on an emulator, ask the package manager where it put the APK, pull it over ADB, run apktool and jadx. You get real Java, the manifest, the resources. Obfuscation has got heavier, but the workflow is recognisably the one I used a decade ago.

On iOS the ground moved, and that is the part that shaped MobHunt more than anything else.

The iOS problem

App Store apps are FairPlay encrypted. For years the answer was a jailbroken device: run the app, dump the decrypted pages out of memory, load the binary in Hopper. That answer keeps getting worse. Jailbreaks arrive later, cover fewer devices and die faster, and at some point I was spending more time maintaining a research device than doing research on it.

So I changed what I was aiming at. The detail I think a lot of people gloss over is that FairPlay does not encrypt the app. It encrypts a range of bytes inside one binary. The Mach-O load command tells you exactly which range. Everything outside it is plaintext, and that is a lot: the Info.plist with its URL schemes, associated domains and ATS exceptions; the entitlements and provisioning profile; every bundled .plist and .json, which is still where credentials go to die; embedded frameworks, many of them not encrypted at all; and for React Native apps the entire JavaScript or Hermes bundle, which is to say the entire business logic. For those apps the encrypted part is mostly the runtime.

Once I started treating the encrypted binary as one input rather than the input, targets that used to be dead ends turned back into work I could do.

There is also a useful trick on Apple Silicon. Because these Macs run iOS apps natively, you can pull the app down on the Mac and decrypt it there with no jailbroken device involved. The catch is that it only works if the app can be installed on a Mac at all, and that is the developer’s choice: in App Store Connect, under Pricing and Availability, there is an “iPhone and iPad Apps on Apple Silicon Macs” setting. Compatible apps ship to the Mac App Store by default, but developers can opt out, and plenty of the interesting ones do. A good first attempt, not a strategy.

MobHunt encodes this directly. Acquisition runs the full decryption chain, tries the Mac path first, and records an analysis_mode: full if it got a real decrypted binary, partial if it did not. Every later stage branches on that flag. In partial mode the orchestrator skips what produces garbage on an encrypted binary, which is string extraction over an encrypted __TEXT, Swift metadata parsing and disassembly of any kind, and runs the resource and configuration analysers instead.

Partial mode is not a failure state. It is the normal state for a lot of App Store targets, and it still finds things. The pipeline was built around not having the binary, because most of the time you do not.

What MobHunt is

It is a Claude Code project with almost no orchestration code in it. The intelligence lives in markdown files loaded as prompts, and the Python is boring glue that runs jadx, parses a manifest or talks to Ghidra.

/hunt <program>  ->  scope -> acquire -> recon -> hunt -> chain -> validate -> report
                                             ^                        |
                                             +---- agentic loop ------+

An orchestrator coordinates eleven agents. The decision everything else follows from is that each hunter gets exactly one vulnerability class. Five iOS hunters, five Android hunters, and three more that sweep the whole app with Mariana Trench and Semgrep. At peak that is thirteen agents reading the same app at once, each blind to the others.

Broad objectives produce noise. Ask one agent to find everything and you get twenty paragraphs about how the app uses AES. Ask it whether the myapp-auth:// scheme carries an OAuth callback that can be hijacked and you get an answer.

The part I actually care about

The hunting is not the hard problem. Automated mobile scanners have existed for years and are mostly useless, not because they cannot find things but because they cannot tell a configuration observation from a vulnerability. “ATS is disabled” is not a finding. “This app disables ATS and then sends the session token over HTTP” is. That gap is the entire job.

So MobHunt spends most of its design budget killing its own findings: a quick triage, a Mobile 7-Question Gate, four post-gates, and a never-submit list encoded as data. The instruction to the validator is blunt. Be strict, and when in doubt kill it.

In practice, on real runs: 34 raw findings became 2 reports on one target. On another, 21 of 29 were killed outright. And the purely mechanical gate is close to useless on its own, which is worth admitting rather than dressing up. On that 34-finding target it killed zero and punted 32 to human review. The reasoning pass did all the actual killing.

My favourite result is the one where the pipeline destroyed its own best looking finding. A chain claimed attacker-controlled state survived across the login boundary through a process-wide singleton. It looked great. Dynamic verification re-decompiled the app and found the singleton is write-only dead code: three call sites null it before anything reads it, and the field the chain assumed was the consumer is never assigned and never read anywhere. The verdict, written down verbatim, was that the claim “was inferred from the field declaration’s existence without tracing the complementary half”. Which is a polite way of saying it was wishful thinking.

Every Firebase and cloud storage configuration it checked came back clean, incidentally. The tool cleared as much as it flagged, and that is a feature.

What happened when I pointed it at real programs

They are private programs, so I cannot name them, describe them or post the findings. That has been true since the first time I wrote about bug bounty hunting and it is still true.

A full run on one app took between four and ten hours and was almost entirely autonomous. I would start /hunt, go do something else, and come back to validated findings and drafted reports.

The aggregate: nine reports filed, two accepted and paid, seven valid findings pushed back on for only doing static analysis.

That nine is not what the tool found. It is how many reports I had time to write. The pipeline produced far more, and there is still a queue of candidates that cleared the gates and are waiting on the last mile: confirmation, a clean reproduction, the writeup. That part the tool cannot do for me. Then I pivoted to other work and the queue stopped moving. Finished reports never got sent, rebuttals I had written never got posted, and four months later the backlog is stale enough that I would re-verify all of it before touching any of it.

The bottleneck was never finding candidates. Thirteen agents generate more plausible leads overnight than I can responsibly chase in a month. The bottleneck was me, and it stayed me.

The mobile knowledge gap

To be careful here: this is not a complaint about any platform or any person. Triagers handle a firehose, most are good at it, and the two reports that got paid were handled well. The gap I want to point at is an industry one. There is far more demand for mobile security review than there are people who have spent years doing it.

Mobile is not web. On a web target you can poke the endpoint and hand someone a curl command. On mobile you often cannot, and that is not laziness. The logic is compiled into a binary sitting on a hundred million phones, and probing a production backend to prove a client-side bug is usually out of scope, rightly. Static analysis is not the lazy path in mobile work, it is the primary tool. That is obvious to everyone who does this and not obvious to a reviewer whose background is web.

So when a report says a shipped release build has a WebView whose onReceivedSslError reads a useUntrustedCertificate flag, throws the result away, and then evaluates if (Boolean.TRUE.booleanValue()) { handler.proceed(); }, that is not theoretical. A debug escape hatch got constant-folded into an unconditional accept-any-certificate in a build that is on real devices right now, and the decompiler even preserves the fossil of the check that no longer does anything. If the answer to that is “come back when you have run it dynamically”, something went wrong before the conversation started, because reading the shipped code is the verification.

I will be fair about the other half, though. A closed report is an opening argument, not a verdict, and the right move is to reply and give the reviewer what they are actually asking for. I never sent my rebuttals. That one is on me.

And there is a version of this worth internalizing rather than arguing with. The two that got paid had something the other seven did not: a way for someone with no access to my machine to see the problem themselves. One needed a single adb logcat line on an unmodified production device, because the app announces its own broken state at startup. That is the real difference, and it is not static versus dynamic. It is whether the person reading your report has to take your word for anything.

Eight years ago my first bug bounty report was closed because I could not produce a proof of concept, and I wrote at the time that the lesson was PoC || GTFO. Apparently I needed to learn it twice.

Open sourcing it

It is up:

https://github.com/ivRodriguezCA/MobHunt

The eleven agents, the nine skills, the ten slash commands, the always-on rules, the validation gates, the recon and analysis tooling, the report templates and the Semgrep and Mariana Trench rule packs. No findings, no reports, no targets, and no proof of concept code written against a real app. The repo has a rule file saying so, because writing the rule down is part of the point.

Getting it ready took longer than writing it, which is worth mentioning because anyone doing this will hit the same thing. A repo that has been pointed at real targets accumulates material that must never be published: proof of concept code written against named apps, obfuscated class names that fingerprint one specific build, program-specific requirements only invited researchers ever see, worked examples in code comments that quietly identify a target. Some of it lives in git history rather than the working tree, so deleting the files is not enough. It ships from a fresh history for exactly that reason.

Two things to know if you run it. It only pulls app binaries from official stores, never from APKPure or APKMirror or any other mirror, because a repackaged binary makes your whole analysis worthless and you will not know. And it will not hand your credentials to third party CLI tools. Both are hard rules in the project, not suggestions.

One thing is missing on purpose. The decryption subsystem ships almost intact, including the Apple Silicon path above, but I cut a later fallback that spawned the target under a debugger to get around CS_RESTRICT. Not because the technique is dangerous or clever, it is neither, and it is publicly documented in jhftss’s “Debug any iOS Apps on M1 Mac”. The reason is narrower. DMCA 1201’s good-faith research exemption covers the act of circumvention, while distributing circumvention tools is restricted separately with no equivalent carve-out. Running something yourself and publishing it for others are different positions.

The distinction is real, and it is the more interesting half. mremap_encrypted does not defeat anything: it is Apple’s own syscall, called against an app whose licence I hold, on hardware built to run iOS apps, with SIP on. The debugger path exists specifically to get around a control put there to stop it. Same result, opposite relationship to the platform’s security model.

The architecture is adapted from claude-bug-bounty by shuvonsec, which does this for web targets. The project structure, agent model and gate pattern come from there. It is MIT licensed, same as the upstream.

Conclusions

  • Android acquisition has barely changed in a decade. iOS changed completely, and the winning move was to stop treating a decrypted binary as a prerequisite. Build the pipeline around the degraded case, because it is the common one.
  • The hunting was never the hard part. Deciding what to throw away was, and that is where the design effort went. A pipeline that can kill its own best looking finding is working correctly.
  • Nine reports is not what the tool found, it is what I had time to write. Automating the hunt just moves the bottleneck to confirmation and writeups, and that last mile is still entirely human.
  • There is a real mobile knowledge gap in bug bounty triage, and it is a capacity problem rather than a people problem. On mobile the shipped binary is the ground truth.
  • The reports that got paid were the ones someone else could verify without trusting me. That is a fair bar, and a better thing to optimize for than winning the argument.

That’s it. If you run it, break it, or disagree with any of the above, let me know on twitter @ivMobAppSec.