[TDOhex] Flutter_Reverse_Engineering_Demo
[TDOhex] Flutter_Reverse_Engineering_Demo
Architecture: arm64-v8a
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.
Why "0x20" is "true" and "0x30" is "false"? We will explain the answer further.
Demonstration:
Version: 3.5.7(357)
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
}
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.
As like:
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
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.
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"?
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:
The "isSubscribed" function is called within a function named "getMaxSubtitleDuration" at adress "0x4d12a0".
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.
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".
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 "
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.
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:
" 0x4d12c8: add x0, PP, #0x2d, lsl #12 ; [pp+0x2d1e8] Obj!Duration@7350c1 "
[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 : {
off_8: int(0x35a4e900)