fig. 02
reverse engineeringmar 22, 20264 min read

i wanted beta features. i got a bot farm.

an LSPosed module, twenty-three feature flags, and a TCP server i should not have written.

instagram, of course, ships features behind feature flags. half of meta does. QuickSnap (their bereal clone), the Friend Map, Community Notes, Imagine Me — they all sit in the binary, gated behind a MobileConfigFactoryImpl that returns false until your account gets the flip.

you can't just ask. so i wrote igunlock.

hooking the right method

instagram on android is a 200mb apk full of obfuscated X.* classes. zero useful symbols, the kind of jar where every method is BD0(long) or A05(j$/u/q;). so the first half of the project is just finding what to hook.

MobileConfigFactoryImpl survives obfuscation as X.2jx for now. BD0(long) is the boolean lookup — pass it a 64-bit specifier, get back the gate.

XposedBridge.hookAllMethods(mcfClass, "BD0",
  new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) {
      long spec = (long) param.args[0];
      if (FORCE_TRUE_SPECIFIERS.contains(spec)) {
        param.setResult(true);
      }
    }
  });

FORCE_TRUE_SPECIFIERS is just an array of twenty-three longs i pulled out of the smali. each one encodes a feature gate: bits 48–55 are the param type (0x81 = session-scoped boolean), bits 32–47 the config index, bits 0–31 the parameter id. flip them all to true, restart instagram, suddenly Friend Map and Secret Reels exist on your phone.

the actual hard part wasn't the hooking. it was X.2jx becoming X.3hk two weeks later, when meta's r8 reshuffles the deck. so i wrote ClassFinder — 32kb of reflection plus a hand-rolled dex string-table parser that re-resolves the obfuscated names every cold start. it cross-validates between strategies, scores each candidate, and writes the result to igunlock_config.json keyed by the crc32 of classes.dex. version bumps invalidate the cache for free.

the part where it gets out of hand

once you can flip flags, you can also see what else the binary is doing. and once i was already in there, hooking certificate pinning seemed reasonable.

instagram has eight layers of pinning. meta's custom TrustManager. a separate sha-256 pin checker. okhttp's CertificatePinner. the tigon HUC pinner. hostname verifiers. the FOD sandbox trust manager. the native CertificateVerifier jni bridge. and SSLContext.init itself. all eight get a setResult(null) from inside the same module.

mitmproxy works again.

with cleartext traffic, i learned that every authenticated request gets built through a RequestBuilder factory bound to the user's session. so i hooked the factory, captured a reference, and held onto it.

a tcp server, in your phone

ServerSocket server = new ServerSocket(6700);

i should not have written that line.

CommandServer is a tcp server that lives inside the instagram process. adb forward tcp:16700 tcp:6700 and you can throw newline-delimited json at it from python:

api = InstagramAPI()
api.like("3567890123456789")
api.follow("zuck")
api.send_dm("zuck", "hey")

every request rides instagram's own pipeline: the captured RequestBuilder, the captured UserSession, the real signature, the real auth token. nothing on the wire distinguishes me from the app. the original point was to talk to graphql endpoints the public api doesn't expose. the dispatcher then grew. it's now seventy actions.

graphql was its own boss fight. you can't construct PandoGraphQLRequest from java — initHybridData produces corrupted jni HybridData and the process eats a SIGBUS mid-call. so instead i hook IgGraphQLQueryExecutor.A05, capture every live request keyed by query name into capturedGqlRequests, and replay them later through Au2 with modified params. you can only call queries the app itself has called. fine.

about the bot farm

i did not set out to build a bot farm. i set out to look at QuickSnap.

then i wrote discover_devices(), which scans for connected android phones over adb and sets up port forwarding to each one (incrementing from :16700). then i wrote ActionQueue, which executes batches with delay_range=(1.0, 3.0) to "look human." then i wrote PersistentInstagramAPI"faster for batch ops."

then i added a token-bucket rate limiter. thirty writes per hour, two hundred reads per hour, sixty searches per hour. "to stay under instagram's anti-spam thresholds." the _WRITE_ACTIONS frozenset has twenty-five entries.

i did not, technically, build a bot farm. i built the substrate of one and stopped. but the build directory has fifty-five versioned apks, so apparently i kept going for a while.

what i learned

  • param.setResult(null) will solve almost any problem in xposed land
  • meta's anti-tampering is layered, but it's all the same kind of layer — once you can hook one trust manager, you can hook eight
  • if your reverse-engineering tool ships with a token-bucket rate limiter, you have already lost the moral high ground
  • the source isn't public. write your own.