Chapter 09·What fell out of the APK

What fell out of the APK

We stripped the handheld APK, converted its bytecode, and read what the obfuscator forgot to hide — delimiter bytes, a TCP target, and a message builder.

What fell out of the APK

The tools

The handheld APK was not hardened. It was a normal production Android build, signed by the vendor, shipped to devices we already had in our hands.

We pulled the APK off the device, unpacked its resources with apktool, converted the Dalvik bytecode to JVM bytecode with dex2jar, and opened the resulting JAR in . Three tools, each one of them boring, each one of them available as an apt or brew install.

None of this is novel. The novelty is what the vendor left readable on the other end.

What was obfuscated

The codebase had been through a ProGuard-style pass before it got packaged. Class names were collapsed to single letters — a, b, c. Method names the same. A call that used to read AuthManager.buildLoginRequest(user, device) now read a.b(c, d). Calls chained through e.f().g() until it was easier to follow the decompile by stack trace than by name.

String constants that mattered had been split into byte arrays assembled at runtime — {0x4F, 0x50, 0x3D} instead of the string "OP=". Whoever configured the obfuscator knew what they were doing.

What was not obfuscated

The three things we needed were the three things the obfuscator could not hide.

The first was the delimiter bytes. A byte array has to evaluate to the same value at runtime that the PDV expects on the wire, and the PDV was not going to be reconfigured. 0x1D (group separator — our [NP]) and 0x04 (end of transmission — our [EOM]) appeared as raw integer literals at every call site that built a message. You cannot obfuscate a hex constant into something else.

The second was the TCP target. The app opened a java.net.Socket against a LAN IP and a port number. The port number, whatever it was, had to be an integer literal in the compiled code. We found it on the first grep.

The third was the message-building method. ProGuard renamed it, but it was the only method in the codebase that took a Map<String,String> of key-value pairs and returned a byte[] that started with 0x1D and ended with 0x04. One method, one shape, one purpose.

The synthetic view

We are not publishing the real decompiled source. What follows is a synthetic snippet in the same shape — it highlights the same structure we actually found, with the vendor-specific symbols replaced.

Synthetic snippet from the handheld APK

Hover an annotation to highlight the matching lines on the left.

01public final class a {02  private static final byte[] NP = { 0x1D };03  private static final byte[] EQ = { 0x3D };04  private static final byte[] EOM = { 0x04 };05  private static final int PORT = 10401;06  private static final String HOST = "192.168.0.23";07 08  public static byte[] b(String op, Map<String,String> kv) {09    ByteArrayOutputStream os = new ByteArrayOutputStream();10    os.write(NP);11    os.write(("OP").getBytes());12    os.write(EQ);13    os.write(op.getBytes());14    for (Map.Entry<String,String> e : kv.entrySet()) {15      os.write(NP);16      os.write(e.getKey().getBytes());17      os.write(EQ);18      os.write(e.getValue().getBytes());19    }20    os.write(EOM);21    return os.toByteArray();22  }23 24  public static Socket c() throws IOException {25    return new Socket(HOST, PORT);26  }27}
  1. Delimiter constants

    0x1D (group separator, which we write as [NP]), 0x3D (the '=' character, which we write as [EQ]), and 0x04 (end of transmission, which we write as [EOM]). The obfuscator renamed the symbols, but the byte values had to survive to runtime or the PDV would not parse the message. That is why we could find them.

    Lines 2, 3, 4
  2. TCP target

    A hardcoded LAN host and port. No DNS lookup, no service discovery, no TLS. The app expects the PDV to be sitting at this address on the restaurant network. Once we had the port number, we had the second half of the integration.

    Lines 5, 6, 26
  3. The message builder

    The method we needed. It takes an opcode plus a map of KEY-value pairs, then writes them out with [NP] between fields, [EQ] between key and value, and [EOM] at the end. Every command the handheld sent went through something that had this shape.

    Lines 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22

What it meant

Once the message builder was legible, the protocol was implicit. We knew the wire format because we had been staring at it in captured packets for weeks. The APK confirmed that the packets we had seen were not encrypted, not compressed, not tunneled through anything exotic — they were exactly what the handheld wrote to a java.net.Socket after running through a single builder.

Now we knew the language. Chapter 10 is what we built with it.