Androidのアプリを逆コンパイル

こんにちは、五十嵐です。

今回は簡単なアプリのアセンブルをしてみようと思います。

androidアプリケーションの逆コンパイルは基本的な作業なので、少し内容が薄いと思う。
基礎なのでおさらい

/*

内容は逆コンパイルを推奨するものではありません。規約を守りましょう。

*/

動作環境

Windows7 home premium

 検証端末

Nexus5 / Android 5.0 Lollipop

 

1.AndroidからAPKを取り出す。

apkとはAndroidのアプリケーションです。

APKファイルは、JARファイルをベースとしたZIP形式で、アーカイブファイルの一種である。 wikipediaより引用

とある。iOSで言う「.ipa」ファイルというものだと思う。

自分のAndroidにapkを取り出すアプリケーションをインストールする。GoolePlayで調べると簡単に出てくる。

インストールして、アプリを実行するとアプリ名.apkと言うファイルができると思う。
(ここはアプリケーションによってフォルダを作ったり/sdcard/に格納されたりいろいろある)
それをPCに持っていく。

 

2.APKを展開してみる。

持ってきたAPKファイルを7zip等の解凍ソフトで展開する。Macの場合は7zxを利用するといいと思う

無理なにおいがするがアーカイブファイルなので可能。強引に展開する。

すると

Meta-INF/

res/

AndroidManifest.xml

classes.dex

resources.arsc

というファイル・フォルダが見える。

ここで大抵の人が欲してる情報はresとclasses.dex、AndroidManifest.xmlに集約している。

画像の素材が見たい方はresの中を適当に見渡せば見ることが可能。

Layoutのxml等は当然そのままの形式では見ることができない。

xmlは以下のサイトにある「akptool」というツールを使えば簡単に解読できる。

android-apktool - A tool for reverse engineering Android apk files - Google Project Hosting

C:\ >.\apktool.bat  d  .\application.apk

と打つだけで簡単に取得できるので特に問題ない。string.xmlの中を覗くと結構いろいろ見ることができる。

なお、公式で配布されているAndroidDeviceMonitorを使えば以下のようにボタンひとつでViewの構造を把握できるので個人的にはお勧め。

f:id:eyesjapan:20141123042441p:plain

蛇足だが、Androidを開発する上で利用される Dalvik Debug Monitor Server(DDMS) を使うことでAndroidの事は結構分かったりするのでInstallしておくと良い。

Logcatを眺めているとたまにおかしな内容が流れていたりするのでお茶のお供に最適。

 

さて、ここで一番の価値があるのは「classes.dex」なのです。コレがアプリの肝。

3.classes.dexを開いてみる!

ということで開いていく。

とはいえ、コマンドを打つだけなので簡単。

dex2jar - Tools to work with android .dex and java .class files - Google Project Hosting

というツールを利用する。

 C:\> .\dex2jar.bat .\classes.dex

コレでjarファイルが生成できたので後は自由に楽む。

お勧めは

Java Decompiler


 

 

・あまりに内容が薄いので比較をしてみようと思う。

自作のコード

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction()
                    .add(R.id.container1, new SampleNormalFragment())
                    .commit();
            new PostTask().execute("hoge");
        }
    }

    public static class SampleNormalFragment extends Fragment {

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, final ViewGroup container,
                                 Bundle savedInstanceState) {
            View rootView = inflater.inflate(R.layout.fragment_layout, container, false);
            final TextView textView = (TextView) rootView.findViewById(R.id.text_fragment);
            textView.setText("Hello World!!");
            return rootView;
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_settings) {
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    public class PostTask extends AsyncTask<String, Integer, Integer> {

        @Override
        protected Integer doInBackground(String... contents) {
            String url = "http://example.com/";
            HttpClient httpClient = new DefaultHttpClient();
            HttpPost post = new HttpPost(url);

            List<NameValuePair> params = new ArrayList<NameValuePair>();
            params.add(new BasicNameValuePair("content", contents[0]));
            HttpResponse res = null;
            try {
                post.setEntity(new UrlEncodedFormEntity(params, "utf-8"));
                res = httpClient.execute(post);
            } catch (IOException e) {
                e.printStackTrace();
            }
            assert res != null;
            return res.getStatusLine().getStatusCode();
        }

        @Override
        protected void onPostExecute(Integer integer) {
            super.onPostExecute(integer);
            Toast.makeText(getApplicationContext(),"result:"+ integer,Toast.LENGTH_SHORT).show();
        }
    }
}

という平凡なコードをBuildし、インストール。apkとして取り出して逆コンパイルをしてみたものが

public class MainActivity extends Activity
{
  protected void onCreate(Bundle paramBundle)
  {
    super.onCreate(paramBundle);
    setContentView(2130903064);
    if (paramBundle == null)
    {
      getFragmentManager().beginTransaction().add(2131230780, new SampleFragment()).commit();
      new PostTask().execute(new String[] { "hoge" });
    }
  }

  public boolean onCreateOptionsMenu(Menu paramMenu)
  {
    getMenuInflater().inflate(2131492864, paramMenu);
    return true;
  }

  public boolean onOptionsItemSelected(MenuItem paramMenuItem)
  {
    if (paramMenuItem.getItemId() == 2131230782)
      return true;
    return super.onOptionsItemSelected(paramMenuItem);
  }

  public class PostTask extends AsyncTask<String, Integer, Integer>
  {
    static
    {
      if (!MainActivity.class.desiredAssertionStatus());
      for (boolean bool = true; ; bool = false)
      {
        $assertionsDisabled = bool;
        return;
      }
    }

    public PostTask()
    {
    }

    protected Integer doInBackground(String[] paramArrayOfString)
    {
      DefaultHttpClient localDefaultHttpClient = new DefaultHttpClient();
      HttpPost localHttpPost = new HttpPost("http://example.com/");
      ArrayList localArrayList = new ArrayList();
      localArrayList.add(new BasicNameValuePair("content", paramArrayOfString[0]));
      HttpResponse localHttpResponse1;
      try
      {
        localHttpPost.setEntity(new UrlEncodedFormEntity(localArrayList, "utf-8"));
        HttpResponse localHttpResponse2 = localDefaultHttpClient.execute(localHttpPost);
        localHttpResponse1 = localHttpResponse2;
        if ((!$assertionsDisabled) && (localHttpResponse1 == null))
          throw new AssertionError();
      }
      catch (IOException localIOException)
      {
        while (true)
        {
          localIOException.printStackTrace();
          localHttpResponse1 = null;
        }
      }
      return Integer.valueOf(localHttpResponse1.getStatusLine().getStatusCode());
    }

    protected void onPostExecute(Integer paramInteger)
    {
      super.onPostExecute(paramInteger);
      Toast.makeText(MainActivity.this.getApplicationContext(), "result:" + paramInteger, 0).show();
    }
  }

  public static class SampleFragment extends Fragment
  {
    public void onCreate(Bundle paramBundle)
    {
      super.onCreate(paramBundle);
    }

    public View onCreateView(LayoutInflater paramLayoutInflater, ViewGroup paramViewGroup, Bundle paramBundle)
    {
      View localView = paramLayoutInflater.inflate(2130903065, paramViewGroup, false);
      ((TextView)localView.findViewById(2131230781)).setText("Hello World!!!");
      return localView;
    }
  }
}

では大まかな部分のdiffを取ってみる
1.onCreate

super.onCreate(paramBundle);
setContentView(R.layout.activity_main);
if (savedInstanceState == null) {
    getFragmentManager().beginTransaction()
        .add(R.id.container1, new SampleFragment())
        .commit();
    new PostTask().execute("hoge");
}
super.onCreate(paramBundle);
setContentView(2130903064);
if (paramBundle == null)
{
    getFragmentManager().beginTransaction().add(2131230780, new SampleFragment()).commit();
    new PostTask().execute(new String[] { "hoge" });
}

setContentViewの内容はR.laout.*が参照先の数字となっている。
R.classの中にspublic static final class layoutというクラスがあるのでそこにactivity_mainという名前の方があるので参照可能。

new PostTask().execute("hoge")は分かりやすい。
new PostTask().execute(new String[] { "hoge" });となっただけ。


2.PostTask.doInBackground

      DefaultHttpClient localDefaultHttpClient = new DefaultHttpClient();
      HttpPost localHttpPost = new HttpPost("http://example.com/");
      ArrayList localArrayList = new ArrayList();
      localArrayList.add(new BasicNameValuePair("content", paramArrayOfString[0]));
      HttpResponse localHttpResponse1;
      try
      {
        localHttpPost.setEntity(new UrlEncodedFormEntity(localArrayList, "utf-8"));
        HttpResponse localHttpResponse2 = localDefaultHttpClient.execute(localHttpPost);
        localHttpResponse1 = localHttpResponse2;
        if ((!$assertionsDisabled) && (localHttpResponse1 == null))
          throw new AssertionError();
      }
      catch (IOException localIOException)
      {
        while (true)
        {
          localIOException.printStackTrace();
          localHttpResponse1 = null;
        }
      }
      return Integer.valueOf(localHttpResponse1.getStatusLine().getStatusCode());
      DefaultHttpClient localDefaultHttpClient = new DefaultHttpClient();
      HttpPost localHttpPost = new HttpPost("http://example.com/");
      ArrayList localArrayList = new ArrayList();
      localArrayList.add(new BasicNameValuePair("content", paramArrayOfString[0]));
      HttpResponse localHttpResponse1;
      try
      {
        localHttpPost.setEntity(new UrlEncodedFormEntity(localArrayList, "utf-8"));
        HttpResponse localHttpResponse2 = localDefaultHttpClient.execute(localHttpPost);
        localHttpResponse1 = localHttpResponse2;
        if ((!$assertionsDisabled) && (localHttpResponse1 == null))
          throw new AssertionError();
      }
      catch (IOException localIOException)
      {
        while (true)
        {
          localIOException.printStackTrace();
          localHttpResponse1 = null;
        }
      }
      return Integer.valueOf(localHttpResponse1.getStatusLine().getStatusCode());

簡単に説明すると、http://example.com/hogeという値を送っている。
ここでポイントなのが当然ですが、ほぼ正確に逆コンパイルできています。urlまで丸見え。

3.結論

Androidでいろいろ隠すのは難しいと常に思う。
 サーバー側のセキュリティを向上させるしかない。
 別の方法でできるだけ鍵を持たないようにする。
 (自分的にToken Vending Machineが好みです)
 SharedPreferanceを使う。(Root化には無力だが……)

CTF的な視点だと


Androidの問題は逆コンパイルをしてからが問題であり、できて当然な感じがあるので
この動作はScriptを書いて自動化したほうがよいと思う。
CTFの場合apkが配布されるのですぐに逆コンパイル可能。

・Android5.0に変わって


Root化が難しいとかなんとか
ARTに完全移行したとか
いろいろ聞く。
なので絶望せずに気を引き締めて開発をしていこう。