Jump to content

Infinitode 2


datboibaku

Recommended Posts

  • 3 weeks later...

Not sure if it helps but I'll just leave my findings here hoping it'd help someone. I'm not good at reverse engineering and static analysis so forgive me if I make mistakes.

As of writing, the latest update is 1.7.1. Resources are now not Int but rather CheatSafeInt (CSI) which code can be found by disassembling the APK in JADX (I really do recommend anyone looking to take a stab to look at the code). CheatSafeInt seems to depend heavily on FastRandom (FR) which again depends on GDX's RandomXS128. Both CSI and FR can be found under "Source code -> com -> prineside.tdi2 -> utils"

Initialization of a CSI which need two values i and i2 which after some reading basically means Value (V) and ResetValue (RsV) with the latter one being the value to which the game will reset V to when V doesn't satisfy a check. Note that the reset value is negative (will be explained later). Upon initialization 5 ints and 1 long is initialized inside a CSI object in order of (int)a, (int)b, (int)c, (long)d, (int)e, (int)f. I'm not sure which is which but when I looked at the memory around the CSI scanned, it somewhat correlates to CSI's structure with the address below the value scanned seemed to crash the game when modified, somehow.

  • e is RsV
  • f is V
  • a,b,c are random generated based on V and RsV
    • a,b are generated by
      Math.min(FastRandom.getInt(V), FastRandom.getInt(16) + 4) - RsV

       

    • c is generated by
      Math.min(FastRandom.getInt(V * 2), FastRandom.getInt(16) + 4) - RsV;
  • d is calculated through a,b,c via
    (long) (((this.f + this.a) - this.b) - this.c);

     

The CSI have two methods called add() and sub() which add or subtract from the CSI without triggering the cheat-trap mechanism. The mechanism work on the get() by checking

if (((long) this.f) == ((this.d + ((long) this.c)) + ((long) this.b)) - ((long) this.a))

If this check returns a true, nothing happens. Else it would reset V to RsV. Note that while in the initialization e is set to -RsV, upon check fail it'd would set to -e which is --RsV hence RsV. There is also a getSetOnCheat() but this will return the value of the CSI to be resetted to when cheating is detected. I don't find a use of the method anywhere using JADX but I have a feeling I'm missing something. Also I'm leaving relevant source code of FR and its dependency RSX128 here also.

FR:

Spoiler

package com.prineside.tdi2.utils;

import com.badlogic.gdx.math.RandomXS128;
import com.prineside.tdi2.Game;
import com.prineside.tdi2.managers.ResearchManager;
import java.util.Random;

public class FastRandom {

    /* renamed from: a  reason: collision with root package name */
    public static final float[] f6777a = new float[ResearchManager.MAP_SIZE];

    /* renamed from: b  reason: collision with root package name */
    public static int f6778b = 0;
    public static final RandomXS128 random = new RandomXS128();

    static {
        for (int i = 0; i < 8192; i++) {
            f6777a[i] = random.nextFloat();
        }
    }

    public static String generateUniqueDistinguishableId() {
        String distinguishableString = StringFormatter.distinguishableString(Game.getTimestampSeconds());
        random.setState(new Random().nextLong(), Game.getTimestampMillis());
        return getDistinguishableString(4, random) + "-" + getDistinguishableString(4, random) + "-" + distinguishableString.substring(distinguishableString.length() - 6, distinguishableString.length());
    }

    public static String getDistinguishableString(int i, RandomXS128 randomXS128) {
        if (randomXS128 == null) {
            randomXS128 = random;
        }
        StringBuilder sb = new StringBuilder(i);
        for (int i2 = 0; i2 < i; i2++) {
            sb.append(StringFormatter.DISTINGUISHABLE_STRING_CHARS.charAt(randomXS128.nextInt(32)));
        }
        return sb.toString();
    }

    public static float getFairFloat() {
        return random.nextFloat();
    }

    public static int getFairInt(int i) {
        return random.nextInt(i);
    }

    public static float getFloat() {
        f6778b++;
        if (f6778b == 8192) {
            f6778b = 0;
        }
        return f6777a[f6778b];
    }

    public static int getInt(int i) {
        return (int) (getFloat() * ((float) i));
    }
}

 

RSX128:

Spoiler

package com.badlogic.gdx.math;

import java.util.Random;

public class RandomXS128 extends Random {

    /* renamed from: a  reason: collision with root package name */
    public long f3916a;

    /* renamed from: b  reason: collision with root package name */
    public long f3917b;

    public RandomXS128() {
        setSeed(new Random().nextLong());
    }

    public static final long a(long j) {
        long j2 = (j ^ (j >>> 33)) * -49064778989728563L;
        long j3 = (j2 ^ (j2 >>> 33)) * -4265267296055464877L;
        return j3 ^ (j3 >>> 33);
    }

    public long getState(int i) {
        return i == 0 ? this.f3916a : this.f3917b;
    }

    public final int next(int i) {
        return (int) (nextLong() & ((1 << i) - 1));
    }

    public boolean nextBoolean() {
        return (nextLong() & 1) != 0;
    }

    public void nextBytes(byte[] bArr) {
        int length = bArr.length;
        while (length != 0) {
            int i = length < 8 ? length : 8;
            long nextLong = nextLong();
            while (true) {
                int i2 = i - 1;
                if (i != 0) {
                    length--;
                    bArr[length] = (byte) ((int) nextLong);
                    nextLong >>= 8;
                    i = i2;
                }
            }
        }
    }

    public double nextDouble() {
        double nextLong = (double) (nextLong() >>> 11);
        Double.isNaN(nextLong);
        return nextLong * 1.1102230246251565E-16d;
    }

    public float nextFloat() {
        double nextLong = (double) (nextLong() >>> 40);
        Double.isNaN(nextLong);
        return (float) (nextLong * 5.9604644775390625E-8d);
    }

    public int nextInt() {
        return (int) nextLong();
    }

    public long nextLong() {
        long j = this.f3916a;
        long j2 = this.f3917b;
        this.f3916a = j2;
        long j3 = j ^ (j << 23);
        long j4 = ((j3 >>> 17) ^ (j3 ^ j2)) ^ (j2 >>> 26);
        this.f3917b = j4;
        return j4 + j2;
    }

    public void setSeed(long j) {
        if (j == 0) {
            j = Long.MIN_VALUE;
        }
        long a2 = a(j);
        setState(a2, a(a2));
    }

    public void setState(long j, long j2) {
        this.f3916a = j;
        this.f3917b = j2;
    }

    public int nextInt(int i) {
        return (int) nextLong((long) i);
    }

    public RandomXS128(long j) {
        setSeed(j);
    }

    public RandomXS128(long j, long j2) {
        setState(j, j2);
    }

    public long nextLong(long j) {
        long nextLong;
        long j2;
        if (j > 0) {
            do {
                nextLong = nextLong() >>> 1;
                j2 = nextLong % j;
            } while ((j - 1) + (nextLong - j2) < 0);
            return j2;
        }
        throw new IllegalArgumentException("n must be positive");
    }
}

 

 

Link to comment
Share on other sites

  • 4 weeks later...
On 2/23/2020 at 5:50 PM, frankly_why said:

Not sure if it helps but I'll just leave my findings here hoping it'd help someone. I'm not good at reverse engineering and static analysis so forgive me if I make mistakes.

As of writing, the latest update is 1.7.1. Resources are now not Int but rather CheatSafeInt (CSI) which code can be found by disassembling the APK in JADX (I really do recommend anyone looking to take a stab to look at the code). CheatSafeInt seems to depend heavily on FastRandom (FR) which again depends on GDX's RandomXS128. Both CSI and FR can be found under "Source code -> com -> prineside.tdi2 -> utils"

Initialization of a CSI which need two values i and i2 which after some reading basically means Value (V) and ResetValue (RsV) with the latter one being the value to which the game will reset V to when V doesn't satisfy a check. Note that the reset value is negative (will be explained later). Upon initialization 5 ints and 1 long is initialized inside a CSI object in order of (int)a, (int)b, (int)c, (long)d, (int)e, (int)f. I'm not sure which is which but when I looked at the memory around the CSI scanned, it somewhat correlates to CSI's structure with the address below the value scanned seemed to crash the game when modified, somehow.

  • e is RsV
  • f is V
  • a,b,c are random generated based on V and RsV
    • a,b are generated by
      
      Math.min(FastRandom.getInt(V), FastRandom.getInt(16) + 4) - RsV

       

    • c is generated by
      
      Math.min(FastRandom.getInt(V * 2), FastRandom.getInt(16) + 4) - RsV;
  • d is calculated through a,b,c via
    
    (long) (((this.f + this.a) - this.b) - this.c);

     

The CSI have two methods called add() and sub() which add or subtract from the CSI without triggering the cheat-trap mechanism. The mechanism work on the get() by checking


if (((long) this.f) == ((this.d + ((long) this.c)) + ((long) this.b)) - ((long) this.a))

If this check returns a true, nothing happens. Else it would reset V to RsV. Note that while in the initialization e is set to -RsV, upon check fail it'd would set to -e which is --RsV hence RsV. There is also a getSetOnCheat() but this will return the value of the CSI to be resetted to when cheating is detected. I don't find a use of the method anywhere using JADX but I have a feeling I'm missing something. Also I'm leaving relevant source code of FR and its dependency RSX128 here also.

FR:

  Reveal hidden contents


package com.prineside.tdi2.utils;

import com.badlogic.gdx.math.RandomXS128;
import com.prineside.tdi2.Game;
import com.prineside.tdi2.managers.ResearchManager;
import java.util.Random;

public class FastRandom {

    /* renamed from: a  reason: collision with root package name */
    public static final float[] f6777a = new float[ResearchManager.MAP_SIZE];

    /* renamed from: b  reason: collision with root package name */
    public static int f6778b = 0;
    public static final RandomXS128 random = new RandomXS128();

    static {
        for (int i = 0; i < 8192; i++) {
            f6777a[i] = random.nextFloat();
        }
    }

    public static String generateUniqueDistinguishableId() {
        String distinguishableString = StringFormatter.distinguishableString(Game.getTimestampSeconds());
        random.setState(new Random().nextLong(), Game.getTimestampMillis());
        return getDistinguishableString(4, random) + "-" + getDistinguishableString(4, random) + "-" + distinguishableString.substring(distinguishableString.length() - 6, distinguishableString.length());
    }

    public static String getDistinguishableString(int i, RandomXS128 randomXS128) {
        if (randomXS128 == null) {
            randomXS128 = random;
        }
        StringBuilder sb = new StringBuilder(i);
        for (int i2 = 0; i2 < i; i2++) {
            sb.append(StringFormatter.DISTINGUISHABLE_STRING_CHARS.charAt(randomXS128.nextInt(32)));
        }
        return sb.toString();
    }

    public static float getFairFloat() {
        return random.nextFloat();
    }

    public static int getFairInt(int i) {
        return random.nextInt(i);
    }

    public static float getFloat() {
        f6778b++;
        if (f6778b == 8192) {
            f6778b = 0;
        }
        return f6777a[f6778b];
    }

    public static int getInt(int i) {
        return (int) (getFloat() * ((float) i));
    }
}

 

RSX128:

  Reveal hidden contents


package com.badlogic.gdx.math;

import java.util.Random;

public class RandomXS128 extends Random {

    /* renamed from: a  reason: collision with root package name */
    public long f3916a;

    /* renamed from: b  reason: collision with root package name */
    public long f3917b;

    public RandomXS128() {
        setSeed(new Random().nextLong());
    }

    public static final long a(long j) {
        long j2 = (j ^ (j >>> 33)) * -49064778989728563L;
        long j3 = (j2 ^ (j2 >>> 33)) * -4265267296055464877L;
        return j3 ^ (j3 >>> 33);
    }

    public long getState(int i) {
        return i == 0 ? this.f3916a : this.f3917b;
    }

    public final int next(int i) {
        return (int) (nextLong() & ((1 << i) - 1));
    }

    public boolean nextBoolean() {
        return (nextLong() & 1) != 0;
    }

    public void nextBytes(byte[] bArr) {
        int length = bArr.length;
        while (length != 0) {
            int i = length < 8 ? length : 8;
            long nextLong = nextLong();
            while (true) {
                int i2 = i - 1;
                if (i != 0) {
                    length--;
                    bArr[length] = (byte) ((int) nextLong);
                    nextLong >>= 8;
                    i = i2;
                }
            }
        }
    }

    public double nextDouble() {
        double nextLong = (double) (nextLong() >>> 11);
        Double.isNaN(nextLong);
        return nextLong * 1.1102230246251565E-16d;
    }

    public float nextFloat() {
        double nextLong = (double) (nextLong() >>> 40);
        Double.isNaN(nextLong);
        return (float) (nextLong * 5.9604644775390625E-8d);
    }

    public int nextInt() {
        return (int) nextLong();
    }

    public long nextLong() {
        long j = this.f3916a;
        long j2 = this.f3917b;
        this.f3916a = j2;
        long j3 = j ^ (j << 23);
        long j4 = ((j3 >>> 17) ^ (j3 ^ j2)) ^ (j2 >>> 26);
        this.f3917b = j4;
        return j4 + j2;
    }

    public void setSeed(long j) {
        if (j == 0) {
            j = Long.MIN_VALUE;
        }
        long a2 = a(j);
        setState(a2, a(a2));
    }

    public void setState(long j, long j2) {
        this.f3916a = j;
        this.f3917b = j2;
    }

    public int nextInt(int i) {
        return (int) nextLong((long) i);
    }

    public RandomXS128(long j) {
        setSeed(j);
    }

    public RandomXS128(long j, long j2) {
        setState(j, j2);
    }

    public long nextLong(long j) {
        long nextLong;
        long j2;
        if (j > 0) {
            do {
                nextLong = nextLong() >>> 1;
                j2 = nextLong % j;
            } while ((j - 1) + (nextLong - j2) < 0);
            return j2;
        }
        throw new IllegalArgumentException("n must be positive");
    }
}

 

 

wonderful analysis of game

 

Link to comment
Share on other sites

  • 1 month later...

 First, thanks @frankly_why for posting the source files! The `RSX128` and `FastRandom` classes really don't have much to do with how `CheatSafeInt` is working. It might look complicated but it's really a simple matter of arithmetic. But let's look at what's happening inside `RSX128` and `FastRandom` doing anyway!

Looking at the code you posted, RandomXS128 seems to be an implementation of Xorshift algorithm. The `FastRandom` class seems to be a thin caching layer on top of Xorshift128 to pre-generate cheap and fast random numbers with acceptable quality with minimal impact to game's performance.

Let's look at the relevant parts from `FastRandom`. It pre-generates 8192 random floating point numbers between 0.0 and 1.0 and every time `getFloat()` is called, it'll use `f6778b` to index into the array of pre-generated random numbers and return one after increment the counter. That little `if` block just wraps the counter around when it reaches the end of the array.

    static {
        for (int i = 0; i < 8192; i++) {
            f6777a[i] = random.nextFloat();
        }
    }

    public static float getFloat() {
        f6778b++;
        if (f6778b == 8192) {
            f6778b = 0;
        }
        return f6777a[f6778b];
    }

    public static int getInt(int i) {
        return (int) (getFloat() * ((float) i));
    }

The method that is used in CheatSafeInt is `getint()` which simply calls `getFloat()` to get a pseudo-random float and multiplies it by the argument passed into it. This results in an integer between 0 and `i` to be returned to caller.

So, for our purposes here you can think of all the calls to `FastRandom.getInt(V)` to result in an integer in the range of 0 to V to be returned to us.

There is really no need to understand how this random value is computed to defeat this system.

 

Now, let's look at the `CheatSafeInt.set` code. You could use any java decompiler to see this, I used CFR here:

    public void set(int value) {
        this.V = value;
        int value_half = value / 2;
        this.A = Math.min(FastRandom.getInt(value), FastRandom.getInt(16) + 4) - value_half;  // [-value_half, -value_half + 20]
        this.B = Math.min(FastRandom.getInt(value), FastRandom.getInt(16) + 4) - value_half;  // [-value_half, -value_half + 20]
        this.C = Math.min(FastRandom.getInt(value * 2), FastRandom.getInt(16) + 4) - value_half;  // [-value_half, -value_half + 20] .. using value*2 doesn't affect the range since this is inside a Math.min() call
        this.D = (long)(this.V + this.A - this.B - this.C);
    }

If you make a simplifying assumption that `V` is always larger than 16, a quick back of the envelope calculation shows that all of these three variables end up with a value in the range of `-V/2` and `20 - V/2`. The exact values of `A`, `B` and `C` don't really matter but knowing the range makes it easier to search for them in the memory.

Finally `D` is calculated as : `V + A + -B + -C`. Now let's look at the `CheatSafeInt.get` code:

    public int get() {
        if(((long)this.V) != this.D + ((long)this.C) + ((long)this.B) - ((long)this.A)) {
            int v0 = -this.reset_val_neg;
            this.set(v0);
            return v0;
        }

        return this.V;
    }

As, @frankly_why pointed out in his post, the logic that determines the validity of a `CheatSafeInt` value is on the second line. Let's rewrite that condition without the casts:

V == D + C + B - A

Now let's expand `D` and replace it with the value it was assigned in `CheatSafeInt.set`:

V == (V + A - B - C) + C + B - A

As you can see, the `+A` here cancels with `-A`, same with `-C` and `C` and `-B` and `B` leaving `V == V`. So as long as the value held by `D` stays equal to `V + A - B - C` this check will always pass.

Knowing this, defeating this scheme is as easy as increment both `V` and `D` variables by the same amount.

Another practical, but less elegant solution is to set the reset value to -9999 and trigger the detection mechanism to force the value to be overwritten with -RsV.

 

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.