1 of 69

Samael Wang (燒賣)

Bytecode 101 �for Android App Developers

2 of 69

About Me

Grindr Android Developer

since 2018

3 of 69

Why to Read Bytecode?

4 of 69

1. Devices are not born equal

Mismatch

5 of 69

1. Devices are not born equal

Mismatch

  • OEM Customizations
  • Different Security Patches

6 of 69

2. Understanding language

details

  • Type erasure/suspend function/extension function
  • VerifyError

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init> (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

  • Low-level optimizations

7 of 69

3. Mitigating 3rd-party issues

Ever wonder how Jettifier or Firebase Performance Plugin work?

Common Bytecode Manipulation Tools:

8 of 69

3. Mitigating 3rd-party issues

e.g. changing the try-block in a 3rd-party jar

9 of 69

JVM Bytecode Basics

10 of 69

JVM Bytecode

  • Portable instruction set for interpreters/VMs.
  • One-byte opcodes, so no more than 256 opcodes.
    • ~50 remain unused on JVM.
  • Not human-readable. We’d read disassembly instead.
    • JVM => javap, “show kotlin bytecode”
    • Dalvik => baksmali (directly or thru apktool)
  • Stack-based (JVM) vs. Register-based (Dalvik/ART).

11 of 69

Disassemble classes with javap

Java

sample/app/build/intermediates/javac/debug/classes

❯ javap -p -v com.example.bytecode.JavaBytecodeSample

Kotlin

sample/app/build/tmp/kotlin-classes/debug

❯ javap -p -v com.example.bytecode.MainActivity

3rd-party Libraries

~/.gradle/caches/modules-2/files-2.1/org.jxmpp/jxmpp-core/0.6.3/<hash>

❯ javap -p -v "jar:file://$PWD/jxmpp-core-0.6.3.jar\!/org/jxmpp/xml/splitter/XmlSplitter.class"

12 of 69

Constant Pool

Constant pool:

#1 = Methodref #6.#20 // java/lang/Object."<init>":()V

#2 = String #21 // Test

#3 = Methodref #22.#23 // java/lang/String.valueOf:(I)Ljava/lang/String;

#4 = Methodref #24.#25 // android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

#5 = Class #26 // com/example/bytecode/JavaBytecodeSample

#6 = Class #27 // java/lang/Object

#7 = Utf8 <init>

#8 = Utf8 ()V

Magic

Version

Constant Pool

Access Flags

This Class

Super Class

Interfaces

Fields

Methods

Attributes

Class file structure

13 of 69

Method Sample

public void testLoop();

descriptor: ()V

flags: (0x0001) ACC_PUBLIC

Code:

stack=2, locals=2, args_size=1

0: iconst_0

1: istore_1

2: iload_1

3: bipush 10

5: if_icmpge 24

8: ldc #2 // String Test

10: iload_1

11: invokestatic #3 // Method

java/lang/String.valueOf:(I)Ljava/lang/String;

14: invokestatic #4 // Method

android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

17: pop

18: iinc 1, 1

21: goto 2

24: return

LineNumberTable:

line 8: 0

line 9: 8

line 8: 18

line 11: 24

LocalVariableTable:

Start Length Slot Name Signature

2 22 1 i I

0 25 0 this Lcom/example/bytecode/JavaBytecodeSample;

7: public void testLoop() {

8: for (int i = 0; i < 10; i++) {

9: Log.d("Test", String.valueOf(i));

10: }

11: }

14 of 69

Method Descriptor

public void testLoop();

descriptor: ()V

flags: (0x0001) ACC_PUBLIC

Code:

stack=2, locals=2, args_size=1

Examples

int d(String tag, String msg) → (Ljava/lang/String;Ljava/lang/String;)I

void showUptime(long) → (J)V

15 of 69

Stack & Locals

public void testLoop();

descriptor: ()V

flags: (0x0001) ACC_PUBLIC

Code:

stack=2, locals=2, args_size=1

0: iconst_0

1: istore_1

2: iload_1

24: return

7: public void testLoop() {

8: for (int i = 0; i < 10; i++) {

9: Log.d("Test", String.valueOf(i));

10: }

11: }

LocalVariableTable:

Start Length Slot Name Signature

2 22 1 i I

0 25 0 this Lcom/example/bytecode/JavaBytecodeSample;

16 of 69

Local Variable Occupations

1 slot

2 slots

boolean

byte

char

short

int

float�reference�returnAddress

long

double

17 of 69

Operand Stack

JVM Spec 2.6.2. Operand Stacks

Each entry on the operand stack can hold a value of any Java Virtual Machine type, including a value of type long or type double.

“Whatever”

SP

18 of 69

Operand Stack

3

“Whatever”

iconst_3

iload_1

iadd

istore_1

SP

8

this

v0

v1

v2

19 of 69

Operand Stack

8

3

“Whatever”

iconst_3

iload_1

iadd

istore_1

SP

8

this

v0

v1

v2

20 of 69

Operand Stack

11

“Whatever”

iconst_3

iload_1

iadd

istore_1

SP

8

this

v0

v1

v2

21 of 69

Operand Stack

“Whatever”

iconst_3

iload_1

iadd

istore_1

SP

11

this

v0

v1

v2

22 of 69

LineNumberTable

public void testLoop();

descriptor: ()V

flags: (0x0001) ACC_PUBLIC

Code:

stack=2, locals=2, args_size=1

0: iconst_0

8: ldc #2 // String Test

18: iinc 1, 1

24: return

LineNumberTable:

line 8: 0

line 9: 8

line 8: 18

line 11: 24

7: public void testLoop() {

8: for (int i = 0; i < 10; i++) {

9: Log.d("Test", String.valueOf(i));

10: }

11: }

23 of 69

Exception table

0: new #5 // class java/lang/RuntimeException

3: dup

4: ldc #6 // String crash

6: invokespecial #7 // Method

java/lang/RuntimeException."<init>":(Ljava/lang/String;)V

9: athrow

10: astore_1

11: ldc #2 // String Test

13: ldc #8 // String error

15: invokestatic #4 // Method

android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

18: pop

19: return

Exception table:

from to target type

0 10 10 Class java/lang/RuntimeException

public void testException() {

try {

throw new RuntimeException("crash");

} catch (RuntimeException e) {

Log.d("Test", "error");

}

}

24 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

ldc #2 // String Test

ldc #8 // String error

invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

pop

return

25 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

ldc #2 // String Test

ldc #8 // String error

invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

pop

return

“Test”

26 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

ldc #2 // String Test

ldc #8 // String error

invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

pop

return

“error”

“Test”

27 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

ldc #2 // String Test

ldc #8 // String error

invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

pop

return

12

28 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

ldc #2 // String Test

ldc #8 // String error

invokestatic #4 // Method android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I

pop

return

29 of 69

Common Types of Invocations

without “this”

with “this”

no vtable lookup

invokestatic

invokespecial

<init>, super, private

requires vtable lookup

invokevirtual

invokeinterface

30 of 69

“Show Kotlin Bytecode”

31 of 69

Dalvik/ART

32 of 69

Disassemble a dex/apk

baksmali

sample/app/build/intermediates/dex/debug/mergeDexDebug

❯ baksmali d classes.dex

apktool

sample/app/build/outputs/apk/debug

❯ apktool d app-debug.apk

33 of 69

Method Sample

.method public testLoop()V

.registers 4

.line 8

const/4 v0, 0x0

.local v0, "i":I

:goto_1

const/16 v1, 0xa

if-ge v0, v1, :cond_11

.line 9

invoke-static {v0},

Ljava/lang/String;->valueOf(I)Ljava/lang/String;

move-result-object v1

const-string v2, "Test"

invoke-static {v2, v1},

Landroid/util/Log;->d(

Ljava/lang/String;Ljava/lang/String;)I

.line 8

add-int/lit8 v0, v0, 0x1

goto :goto_1

.line 11

.end local v0 # "i":I

:cond_11

return-void

.end method

7: public void testLoop() {

8: for (int i = 0; i < 10; i++) {

9: Log.d("Test", String.valueOf(i));

10: }

11: }

34 of 69

Locals & Params

Java Example

public void testLoop() {

for (int i = 0; i < 10; i++) {

Log.d("Test", String.valueOf(i));

}

}

Kotlin Example

private fun showUptime(uptime: Long) {

txt_uptime.text =

getString(R.string.uptime, uptime)

}

.method public testLoop()V

.locals 3

.local v0, "i":I

.end local v0 # "i":I

.method private final showUptime(J)V

.locals 4

.param p1, "uptime" # J

35 of 69

Line Numbers

.line 8

add-int/lit8 v0, v0, 0x1

goto :goto_0

.line 11

.end local v0 # "i":I

:cond_0

return-void

7: public void testLoop() {

8: for (int i = 0; i < 10; i++) {

9: Log.d("Test", String.valueOf(i));

10: }

11: }

36 of 69

Exceptions

:try_start_0

const-string v1, "crash"

:try_end_0

.catch Ljava/lang/RuntimeException; {:try_start_0 .. :try_end_0} :catch_0

:catch_0

move-exception v0

return-void

public void testException() {

try {

throw new RuntimeException("crash");

} catch (RuntimeException e) {

Log.d("Test", "error");

}

}

37 of 69

Operating on Registers

const/4 v0, 0x3

const/4 v1, 0x8

add-int v0, v1, v0

this

v0

v1

p0/v2

38 of 69

Operating on Registers

const/4 v0, 0x3

const/4 v1, 0x8

add-int v0, v1, v0

this

3

v0

v1

p0/v2

39 of 69

Operating on Registers

const/4 v0, 0x3

const/4 v1, 0x8

add-int v0, v1, v0

this

8

3

v0

v1

p0/v2

40 of 69

Operating on Registers

const/4 v0, 0x3

const/4 v1, 0x8

add-int v0, v1, v0

this

8

11

v0

v1

p0/v2

41 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

const-string v0, "Test"

const-string v1, "error"

invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

move-result-object v0

this

"Test"

v0

v1

p0/v2

42 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

const-string v0, "Test"

const-string v1, "error"

invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

move-result-object v0

this

"error”

"Test"

v0

v1

p0/v2

43 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

const-string v0, "Test"

const-string v1, "error"

invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

move-result-object v0

this

"error”

"Test"

v0

v1

p0/v2

44 of 69

Invocations

Java

Log.d("Test", "error");

Bytecode Assembly

const-string v0, "Test"

const-string v1, "error"

invoke-static {v0, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

move-result-object v0

this

"error”

12

v0

v1

p0/v2

45 of 69

Invocation Opcode

Mapping

JVM

Dalvik

invokestatic

invoke-static

invokevirtual

invoke-virtual

invokeinterface

invoke-interface

invokespecial

invoke-super

super

invoke-direct

<init>, private

46 of 69

Appendix A

VerifyError

Case Study

47 of 69

KT-35616

fun errorResumeWithDefaultParametersPreL() {

GlobalScope.launch {

val foo = Foo(baz = randomInt())

Log.d(TAG, "foo=$foo")

}

}

private suspend fun randomInt(): Int = withContext(Dispatchers.IO) {

return@withContext Random.nextInt()

}

class Foo(val bar: Boolean = false, val baz: Int = 0)

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init> (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend (Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

48 of 69

Bytecode Verification

  • The defensive JVM approach - part of the Java security model.
  • Enforcement of some important properties, such as
    • No illegal type conversions.
    • No violation to access restrictions.
    • No subclassing to a final class.
    • No operand stack overflows or underflows�(for stack-based VMs).
  • VerifyError: the verifier rejected the bytecode.

49 of 69

What the Verifier Said

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend

(Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

The verification of this class has failed:

com.example.verifyerror.Sample$errorResumeWithDefaultParameters$1

50 of 69

What the Verifier Said

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend

(Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

Because the verification of this function failed:

invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;

51 of 69

What the Verifier Said

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend

(Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

Because the verification of an operation 0x70 (invoke-direct) at 0x0040 failed.

(refer https://source.android.com/devices/tech/dalvik/dalvik-bytecode)

52 of 69

What the Verifier Said

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend

(Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

Which is a constructor call to Foo with this method descriptor

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

53 of 69

What the Verifier Said

W/dalvikvm( 3892): VFY: register1 v1 type 17, wanted 5

W/dalvikvm( 3892): VFY: rejecting call to Lcom/example/verifyerror/Foo;.<init>

(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

W/dalvikvm( 3892): VFY: rejecting opcode 0x70 at 0x0040

W/dalvikvm( 3892): VFY: rejected

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;.invokeSuspend

(Ljava/lang/Object;)Ljava/lang/Object;

W/dalvikvm( 3892): Verifier rejected class

Lcom/example/verifyerror/Sample$errorResumeWithDefaultParameters$1;

Because the verification of register v1 failed.

The verifier was expecting for type 5 (kRegTypeBoolean) but found type 17 (kRegTypeInteger).

54 of 69

Locate the Invocation

.line 49

iget v1, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->label:I

if-eqz v1, :cond_21

# restore state and continue execution

.local v0, "$this$launch":Lkotlinx/coroutines/CoroutineScope;

iget v1, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->I$0:I

goto :goto_38

# save state and suspend

:cond_21

const/4 v4, 0x0

iput v4, p0, Lcom/example/verifyerror/Sample$errorResumeWithDefaultParametersPreL$1;->I$0:I

:goto_38

invoke-direct {v5, v1, v4, v3, v2},

Lcom/example/verifyerror/Foo;-><init>(ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V

The suspension point at constructor invocation caused the parameters being recorded in the state machine.

However the stored type didn’t match the constructor. It should be Z rather than I.

On resuming it cause v1 being int rather than boolean.

55 of 69

Dalvik

Category 1

Registers

Conversion

Table

No conversion from int to boolean.

56 of 69

Works on ART

ART only check if it’s an integral type (int, short, byte, boolean).

57 of 69

Workaround

fun errorResumeWithDefaultParametersPreL() {

GlobalScope.launch {

val baz = randomInt()

val foo = Foo(baz = baz)

Log.d(TAG, "foo=$foo")

}

}

private suspend fun randomInt(): Int = withContext(Dispatchers.IO) {

return@withContext Random.nextInt()

}

class Foo(val bar: Boolean = false, val baz: Int = 0)

Prevent the suspension point from storing the constructor parameters.

58 of 69

Appendix B

Debugging with on-device framework.jar

59 of 69

Disassemble

Device Frameworks

60 of 69

File Types

OAT

ELF file which contains compiled machine code. Before Oreo it also contains the original DEX. The extension can be .oat or .odex.

VDEX

Introduced in Oreo to store original DEXs. OAT no longer contains DEXs.

ART

We don’t really care about this one, but it’s basically common object dumps to speed up process initialization.

OAT before Android O

61 of 69

Tools

62 of 69

Extract Device

System Frameworks

Android 10

❯ adb pull /system/framework/framework.jar

+ adb pull /system/framework/framework.jar

/system/framework/framework.jar: 1 file pulled, 0 skipped. 142.8 MB/s (27265346 bytes in 0.182s)

❯ unzip framework.jar

Archive: framework.jar

extracting: classes.dex

extracting: classes2.dex

extracting: classes3.dex

❯ for i in *.dex; do baksmali d $i; done

63 of 69

Extract Device

System Frameworks

Android 8.x ~ 9.x

❯ adb pull /system/framework/boot-framework.vdex

+ adb pull /system/framework/boot-framework.vdex

/system/framework/boot-framework.vdex: 1 file pulled, 0 skipped. 33.7 MB/s (29357052 bytes in 0.830s)

❯ /opt/vdex-extractor/tools/deodex/run.sh -i .

[INFO]: Processing 1 input Vdex files

[INFO]: 1 binaries have been successfully deodexed

❯ for i in vdexExtractor_deodexed/boot-framework/*.dex; do baksmali d $i; done

64 of 69

Extract Device

System Frameworks

Android 5.x ~ 7.x

❯ oat2dex devfw

07-31 16:14:03:213 Preparing boot jars from sony-g3426-RQ3007D4NJ

07-31 16:14:03:866 Pulling /system/framework/arm/boot-framework.oat -> /data/device-dump/android-n/boot-raw

❯ baksmali list dex boot-jar-with-dex/framework.jar

classes.dex

classes2.dex

❯ unzip boot-jar-with-dex/framework.jar

Archive: boot-jar-with-dex/framework.jar

inflating: classes.dex

inflating: classes2.dex

❯ for i in *.dex; do baksmali d $i; done

65 of 69

Extract Device

System Frameworks

Android Pre-L

❯ adb pull /system/framework/framework.jar

+ adb -s ZX1PD2R346 pull /system/framework/framework.jar

/system/framework/framework.jar: 1 file pulled, 0 skipped. 6.1 MB/s (3762126 bytes in 0.587s)

❯ baksmali d framework.jar

66 of 69

Run Debugger with Smali

67 of 69

Smalidea

  • Install the plugin

https://github.com/JesusFreke/smalidea

  • Ensure the .smali is associated to the plugin (the one with a black S).

68 of 69

Import Smali

  • Copy all smali files extracted to app/smali �(the directory name doesn’t really matter)
  • Set breakpoints

69 of 69

Run