외로운 Nova의 작업실

insecurebankv2 - 로컬 암호화 이슈 취약점 본문

Mobile App Penetesting/Android App Vulnerability

insecurebankv2 - 로컬 암호화 이슈 취약점

Nova_ 2023. 5. 8. 15:10

- 취약점 소개

안드로이드 애플리케이션은 특정 정보들을 저장해야할때가 있습니다. 이 특정 정보들을 암호화를 적용하여 저장하게되는데, 짧은 키값을 쓰거나, 소스 코드를 얻으면 키값이 노출되는 경우가 있습니다. 

 

- 취약점 진단

보통 앱들에는 자동 로그인 기능이 있습니다. insecurebank에서도 Autofill credencial 기능이 있습니다.

이 기능은 최근에 로그인했던 정보를 핸드폰내에 저장하고, 그값을 불러와 로그인을 하는 메커니즘을 가지고 있습니다. 즉, 로그인 했던 정보들이 어딘가에 저장되어있습니다. 보통 공유 프리퍼런스에 저장하니 한번 검사해보겠습니다.

 

<공유 프리퍼런스>

먼저 공유 프리퍼런스 파일이 있는 경로로 들어가줍니다.

/data/data/com.android.insecurebankv2/shared_prefs

여기에 mySharedPreferences.xml 파일이 있는데, 출력해봅시다.

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="superSecurePassword">v/sJpihDCo2ckDmLW5Uwiw==&#10;    </string>
    <string name="EncryptedUsername">amFjaw==&#13;&#10;    </string>
</map>

최근에 로그인했던 username과 password가 있습니다. 이는 각각 암호화되어있는 것을 확인할 수 있습니다. 어떻게 암호화 하는지 한번 소스파일을 봐보겠습니다. DoLogin$RequestTask.class 파일을 보겠습니다.

package com.android.insecurebankv2;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONException;

class DoLogin$RequestTask extends AsyncTask {
   final DoLogin this$0;

   DoLogin$RequestTask(DoLogin var1) {
      this.this$0 = var1;
   }

   private String convertStreamToString(InputStream var1) throws IOException {
      try {
         DoLogin var2 = this.this$0;
         InputStreamReader var4 = new InputStreamReader(var1, "UTF-8");
         BufferedReader var3 = new BufferedReader(var4);
         var2.reader = var3;
      } catch (UnsupportedEncodingException var5) {
         var5.printStackTrace();
      }

      StringBuilder var6 = new StringBuilder();

      while(true) {
         String var7 = this.this$0.reader.readLine();
         if (var7 == null) {
            var1.close();
            return var6.toString();
         }

         var6.append(var7 + "\n");
      }
   }

   private void saveCreds(String var1, String var2) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      SharedPreferences.Editor var3 = this.this$0.getSharedPreferences("mySharedPreferences", 0).edit();
      this.this$0.rememberme_username = var1;
      this.this$0.rememberme_password = var2;
      var2 = new String(Base64.encodeToString(this.this$0.rememberme_username.getBytes(), 4));
      CryptoClass var4 = new CryptoClass();
      this.this$0.superSecurePassword = var4.aesEncryptedString(this.this$0.rememberme_password);
      var3.putString("EncryptedUsername", var2);
      var3.putString("superSecurePassword", this.this$0.superSecurePassword);
      var3.commit();
   }

   private void trackUserLogins() {
      this.this$0.runOnUiThread(new DoLogin.RequestTask.1(this));
   }

   protected String doInBackground(String... var1) {
      Object var10;
      try {
         this.postData(var1[0]);
         return null;
      } catch (InvalidKeyException var2) {
         var10 = var2;
      } catch (NoSuchAlgorithmException var3) {
         var10 = var3;
      } catch (NoSuchPaddingException var4) {
         var10 = var4;
      } catch (InvalidAlgorithmParameterException var5) {
         var10 = var5;
      } catch (IllegalBlockSizeException var6) {
         var10 = var6;
      } catch (BadPaddingException var7) {
         var10 = var7;
      } catch (IOException var8) {
         var10 = var8;
      } catch (JSONException var9) {
         var10 = var9;
      }

      ((Exception)var10).printStackTrace();
      return null;
   }

   protected void onPostExecute(Double var1) {
   }

   protected void onProgressUpdate(Integer... var1) {
   }

   public void postData(String var1) throws ClientProtocolException, IOException, JSONException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      DefaultHttpClient var2 = new DefaultHttpClient();
      HttpPost var4 = new HttpPost(this.this$0.protocol + this.this$0.serverip + ":" + this.this$0.serverport + "/login");
      HttpPost var3 = new HttpPost(this.this$0.protocol + this.this$0.serverip + ":" + this.this$0.serverport + "/devlogin");
      ArrayList var5 = new ArrayList(2);
      var5.add(new BasicNameValuePair("username", this.this$0.username));
      var5.add(new BasicNameValuePair("password", this.this$0.password));
      HttpResponse var6;
      if (this.this$0.username.equals("devadmin")) {
         var3.setEntity(new UrlEncodedFormEntity(var5));
         var6 = var2.execute(var3);
      } else {
         var4.setEntity(new UrlEncodedFormEntity(var5));
         var6 = var2.execute(var4);
      }

      InputStream var7 = var6.getEntity().getContent();
      this.this$0.result = this.convertStreamToString(var7);
      this.this$0.result = this.this$0.result.replace("\n", "");
      if (this.this$0.result != null) {
         Intent var8;
         if (this.this$0.result.indexOf("Correct Credentials") != -1) {
            Log.d("Successful Login:", ", account=" + this.this$0.username + ":" + this.this$0.password);
            this.saveCreds(this.this$0.username, this.this$0.password);
            this.trackUserLogins();
            var8 = new Intent(this.this$0.getApplicationContext(), PostLogin.class);
            var8.putExtra("uname", this.this$0.username);
            this.this$0.startActivity(var8);
         } else {
            var8 = new Intent(this.this$0.getApplicationContext(), WrongLogin.class);
            this.this$0.startActivity(var8);
         }
      }

   }
}

여기서 savecreds 함수가 존재합니다. 이부분만 봐보겠습니다.

private void saveCreds(String var1, String var2) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      SharedPreferences.Editor var3 = this.this$0.getSharedPreferences("mySharedPreferences", 0).edit();
      this.this$0.rememberme_username = var1;
      this.this$0.rememberme_password = var2;
      var2 = new String(Base64.encodeToString(this.this$0.rememberme_username.getBytes(), 4));
      CryptoClass var4 = new CryptoClass();
      this.this$0.superSecurePassword = var4.aesEncryptedString(this.this$0.rememberme_password);
      var3.putString("EncryptedUsername", var2);
      var3.putString("superSecurePassword", this.this$0.superSecurePassword);
      var3.commit();
   }

username은 base64로 인코딩하고, password는 aesEncryptedString 함수로 aes 인코딩을 하는 것을 볼 수 있습니다. base64는 쉽게 디코딩이 가능합니다. aes의 경우 key값을 알아야 디코딩이 가능하기때문에 key값을 찾아보겠습니다.

CryptoClass.class 파일을 보겠습니다.

package com.android.insecurebankv2;

import android.util.Base64;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class CryptoClass {
   String base64Text;
   byte[] cipherData;
   String cipherText;
   byte[] ivBytes = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
   String key = "This is the super secret key 123";
   String plainText;

   public static byte[] aes256decrypt(byte[] var0, byte[] var1, byte[] var2) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      IvParameterSpec var4 = new IvParameterSpec(var0);
      SecretKeySpec var5 = new SecretKeySpec(var1, "AES");
      Cipher var3 = Cipher.getInstance("AES/CBC/PKCS5Padding");
      var3.init(2, var5, var4);
      return var3.doFinal(var2);
   }

   public static byte[] aes256encrypt(byte[] var0, byte[] var1, byte[] var2) throws UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      IvParameterSpec var4 = new IvParameterSpec(var0);
      SecretKeySpec var5 = new SecretKeySpec(var1, "AES");
      Cipher var3 = Cipher.getInstance("AES/CBC/PKCS5Padding");
      var3.init(1, var5, var4);
      return var3.doFinal(var2);
   }

   public String aesDeccryptedString(String var1) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      byte[] var2 = this.key.getBytes("UTF-8");
      this.cipherData = aes256decrypt(this.ivBytes, var2, Base64.decode(var1.getBytes("UTF-8"), 0));
      this.plainText = new String(this.cipherData, "UTF-8");
      return this.plainText;
   }

   public String aesEncryptedString(String var1) throws UnsupportedEncodingException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {
      byte[] var2 = this.key.getBytes("UTF-8");
      this.plainText = var1;
      this.cipherData = aes256encrypt(this.ivBytes, var2, this.plainText.getBytes("UTF-8"));
      this.cipherText = Base64.encodeToString(this.cipherData, 0);
      return this.cipherText;
   }
}

aesEncrypedString을 보면 this.key 객체로 키값을 불러오는 것을 확인할 수 있습니다. key값은 해당 클래스의 멤버 변수이며 위쪽에보면 값을 알 수 있습니다.

 String key = "This is the super secret key 123";

이것을 토대로 디코딩해보겠습니다.

http://yunyun28.cafe24.com/enc/encViewMain.do#

 

XY 염색체의 BIOS

Base64 Decryption Decryption Text Decryption Result

yunyun28.cafe24.com

위 사이트에 접속후 디코딩을 해보면

잘 되는 것을 볼 수 있습니다. base64로 username도 디코딩 해보겠습니다.

이렇듯 암호화를 잘못 적용하거나 키값을 보호하지못하면 암호화 이슈가 발생할 수 있습니다.

 

- 취약점 대응 방안

1. username의 암호화방식을 base64에서 AES256으로 변경합니다.

2. 키를 관리하는 서버를 별도로 두고 키를 주기적으로 변경하는 방법으로 키값으로 보호합니다.

3. 키가 평문으로 저장되어있으면 안되기때문에 소스코드 난독화화 바이너리 무결성을 검증해야합니다.

 

Comments