You're inspecting network traffic from a mobile app. The response body isn't JSON - it's a blob of binary data. No documentation, no .proto files, no schema. Welcome to Protobuf in the wild.
Protocol Buffers (Protobuf) is Google's binary serialization format, and it's everywhere - gRPC services, mobile apps, internal APIs. When you're reverse engineering an app that uses it, you need to decode it without the schema. Here's how.
Recognizing Protobuf
Before you can decode it, you need to identify it. Protobuf responses have a few tells:
- Content-Type header is often
application/x-protobuf,application/grpc, orapplication/octet-stream - The response body is binary (not human-readable)
- The first few bytes follow the wire format pattern - field numbers and wire types packed into varints
If you see a binary response from a Google, Facebook, or any modern mobile app backend, there's a good chance it's Protobuf.
The Wire Format
Protobuf encodes data as a series of key-value pairs. Each key is a varint that encodes both the field number and the wire type:
key = (field_number << 3) | wire_typeWire types tell you how to read the value:
| Wire Type | Meaning | Used For |
|---|---|---|
| 0 | Varint | int32, int64, uint32, bool, enum |
| 1 | 64-bit | fixed64, sfixed64, double |
| 2 | Length-delimited | string, bytes, embedded messages |
| 5 | 32-bit | fixed32, sfixed32, float |
This is the foundation. Without a .proto schema, you won't know field names - but you can decode the structure.
Decoding Without a Schema
The protoc tool can decode raw Protobuf using --decode_raw:
cat response.bin | protoc --decode_rawThis gives you something like:
1: 12345
2: "John Doe"
3 {
1: "[email protected]"
2: 1
}
4: 1708300800You can see the structure: field 1 is an integer (probably an ID), field 2 is a string (name), field 3 is a nested message (contact info), and field 4 looks like a Unix timestamp.
Python Tooling
For programmatic decoding, I use the blackboxprotobuf library:
import blackboxprotobuf
import requests
response = requests.get("https://api.example.com/data",
headers={"Accept": "application/x-protobuf"})
# Decode without a schema
message, typedef = blackboxprotobuf.decode_message(response.content)
print(message)
# {1: 12345, 2: b'John Doe', 3: {1: b'[email protected]', 2: 1}}
# The typedef tells you the inferred types
print(typedef)
# {'1': {'type': 'int'}, '2': {'type': 'bytes'}, ...}The library infers types from the wire format. It can't tell you that field 2 is a "username" - but it can tell you it's a string, and from context, you can figure the rest out.
Reconstructing the Schema
Once you've decoded enough responses, you can reconstruct a working .proto file:
syntax = "proto3";
message UserResponse {
int64 user_id = 1;
string name = 2;
ContactInfo contact = 3;
int64 created_at = 4;
}
message ContactInfo {
string email = 1;
bool verified = 2;
}With this schema, you can use protoc to generate Python classes and decode/encode messages properly:
from generated_pb2 import UserResponse
user = UserResponse()
user.ParseFromString(response.content)
print(user.name) # "John Doe"
print(user.contact.email) # "[email protected]"gRPC: Protobuf Over HTTP/2
Many modern APIs use gRPC, which is Protobuf over HTTP/2 with a specific framing format. gRPC responses have a 5-byte header before the Protobuf payload:
def decode_grpc_response(data):
# Byte 0: compression flag (0 = none)
compressed = data[0]
# Bytes 1-4: message length (big-endian)
length = int.from_bytes(data[1:5], 'big')
# Bytes 5+: the actual Protobuf message
protobuf_data = data[5:5 + length]
return protobuf_dataStrip those 5 bytes and you're back to standard Protobuf decoding.
The Workflow
My process for any Protobuf-based target:
- Capture traffic - Use mitmproxy with a mobile device or emulator to intercept requests
- Identify Protobuf endpoints - Look for binary responses and
x-protobufcontent types - Raw decode - Use
protoc --decode_raworblackboxprotobufto see the structure - Map fields to meaning - Cross-reference decoded values with what you see in the UI
- Reconstruct the schema - Build a
.protofile that matches the wire format - Generate code - Use
protocto generate Python classes for clean encode/decode
Common Pitfalls
- Nested messages vs. bytes - Wire type 2 is used for both strings and embedded messages.
blackboxprotobufsometimes guesses wrong - if a "bytes" field looks structured, try decoding it as a message - Packed repeated fields - Arrays of numbers can be packed into a single length-delimited field. If you see an unexpectedly long bytes field, try decoding it as packed varints
- Signed integers - Protobuf uses ZigZag encoding for signed integers (
sint32/sint64). If decoded values look wrong (huge numbers for what should be small negatives), you might be reading asintas auint - Enums look like ints - You'll just see
0,1,2- you need the app context to know what they mean
Key Takeaways
- Protobuf is everywhere in modern APIs, especially mobile apps and gRPC services
- You can always decode the structure without a schema - you just won't have field names
protoc --decode_rawandblackboxprotobufare your primary tools- Reconstructing a
.protofile from decoded responses is tedious but straightforward - gRPC adds a 5-byte framing header - strip it before decoding
The binary format is designed for machines, not humans. But with the right tools and a systematic approach, you can read it just fine.
This is an educational overview for security research and authorized testing. Always ensure you have permission before testing any system.