Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(profiling): Deobfuscate Android methods' signature #53427

Merged
merged 13 commits into from
Jul 25, 2023
Merged
89 changes: 89 additions & 0 deletions src/sentry/profiles/java.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from typing import List, Tuple

JAVA_BASE_TYPES = {
"Z": "boolean",
"B": "byte",
"C": "char",
"S": "short",
"I": "int",
"J": "long",
"F": "float",
"D": "double",
"V": "void",
}


# parse_obfuscated_signature will parse an obfuscated signatures into parameter
# and return types that can be then deobfuscated
def parse_obfuscated_signature(signature: str) -> Tuple[List[str], str]:
signature = signature[1:]
parameter_types, return_type = signature.rsplit(")", 1)
types = []
i = 0
arrays = 0

while i < len(parameter_types):
t = parameter_types[i]

if t in JAVA_BASE_TYPES:
start_index = i - arrays
types.append(parameter_types[start_index : i + 1])
arrays = 0
elif t == "L":
start_index = i - arrays
end_index = parameter_types[i:].index(";")
types.append(parameter_types[start_index : i + end_index + 1])
arrays = 0
i += end_index
elif t == "[":
arrays += 1
else:
arrays = 0

i += 1

return types, return_type


# format_signature formats the types into a human-readable signature
def format_signature(parameter_java_types: List[str], return_java_type: str) -> str:
signature = f"({', '.join(parameter_java_types)})"
if return_java_type and return_java_type != "void":
signature += f": {return_java_type}"
return signature


def byte_code_type_to_java_type(byte_code_type: str) -> str:
token = byte_code_type[0]
if token in JAVA_BASE_TYPES:
return JAVA_BASE_TYPES[token]
elif token == "L":
return byte_code_type[1 : len(byte_code_type) - 1].replace("/", ".")
elif token == "[":
return f"{byte_code_type_to_java_type(byte_code_type[1:])}[]"
else:
return byte_code_type


# map_obfucated_signature will parse then deobfuscated a signature and
# format it appropriately
def deobfuscate_signature(mapper, signature: str) -> str:
if not signature:
return ""

parameter_types, return_type = parse_obfuscated_signature(signature)
parameter_java_types = []

for parameter_type in parameter_types:
new_class = byte_code_type_to_java_type(parameter_type)
mapped = mapper.remap_class(new_class)
if mapped:
new_class = mapped
Zylphrex marked this conversation as resolved.
Show resolved Hide resolved
parameter_java_types.append(new_class)

return_java_type = byte_code_type_to_java_type(return_type) if return_type else ""
mapped = mapper.remap_class(return_java_type)
if mapped:
return_java_type = mapped

return format_signature(parameter_java_types, return_java_type)
3 changes: 3 additions & 0 deletions src/sentry/profiles/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from sentry.lang.native.symbolicator import RetrySymbolication, Symbolicator, SymbolicatorTaskKind
from sentry.models import EventError, Organization, Project, ProjectDebugFile
from sentry.profiles.device import classify_device
from sentry.profiles.java import deobfuscate_signature
from sentry.profiles.utils import get_from_profiling_service
from sentry.signals import first_profile_received
from sentry.tasks.base import instrumented_task
Expand Down Expand Up @@ -655,6 +656,8 @@ def _deobfuscate(profile: Profile, project: Project) -> None:
else:
method["data"]["deobfuscation_status"] = "missing"

method["signature"] = deobfuscate_signature(mapper, method["signature"])


@metrics.wraps("process_profile.track_outcome")
def _track_outcome(
Expand Down
48 changes: 48 additions & 0 deletions tests/sentry/profiles/test_java.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from tempfile import mkstemp

import pytest
from symbolic.proguard import ProguardMapper

from sentry.profiles.java import deobfuscate_signature

PROGUARD_SOURCE = b"""\
# compiler: R8
# compiler_version: 2.0.74
# min_api: 16
# pg_map_id: 5b46fdc
# common_typos_disable
# {"id":"com.android.tools.r8.mapping","version":"1.0"}
org.slf4j.helpers.Util$ClassContextSecurityManager -> org.a.b.g$a:
65:65:void <init>() -> <init>
67:67:java.lang.Class[] getClassContext() -> a
69:69:java.lang.Class[] getExtraClassContext() -> a
65:65:void <init>(org.slf4j.helpers.Util$1) -> <init>
"""


@pytest.fixture
def mapper():
_, mapping_file_path = mkstemp()
with open(mapping_file_path, "wb") as f:
f.write(PROGUARD_SOURCE)
mapper = ProguardMapper.open(mapping_file_path)
assert mapper.has_line_info
return mapper


@pytest.mark.parametrize(
["obfuscated", "expected"],
[
("", ""),
("()", "()"),
("([I)", "(int[])"),
("(III)", "(int, int, int)"),
("([Ljava/lang/String;)", "(java.lang.String[])"),
("([[J)", "(long[][])"),
("(I)I", "(int): int"),
("([B)V", "(byte[])"),
],
)
def test_deobfuscate_signature(mapper, obfuscated, expected):
result = deobfuscate_signature(mapper, obfuscated)
assert result == expected
6 changes: 4 additions & 2 deletions tests/sentry/profiles/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,18 @@
"profile": {
"methods": [
{
"name": "a",
"abs_path": None,
"class_name": "org.a.b.g$a",
"name": "a",
"signature": "()V",
"source_file": None,
"source_line": 67,
},
{
"name": "a",
"abs_path": None,
"class_name": "org.a.b.g$a",
"name": "a",
"signature": "()V",
"source_file": None,
"source_line": 69,
},
Expand Down
Loading