Skip to content

Commit aec0072

Browse files
xaionaro@dx.centerxaionaro@dx.center
authored andcommitted
Extract transaction codes from device DEX bytecode (primary method)
New package binder/versionaware/dex/ parses Android framework JARs to extract TRANSACTION_* constants from AIDL $Stub classes in DEX bytecode. This provides definitive transaction codes for the exact device firmware — no version guessing or probing needed. Detection priority: 1. Parse all JARs in /system/framework/ for DEX TRANSACTION_* fields 2. Cache result to /data/local/tmp/.aidl_cache/codes.json (keyed by JAR fingerprint; invalidated on OS update) 3. Fallback: compiled version tables + .so method set + binder probing Performance: first run ~1.5s (scans 38 JARs), cached runs ~0.8s. Cache is 385KB JSON file. Verified on Pixel 8a (API 36) and emulator (API 35) — all README example commands pass.
1 parent 3fdb9d0 commit aec0072

File tree

9 files changed

+1055
-2
lines changed

9 files changed

+1055
-2
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package dex
2+
3+
import (
4+
"encoding/binary"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
// DEX header field offsets.
10+
// Per https://source.android.com/docs/core/runtime/dex-format#header-item
11+
const (
12+
headerSize = 0x70
13+
14+
offStringIDsSize = 0x38
15+
offStringIDsOff = 0x3C
16+
offTypeIDsSize = 0x40
17+
offTypeIDsOff = 0x44
18+
offFieldIDsSize = 0x50
19+
offFieldIDsOff = 0x54
20+
offClassDefsSize = 0x60
21+
offClassDefsOff = 0x64
22+
)
23+
24+
// Size of fixed-length structures in the DEX file.
25+
const (
26+
stringIDItemSize = 4
27+
typeIDItemSize = 4
28+
fieldIDItemSize = 8
29+
classDefItemSize = 32
30+
)
31+
32+
// dexFile provides indexed access to a parsed DEX file's sections.
33+
// Strings are read on demand to avoid loading the entire string table.
34+
type dexFile struct {
35+
data []byte
36+
37+
stringIDsSize uint32
38+
stringIDsOff uint32
39+
typeIDsSize uint32
40+
typeIDsOff uint32
41+
fieldIDsSize uint32
42+
fieldIDsOff uint32
43+
classDefsSize uint32
44+
classDefsOff uint32
45+
}
46+
47+
// parseDEXFile validates the header and extracts section offsets.
48+
func parseDEXFile(data []byte) (*dexFile, error) {
49+
if len(data) < headerSize {
50+
return nil, fmt.Errorf("DEX data too short: %d bytes (need at least %d)", len(data), headerSize)
51+
}
52+
53+
magic := string(data[0:4])
54+
if magic != "dex\n" {
55+
return nil, fmt.Errorf("bad DEX magic: %q", data[0:8])
56+
}
57+
58+
f := &dexFile{
59+
data: data,
60+
stringIDsSize: binary.LittleEndian.Uint32(data[offStringIDsSize:]),
61+
stringIDsOff: binary.LittleEndian.Uint32(data[offStringIDsOff:]),
62+
typeIDsSize: binary.LittleEndian.Uint32(data[offTypeIDsSize:]),
63+
typeIDsOff: binary.LittleEndian.Uint32(data[offTypeIDsOff:]),
64+
fieldIDsSize: binary.LittleEndian.Uint32(data[offFieldIDsSize:]),
65+
fieldIDsOff: binary.LittleEndian.Uint32(data[offFieldIDsOff:]),
66+
classDefsSize: binary.LittleEndian.Uint32(data[offClassDefsSize:]),
67+
classDefsOff: binary.LittleEndian.Uint32(data[offClassDefsOff:]),
68+
}
69+
70+
return f, nil
71+
}
72+
73+
// readString reads the MUTF-8 string at the given string_ids index.
74+
// For TRANSACTION_* field names (which are ASCII), reading until the
75+
// null terminator is sufficient.
76+
func (f *dexFile) readString(idx uint32) (string, error) {
77+
if idx >= f.stringIDsSize {
78+
return "", fmt.Errorf("string index %d out of range (size=%d)", idx, f.stringIDsSize)
79+
}
80+
81+
off := f.stringIDsOff + idx*stringIDItemSize
82+
if off+4 > uint32(len(f.data)) {
83+
return "", fmt.Errorf("string_id_item at offset 0x%x out of bounds", off)
84+
}
85+
86+
dataOff := binary.LittleEndian.Uint32(f.data[off:])
87+
if dataOff >= uint32(len(f.data)) {
88+
return "", fmt.Errorf("string_data_off 0x%x out of bounds", dataOff)
89+
}
90+
91+
// Skip the ULEB128-encoded utf16_size.
92+
pos := dataOff
93+
_, pos, err := readULEB128(f.data, pos)
94+
if err != nil {
95+
return "", fmt.Errorf("reading string utf16_size at 0x%x: %w", dataOff, err)
96+
}
97+
98+
// Read until null terminator.
99+
end := pos
100+
for end < uint32(len(f.data)) && f.data[end] != 0 {
101+
end++
102+
}
103+
if end >= uint32(len(f.data)) {
104+
return "", fmt.Errorf("string at offset 0x%x not null-terminated", pos)
105+
}
106+
107+
return string(f.data[pos:end]), nil
108+
}
109+
110+
// readTypeDescriptor returns the type descriptor string for the given type_ids index.
111+
func (f *dexFile) readTypeDescriptor(idx uint32) (string, error) {
112+
if idx >= f.typeIDsSize {
113+
return "", fmt.Errorf("type index %d out of range (size=%d)", idx, f.typeIDsSize)
114+
}
115+
116+
off := f.typeIDsOff + idx*typeIDItemSize
117+
if off+4 > uint32(len(f.data)) {
118+
return "", fmt.Errorf("type_id_item at offset 0x%x out of bounds", off)
119+
}
120+
121+
descriptorIdx := binary.LittleEndian.Uint32(f.data[off:])
122+
return f.readString(descriptorIdx)
123+
}
124+
125+
// fieldID represents a parsed field_id_item.
126+
type fieldID struct {
127+
classIdx uint16
128+
typeIdx uint16
129+
nameIdx uint32
130+
}
131+
132+
// readFieldID parses the field_id_item at the given field_ids index.
133+
func (f *dexFile) readFieldID(idx uint32) (fieldID, error) {
134+
if idx >= f.fieldIDsSize {
135+
return fieldID{}, fmt.Errorf("field index %d out of range (size=%d)", idx, f.fieldIDsSize)
136+
}
137+
138+
off := f.fieldIDsOff + idx*fieldIDItemSize
139+
if off+fieldIDItemSize > uint32(len(f.data)) {
140+
return fieldID{}, fmt.Errorf("field_id_item at offset 0x%x out of bounds", off)
141+
}
142+
143+
return fieldID{
144+
classIdx: binary.LittleEndian.Uint16(f.data[off:]),
145+
typeIdx: binary.LittleEndian.Uint16(f.data[off+2:]),
146+
nameIdx: binary.LittleEndian.Uint32(f.data[off+4:]),
147+
}, nil
148+
}
149+
150+
// stubDescriptorToInterface converts a $Stub class descriptor to the
151+
// AIDL interface's dot-separated name.
152+
//
153+
// "Landroid/app/IActivityManager$Stub;" -> "android.app.IActivityManager"
154+
func stubDescriptorToInterface(desc string) string {
155+
// Strip leading 'L' and trailing '$Stub;'.
156+
s := strings.TrimPrefix(desc, "L")
157+
s = strings.TrimSuffix(s, "$Stub;")
158+
return strings.ReplaceAll(s, "/", ".")
159+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package dex
2+
3+
import "fmt"
4+
5+
// DEX encoded_value type constants.
6+
// Per https://source.android.com/docs/core/runtime/dex-format#value-formats
7+
const (
8+
valueTypeByte = 0x00
9+
valueTypeShort = 0x02
10+
valueTypeChar = 0x03
11+
valueTypeInt = 0x04
12+
valueTypeLong = 0x06
13+
valueTypeFloat = 0x10
14+
valueTypeDouble = 0x11
15+
valueTypeMethodType = 0x15
16+
valueTypeMethodHandle = 0x16
17+
valueTypeString = 0x17
18+
valueTypeType = 0x18
19+
valueTypeField = 0x19
20+
valueTypeMethod = 0x1a
21+
valueTypeEnum = 0x1b
22+
valueTypeArray = 0x1c
23+
valueTypeAnnotation = 0x1d
24+
valueTypeNull = 0x1e
25+
valueTypeBoolean = 0x1f
26+
)
27+
28+
// encodedValue holds a decoded DEX encoded_value.
29+
// Only the integer representation is stored, which suffices for
30+
// extracting TRANSACTION_* constants.
31+
type encodedValue struct {
32+
intVal int64
33+
}
34+
35+
// readEncodedValue decodes a single encoded_value at the given position.
36+
// It returns the decoded value and the new position past the consumed bytes.
37+
func readEncodedValue(
38+
data []byte,
39+
pos uint32,
40+
) (encodedValue, uint32, error) {
41+
if pos >= uint32(len(data)) {
42+
return encodedValue{}, pos, fmt.Errorf("encoded_value truncated at offset 0x%x", pos)
43+
}
44+
45+
byte0 := data[pos]
46+
pos++
47+
valueType := byte0 & 0x1F
48+
valueArg := byte0 >> 5
49+
50+
switch valueType {
51+
case valueTypeNull:
52+
return encodedValue{}, pos, nil
53+
54+
case valueTypeBoolean:
55+
var v int64
56+
if valueArg != 0 {
57+
v = 1
58+
}
59+
return encodedValue{intVal: v}, pos, nil
60+
61+
case valueTypeByte:
62+
return readSignedEncodedInt(data, pos, 1)
63+
64+
case valueTypeShort:
65+
return readSignedEncodedInt(data, pos, uint32(valueArg)+1)
66+
67+
case valueTypeChar:
68+
return readUnsignedEncodedInt(data, pos, uint32(valueArg)+1)
69+
70+
case valueTypeInt:
71+
return readSignedEncodedInt(data, pos, uint32(valueArg)+1)
72+
73+
case valueTypeLong:
74+
return readSignedEncodedInt(data, pos, uint32(valueArg)+1)
75+
76+
case valueTypeFloat:
77+
// Right-zero-extended: read value_arg+1 bytes into the high bytes of a 4-byte value.
78+
// We don't need float precision for transaction codes; store raw bits.
79+
return readUnsignedEncodedInt(data, pos, uint32(valueArg)+1)
80+
81+
case valueTypeDouble:
82+
return readUnsignedEncodedInt(data, pos, uint32(valueArg)+1)
83+
84+
case valueTypeMethodType, valueTypeMethodHandle,
85+
valueTypeString, valueTypeType,
86+
valueTypeField, valueTypeMethod, valueTypeEnum:
87+
// Index types: unsigned, value_arg+1 bytes.
88+
return readUnsignedEncodedInt(data, pos, uint32(valueArg)+1)
89+
90+
case valueTypeArray:
91+
size, newPos, err := readULEB128(data, pos)
92+
if err != nil {
93+
return encodedValue{}, newPos, fmt.Errorf("reading array size: %w", err)
94+
}
95+
pos = newPos
96+
for i := uint32(0); i < size; i++ {
97+
_, pos, err = readEncodedValue(data, pos)
98+
if err != nil {
99+
return encodedValue{}, pos, fmt.Errorf("reading array element %d: %w", i, err)
100+
}
101+
}
102+
return encodedValue{}, pos, nil
103+
104+
case valueTypeAnnotation:
105+
// type_idx (uleb128) + size (uleb128) + name/value pairs.
106+
_, newPos, err := readULEB128(data, pos)
107+
if err != nil {
108+
return encodedValue{}, newPos, fmt.Errorf("reading annotation type_idx: %w", err)
109+
}
110+
pos = newPos
111+
annSize, newPos, err := readULEB128(data, pos)
112+
if err != nil {
113+
return encodedValue{}, newPos, fmt.Errorf("reading annotation size: %w", err)
114+
}
115+
pos = newPos
116+
for i := uint32(0); i < annSize; i++ {
117+
_, pos, err = readULEB128(data, pos) // name_idx
118+
if err != nil {
119+
return encodedValue{}, pos, fmt.Errorf("reading annotation name %d: %w", i, err)
120+
}
121+
_, pos, err = readEncodedValue(data, pos)
122+
if err != nil {
123+
return encodedValue{}, pos, fmt.Errorf("reading annotation value %d: %w", i, err)
124+
}
125+
}
126+
return encodedValue{}, pos, nil
127+
128+
default:
129+
return encodedValue{}, pos, fmt.Errorf("unknown encoded_value type 0x%02x at offset 0x%x", valueType, pos-1)
130+
}
131+
}
132+
133+
// readSignedEncodedInt reads size bytes of little-endian data and sign-extends the result.
134+
func readSignedEncodedInt(
135+
data []byte,
136+
pos uint32,
137+
size uint32,
138+
) (encodedValue, uint32, error) {
139+
if pos+size > uint32(len(data)) {
140+
return encodedValue{}, pos, fmt.Errorf("signed int truncated at offset 0x%x (need %d bytes)", pos, size)
141+
}
142+
143+
var val int64
144+
for i := uint32(0); i < size; i++ {
145+
val |= int64(data[pos+i]) << (8 * i)
146+
}
147+
148+
// Sign-extend from the topmost byte.
149+
signBit := int64(1) << (size*8 - 1)
150+
if val&signBit != 0 {
151+
val |= ^((signBit << 1) - 1)
152+
}
153+
154+
return encodedValue{intVal: val}, pos + size, nil
155+
}
156+
157+
// readUnsignedEncodedInt reads size bytes of little-endian data as an unsigned value.
158+
func readUnsignedEncodedInt(
159+
data []byte,
160+
pos uint32,
161+
size uint32,
162+
) (encodedValue, uint32, error) {
163+
if pos+size > uint32(len(data)) {
164+
return encodedValue{}, pos, fmt.Errorf("unsigned int truncated at offset 0x%x (need %d bytes)", pos, size)
165+
}
166+
167+
var val int64
168+
for i := uint32(0); i < size; i++ {
169+
val |= int64(data[pos+i]) << (8 * i)
170+
}
171+
172+
return encodedValue{intVal: val}, pos + size, nil
173+
}

0 commit comments

Comments
 (0)