Text-To-Speech speaks pwned

Text-To-Speech engine is a default enabled module in all Android phones, and exists up to Android 1.5 HTC era, even acting as a selling point at that time. But various vendor implementations may lead to various interesting stuff, i.e. CVE-2019-16253, a seemly harmless language pack, or nearly any seemly benign application, without requring any permission, can obtain a persistent SYSTEM shell through the TTS bug (or feature?).

Vulnerability Briefing

TL;DR: Samsung TTS component (a.k.a SMT) is a privileged process running in system uid, responsible for managing the whole TTS functionality. It has a privilege escalation vulnerability (or feature?), which can be exploited by malicious applications to gain system-app privilege without requiring any permission or user interaction.

systemuid

SMT application declares a exported receiver in com.samsung.SMT.mgr.LangPackMgr$2, registered by SamsungTTSService->onCreate => LangPackMgr->init which accepts Intent with action com.samsung.SMT.ACTION_INSTALL_FINISHED. The receiver blindly trusts incoming data supplied by SMT_ENGINE_PATH, and after some processing, LangPackMgr.updateEngine creates a thread which calls com.samsung.SMT.engine.SmtTTS->reloadEngine which lead to a System->load, leading to arbitrary code execution in SMT itself. It at first glance seems unbelievable but it does actually exist, a typical local privilege escalation vulnerability.

What’s worth mentioning is that this vulnerability does not require manually starting the attacking application. With carefully crafted arguments, installing the seemly benign POC apk will trigger this vulnerability. Besides, as SMT will restart every registered library at boot time, attacker can silently obtain a persistent shell without user notice.

Imagine a malicious actor uploads a seemly normal application containing the exploitation code to each Android Application market. As the exploit does not require the application asking for any privilege, it’s very likely to evade the screening process of various markets. As long as users download and install it, a persistent system shell is given out to attacker. Attacker can use this privilege to sniff on SMS, call logs/recordings, contacts, photos, or even use it as a stepstone for further attacking other privileged components, like other system services and the kernel.

Vulnerability analysis

The corresponding vulnerable code is listed below, some omitted and renamed for better visibility:

package com.samsung.SMT.mgr;

class LangPackMgr$2 extends BroadcastReceiver {
//...
    public void onReceive(Context arg10, Intent arg11) {
        int v7 = -1;
        if(arg11.getAction().equals("com.samsung.SMT.ACTION_INSTALL_FINISHED")) {
           //...
            int v0_1 = arg11.getIntExtra("SMT_ENGINE_VERSION", v7);
            String v2 = arg11.getStringExtra("SMT_ENGINE_PATH");
            if(v0_1 > SmtTTS.get().getEngineVersion() && (CString.isValid(v2))) {
                if(CFile.isExist(v2)) {
                    LangPackMgr.getUpdateEngineQueue(this.a).add(new LangPackMgr$UpdateEngineInfo(v0_1, v2));
                    CLog.i(CLog$eLEVEL.D, "LangPackMgr - Add candidate engine [%d][%s]", new Object[]{Integer.valueOf(v0_1), v2});
                }
                else {
                    CLog.e("LangPackMgr - Invalid engine = " + v2);
                }
            }
//...
            LangPackMgr.decreaseTriggerCount(this.a);
            if(LangPackMgr.getTriggerPackageCount(this.a) != 0) {
                return;
            }

            if(LangPackMgr.getUpdateEngineQueue(this.a).size() <= 0) {
                return;
            }

            LangPackMgr.doUpdateEngine(this.a);
        }
    }
}

After some checks, a new LangPackMgr.UpdateEngineInfo is added to the Queue. In doUpdateEngine,

    private void updateEngine() {
        if(this.mThreadUpdateEngine == null || !this.mThreadUpdateEngine.isAlive()) {
            this.mThreadUpdateEngine = new LangPackMgr$EngineUpdateThread(this, null);
            this.mThreadUpdateEngine.start();
        }
    }
    
        LangPackMgr$EngineUpdateThread(LangPackMgr arg1, LangPackMgr$1 arg2) {
        this(arg1);
    }

    public void run() {
    //...
        if(LangPackMgr.getUpdateEngineQueue(this.a).size() <= 0) {
            return;
        }

        try {
            v1 = LangPackMgr.getUpdateEngineQueue(this.a).poll();
            while(true) {
                if(LangPackMgr.getUpdateEngineQueue(this.a).size() <= 0) {
                    goto label_20;
                }

                v0_1 = LangPackMgr.getUpdateEngineQueue(this.a).poll();
//...
            if(v1 != null && ((LangPackMgr$UpdateEngineInfo)v1).VERSION > SmtTTS.get().getEngineVersion()) {
                CHelper.get().set_INSTALLED_ENGINE_PATH(((LangPackMgr$UpdateEngineInfo)v1).PATH);
                if(SmtTTS.get().reloadEngine()) {
                
-----
    public boolean reloadEngine() {
//...
        this.stop();
        try {
            String v0_2 = CHelper.get().INSTALLED_ENGINE_PATH();
            if(CString.isValid(v0_2)) {
                System.load(v0_2); //<- triggers load
            }
            else {
                goto label_70;
            }
        }

SmtTTS.reloadEngine finally reaches System.load, with the path supplied in Intent.

To successfully reach this code path, some conditions should be met for the attacking Intent sent out, which is listed as follows:

  • SMT_ENGINE_VERSION in Intent should be larger than the embeded version (361811291)
  • mTriggerCount should be first increased. This can be achieved by supplying a package name begins with com.samsung.SMT.lang. As mentioned above, com.samsung.SMT.SamsungTTSService registers two receivers, one of which will scan for this package prefix and increase the mTriggerCount for us.

One problem still exists though. As I stated above, triggering this vulnerability does not require starting the attacking app. How is this fulfilled?

It turns out SMT does this job for us. The code piece in SMT that calls our attacking service without user interaction is as follows:

package com.samsung.SMT.mgr;
//...
class LangPackMgr$1 extends BroadcastReceiver {
    LangPackMgr$1(LangPackMgr arg1) {
        this.a = arg1;
        super();
    }

    public void onReceive(Context arg4, Intent arg5) {
        String v0 = arg5.getAction();
        String v1 = arg5.getData().getSchemeSpecificPart();
        if(((v0.equals("android.intent.action.PACKAGE_ADDED")) || (v0.equals("android.intent.action.PACKAGE_CHANGED")) || (v0.equals("android.intent.action.PACKAGE_REMOVED"))) && (v1 != null && (v1.startsWith("com.samsung.SMT.lang")))) {
            this.a.syncLanguagePack();
        }
    }
}
//...
    private void triggerLanguagePack() {
        if(this.mThreadTriggerLanguagePack == null || !this.mThreadTriggerLanguagePack.isAlive()) {
            this.mThreadTriggerLanguagePack = new LangPackMgr$LanguagePackTriggerThread(this, null);
            this.mThreadTriggerLanguagePack.start();
        }
    }

The checking thread reaches here:

package com.samsung.SMT.mgr;
//...

class LangPackMgr$LanguagePackTriggerThread extends Thread {
  //...

    public void run() {
        Object v0_1;
        HashMap v3 = new HashMap();
        HashMap v4 = new HashMap();
        try {
            Iterator v5 = LangPackMgr.f(this.langpackmgr).getPackageManager().getInstalledPackages(0x2000).iterator();
            while(true) {
        //...
                if(!((PackageInfo)v0_1).packageName.startsWith("com.samsung.SMT.lang")) {
                    continue;
                }

                break;
            }
        }
        catch(Exception v0) {
            goto label_53;
        }

        try {
            Intent v1_1 = new Intent(((PackageInfo)v0_1).packageName);
            v1_1.setPackage(((PackageInfo)v0_1).packageName);
            LangPackMgr.f(this.langpackmgr).startService(v1_1);
            LangPackMgr.increaseTriggerCount(this.langpackmgr);

SMT has some requirements for the loaded library (it should look like an language pack… implements some interfaces), which can be resolved by reversing the default library.

So the whole attack flows as below:

smtflow

Step to reproduce with the provided POC

A demo video shows the POC of getting system shell.

If you want to change the remote IP and port for the reverse shell, please modify solib/jni/libmstring/mstring.c

    ip = "172.16.x.x";

To your own service addr. You will need to rebuild the project (run ndk-build in solib directory, copy the arm64-v8a/libmstring.so to the APK project (src/main/jniLibs/arm64-v8a/) and rebuild the APK.

Also, monitoring the logcat reveals the following output:

➜  ~ adb logcat | grep -i SamsungTTS
 21:27:09.851 16662 16662 I SamsungTTS: Init CHelper
 21:27:09.908 16662 16662 I SamsungTTS: Success to load EMBEDDED engine.
 21:27:09.980 16662 16662 I SamsungTTS: Empty install voice data : STUB_IDLE
 21:27:10.001 16662 16684 I SamsungTTS: Request check version [82]
 21:27:10.044 16662 16684 E SamsungTTS: Invalid response. request=82 receive=0
 21:27:17.155 16662 16885 I SamsungTTS: Success to reload INSTALLED engine.
 21:27:17.155 16662 16885 I SamsungTTS: Restart engine...
Which shows the malicious engine is loaded, and output from the malicious engine library printing it’s uid 
➜  ~ adb logcat | grep -i mercury
 16:29:48.816 24289 24317 E mercury-native: somehow I'm in the library yah, my uid is 1000
 16:29:48.885 24318 24318 E mercury-native: weasel begin connect
Which shows our library is running in the SMT context and as system uid.

The full POC will be available shortly at https://github.com/flankerhqd/vendor-android-cves

Conclusion

Vendor has already published fix through Galaxy Appstore and others. Updated Samsung Text-To-Speech application to following versions in corresponding markets, and examine any previously installed application with package name starts with com.samsung.SMT.lang

For all Samsung devices:

  • Android N,O or older systems, please update SMT to 3.0.00.101 or higher
  • Android P, please update SMT to 3.0.02.7 or higher

This issue is assigned CVE-2019-16253.

7 thoughts on “Text-To-Speech speaks pwned

  1. gandalf

    Hello, I tried this with my A5 2015 A500FU with versionCode 201503041 of the com.samsung.SMT apk and firmware 6.0.1, but it didn’t work.
    Is there a way to check if the vulnerability was closed?
    I changed the hexadecimal value of SMT_ENGINE_VERSION accordingly, changed the path from arm64 to arm and recompiled libmstring for armeabi-v7.
    The service also already doesn’t even start, so it’s probably fixed by now.

    Reply
  2. Elizabeth

    How is a lay person able to fix this problem? I keep getting notifications from Lookout that this app is causing major problems. When I go to the Galaxy store and try and update it, it just says that it’s updating then the button turns to a light green which means you can’t do anything. What should I do please? If someone could help me I would greatly appreciate it. Thank you

    Reply

Leave a Reply to flanker017 Cancel reply

Your email address will not be published. Required fields are marked *