0% found this document useful (0 votes)
21 views

[TDOhex] Flutter_Reverse_Engineering_Demo

The document provides a detailed guide on reverse engineering a Flutter application using Dart assembly, specifically focusing on the 'isSubscribed' function. It explains how to manipulate Boolean values in assembly, demonstrates the function's logic, and suggests various methods for patching it to always return true. Additionally, it discusses the implications of the function's return value on subsequent operations within the app.

Uploaded by

yukitsukumo2004
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
21 views

[TDOhex] Flutter_Reverse_Engineering_Demo

The document provides a detailed guide on reverse engineering a Flutter application using Dart assembly, specifically focusing on the 'isSubscribed' function. It explains how to manipulate Boolean values in assembly, demonstrates the function's logic, and suggests various methods for patching it to always return true. Additionally, it discusses the implications of the function's return value on subsequent operations within the app.

Uploaded by

yukitsukumo2004
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

Dive into Assembly with TDOhex

Flutter Reverse Engineering

Language Code: Dart Assembly

Architecture: arm64-v8a

Author: Dimension of TDO

Video Tutorial: Flutter Examples to Reach Place to Patch Through revenuecat

First of all, you should remember that in Dart assembly, to set a Boolean value to "true", " add x0, x22, 0x20 "
is used. "x0" is the destination register where the result of the addition will be stored, and this register can be
either 32-bit (Wd) or 64-bit (Xd). And in Dart assembly, to set a Boolean value to "false", " add x0, x22, 0x30 "
is used.

Assembly for return true:


add x0, x22, 0x20
ret
Encoded Hex: C0820091C0035FD6

Assembly for return false:


add x0, x22, 0x30
ret
Encoded Hex: C0C20091C0035FD6

Why "0x20" is "true" and "0x30" is "false"? We will explain the answer further.

Demonstration:

App Name: Automatic Subtitles & Captions

Version: 3.5.7(357)

Play Store Link:

Automatic Subtitles & Captions

In this app, there is a Boolean function named " isSubscribed " located at the address "0x3d8abc".
static bool isSubscribed() {
// ** addr: 0x3d8abc, size: 0x7c
// 0x3d8abc: EnterFrame
// 0x3d8abc: stp fp, lr, [SP, #-0x10]!
// 0x3d8ac0: mov fp, SP
// 0x3d8ac4: AllocStack(0x10)
// 0x3d8ac4: sub SP, SP, #0x10
// 0x3d8ac8: CheckStackOverflow
// 0x3d8ac8: ldr x16, [THR, #0x38] ; THR::stack_limit
// 0x3d8acc: cmp SP, x16
// 0x3d8ad0: b.ls #0x3d8b30
// 0x3d8ad4: r0 = InitLateStaticField(0xb68) // [package:automatic_subtitler/main.dart] ::cach
// 0x3d8ad4: ldr x0, [THR, #0x68] ; THR::field_table_values
// 0x3d8ad8: ldr x0, [x0, #0x16d0]
// 0x3d8adc: ldr x16, [PP, #0x38] ; [pp+0x38] Sentinel
// 0x3d8ae0: cmp w0, w16
// 0x3d8ae4: b.ne #0x3d8af4
// 0x3d8ae8: add x2, PP, #0xd, lsl #12 ; [pp+0xd518] Field <::.cachedPurchase
// 0x3d8aec: ldr x2, [x2, #0x518]
// 0x3d8af0: bl #0x70e9e8
// 0x3d8af4: LoadField: r1 = r0->field_27
// 0x3d8af4: ldur w1, [x0, #0x27]
// 0x3d8af8: DecompressPointer r1
// 0x3d8af8: add x1, x1, HEAP, lsl #32
// 0x3d8afc: r16 = <CachedPurhase>
// 0x3d8afc: add x16, PP, #0xd, lsl #12 ; [pp+0xd520] TypeArguments:
// 0x3d8b00: ldr x16, [x16, #0x520]
// 0x3d8b04: stp x1, x16, [SP]
// 0x3d8b08: r4 = const [0x1, 0x1, 0x1, 0x1, null]
// 0x3d8b08: ldr x4, [PP, #0xce0] ; [pp+0xce0] List(5) [0x1, 0x1, 0x1, 0x1, N
// 0x3d8b0c: r0 = IterableExtension.firstOrNull()
// 0x3d8b0c: bl #0x407940 ; [package:collection/src/iterable_extensions.dart
// 0x3d8b10: cmp w0, NULL
// 0x3d8b14: r16 = true
// 0x3d8b14: add x16, NULL, #0x20 ; true
// 0x3d8b18: r17 = false
// 0x3d8b18: add x17, NULL, #0x30 ; false
// 0x3d8b1c: csel x1, x16, x17, ne
// 0x3d8b20: mov x0, x1
// 0x3d8b24: LeaveFrame
// 0x3d8b24: mov SP, fp
// 0x3d8b28: ldp fp, lr, [SP], #0x10
// 0x3d8b2c: ret
// 0x3d8b2c: ret
// 0x3d8b30: r0 = StackOverflowSharedWithoutFPURegs()
// 0x3d8b30: bl #0x7106ac ; StackOverflowSharedWithoutFPURegsStub
// 0x3d8b34: b #0x3d8ad4
}

Q. How did we get the "isSubscribed" function?

A. We analyzed it using common keywords, it depends on basic experience.

Let's break down "isSubscribed" function:

1. From address "0x3d8abc" to "0x3d8ad0" common sets up to create a function.

2. From address "0x3d8ad4" to "0x3d8b08" loading static field values of " cachedPurchases " list.

3. At address "0x3d8b0c" call " firstOrNull() " method. It is to return the first non-null element of the
iterable, or null if there are no non-null elements as like " hasNext() ".

4. " 0x3d8b10 cmp w0, NULL ". Compare the result of " firstOrNull() " (in w0) with NULL.

5. " 0x3d8b14: add x16, NULL, #0x20 ; true ". True value as like " const/4 v16, 0x1 "

6. " 0x3d8b18: add x17, NULL, #0x30 ; false ". False value as like " const/4 v17, 0x0 "

7. " 0x3d8b1c: csel x1, x16, x17, ne ". csel "Conditional Select." It selects the (true) value from "x16" if
the condition specified by "ne" (not equal (to NULL)) is true from "w0", otherwise, it selects the (false)
value from "x17". And stores it in "x1". In this case there is no any subscription in the list so,
" firstOrNull() " will return NULL, and csel will selects the (false) value from "x17" and stores it in "x1".

8. " 0x3d8b20: mov x0, x1 ". Move the value of register x1 into register x0.

Potential Solutions for Patching this Function:

1. Go to function address "0x3d8abc" and put:

add x0, x22, 0x20


ret

As like:

const/4 v0, 0x1


return v0

2. Go to address "0x3d8b18" and change " add x17, x22, 0x30 " to " add x17, x22, 0x20 "

3. Go to address "0x3d8b1c" and change csel x1, x16, x17, ne to csel x1, x17, x16, ne

4. Or change "x16, x17" to "x16, x16".

5. Or change "ne" to "eq"

6. Go to address "0x3d8b20" and change " mov x0, x1 " to " add x0, x22, 0x20 ".
How to return true value:

There are several ways to return a true value from this function. We will discuss three ways. You can return
any value whose 4th bit should be 0.

1. " add x0, x22, 0x20 "

2. " mov x0, 0x1 "

3. " mov x0, 0x0 "

We choose " mov x0, 0x1" and " mov x0, 0x0 " because our members were confused with them.

Q. How can the value of a Boolean be set to "true" in Dart assembly using "mov x0, 0x0"?

A. Don't be shocked, we're explaining.

The machine doesn't care about how you've stored the value, it only sees what value you've stored.

If you have stored "0x20", "0x1", or "0x0", the machine will load that hexadecimal value into binary
representation.

Whenever "isSubscribed located at the address 0x3d8abc" function is called, the data will be loaded in binary
representation, and that data is:

# If you return 0x20 from "isSubscribed"


x0 = 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00100000

# If you return 0x1 from "isSubscribed"


x0 = 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

# If you return 0x0 from "isSubscribed"


x0 = 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Now let's look at a call to the "isSubscribed" function.

The "isSubscribed" function is called within a function named "getMaxSubtitleDuration" at adress "0x4d12a0".

static Duration getMaxSubtitleDuration() {


// ** addr: 0x4d12a0, size: 0x44
// 0x4d12a0: EnterFrame
// 0x4d12a0: stp fp, lr, [SP, #-0x10]!
// 0x4d12a4: mov fp, SP
// 0x4d12a8: CheckStackOverflow
// 0x4d12a8: ldr x16, [THR, #0x38] ; THR::stack_limit
// 0x4d12ac: cmp SP, x16
// 0x4d12b0: b.ls #0x4d12dc
// 0x4d12b4: r0 = isSubscribed()
// 0x4d12b4: bl #0x3d8abc ; [package:automatic_subtitler/main.dart] ::isSubs
// 0x4d12b8: tbnz w0, #4, #0x4d12c8
// 0x4d12bc: r0 = Instance_Duration
// 0x4d12bc: add x0, PP, #0x2d, lsl #12 ; [pp+0x2d1e0] Obj!Duration@7350d1
// 0x4d12c0: ldr x0, [x0, #0x1e0]
// 0x4d12c4: b #0x4d12d0
// 0x4d12c8: r0 = Instance_Duration
// 0x4d12c8: add x0, PP, #0x2d, lsl #12 ; [pp+0x2d1e8] Obj!Duration@7350c1
// 0x4d12cc: ldr x0, [x0, #0x1e8]
// 0x4d12d0: LeaveFrame
// 0x4d12d0: mov SP, fp
// 0x4d12d4: ldp fp, lr, [SP], #0x10
// 0x4d12d8: ret
// 0x4d12d8: ret
// 0x4d12dc: r0 = StackOverflowSharedWithoutFPURegs()
// 0x4d12dc: bl #0x7106ac ; StackOverflowSharedWithoutFPURegsStub
// 0x4d12e0: b #0x4d12b4
}
}

" 0x4d12b4: bl #0x3d8abc "

At address "0x4d12b4", the "isSubscribed located at the address 0x3d8abc" function is called. And the data
loaded from that call is stored in a 32-bit value in "r0" or let's say "w0" and that data is.

# If you return 0x20 from "isSubscribed"


w0 = 00000000 00000000 00000000 00100000

# If you return 0x1 from "isSubscribed"


w0 = 00000000 00000000 00000000 00000001

# If you return 0x0 from "isSubscribed"


w0 = 00000000 00000000 00000000 00000000
Explanation of tbnz #4:

" 0x4d12b8: tbnz w0, #4, #0x4d12c8 "

At address "0x4d12b8", there is a conditional instruction that checks if the 4th bit from the right side of the
data loaded into "w0" is nonzero(!0) or not. If the 4th bit from the right side of the loaded data is set to one,
the instruction jumps to address "0x4d12c8", and if it's set to zero, the instruction doesn't jump.

In our example, the 4th bit is set to zero after patching of "isSubscribed" function, so the instructions will not
be jumped to address "0x4d12c8".

Let's test of 4th bit:

# If you return 0x20 from "isSubscribed"


𝟬 0 0 0 0 = 4th bit of w0 from right side
𝟰 3 2 1 0 = positions of bits

# If you return 0x1 from "isSubscribed"


𝟬 0 0 0 0 = 4th bit of w0 from right side
𝟰 3 2 1 0 = positions of bits

# If you return 0x0 from "isSubscribed"


𝟬 0 0 0 0 = 4th bit of w0 from right side
𝟰 3 2 1 0 = positions of bits

All three methods have the 4th bit set to zero. So the next address will continue at "0x4d12bc":

" 0x4d12bc: add x0, PP, #0x2d, lsl #12 ; [pp+0x2d1e0] Obj!Duration@7350d1 "

And address "0x4d12c8" will be skipped.

Note: Remember that in Dart assembly, the 4th bit of data is tested to check the Boolean value, while in
another assembly, the 0th position bit is checked to verify the Boolean value.

Blutter advantages:

Let's check what data is being stored in "0x4d12bc". For that, you'll thank "Blutter". "Blutter" has given us two
types of locations for checking data.

1. " pp+0x2d1e0 "

2. " Obj!Duration@7350d1 "

To know about the first location, open the " pp.txt " file generated by "Blutter", go to the address
" pp+0x2d1e0 ", you'll find this.

[pp+0x2d1e0] Obj!Duration@7350d1 : {

off_8: int(0x35a4e900)

At address " pp+0x2d1e0 ", you'll find integer type data represented in hexadecimal as " 0x35a4e900 ", if you
convert it to decimal, you'll get " 900000000 ". These are " 15:00 " minutes in microseconds given to premium
users.
Default behaviour:

Supposed, If we don't patch "isSubscribed" function, the data loaded in "w0" will be:

" w0 = 00000000 00000000 00000000 00110000 "

With its 4th bit set to one:

# w0 = 0x30 Default value of "isSubscribed"


𝟭 0 0 0 0 = 4th bit of w0 from right side
𝟰 3 2 1 0 = position of bits

And the instruction will jump to address "0x4d12c8":

" 0x4d12c8: add x0, PP, #0x2d, lsl #12 ; [pp+0x2d1e8] Obj!Duration@7350c1 "

And the data retrieved from address "pp+0x2d1e8" is:

[pp+0x2d1e8] Obj!Duration@7350c1 : {

off_8: int(0x11e1a300)

Converting " 0x11e1a300 " to decimal will result in " 300000000 ", meaning " 05:00 " minutes for free users.

To understand the second location, open the "obj.txt" file and search for

" Obj!Duration@7350d1 ", you'll find:

Obj!Duration@7350d1 : {

off_8: int(0x35a4e900)

The rest we have explained.

Main channel: Dimensions of TDO

Second channel: Useful Patches

Discussion group: Discussion of TDOhex

You might also like