1. 개요#
HTTP 패킷 관찰 시 E2E 암호화되는 경우는 빈번히 확인할 수 있다. 이번 경우는 안드로이드 앱에서 HTTP E2E 암호화 시, 이를 어떻게 복호화하는지에 대해 다룬다.

2. 안드로이드 E2E 암호화 코드 예시#
Javax.crypto.Cipher은 데이터 암, 복호화를 위한 자바 클래스다. 암호화 과정 내용은 우리에게 중요하지 않으며, 핵심 부분은 DoFinal 메서드다. 모든 암호화 되기 직전 플레인 텍스트는 DoFinal을 거쳐 암호화된다. 즉, DoFinal 메서드를 후킹해 해당 메서드에 삽입되는 모든 데이터를 가져올 시 평문 데이터 확인이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| javax.crypto.Cipher 클래스를 이용한 암, 복호화 유틸리티 코드 예시
package com.example.testapp;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtils {
private static final String TRANSFORMATION = "AES/CBC/PKCS7Padding";
private static final String ALGORITHM = "AES";
/**
* Encrypts plaintext bytes using AES/CBC/PKCS7Padding.
*/
public static byte[] encrypt(byte[] plaintext, byte[] keyBytes, byte[] ivBytes) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(plaintext); // 사전 협상한 암호화 스펙 조건으로 플레인 텍스트를 암호화함.
}
/**
* Decrypts ciphertext bytes using AES/CBC/PKCS7Padding.
*/
public static byte[] decrypt(byte[] ciphertext, byte[] keyBytes, byte[] ivBytes) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, ALGORITHM);
IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(ciphertext);
}
}
|
3. 프리다를 이용한 E2E 암호화 복호화#
Dofinal
인자 값을 보면 요구 데이터 타입이 Byte 다. 이를 우리가 볼 수 있는 값으로 바꾸기 위해선 Byte -> Hex -> ASCII 로 변경하는 과정이 필요하다. 이를 해주는 메서드가 java.lang.String의 StringClass 메서드다.
정리하면, Dofinal 메서드 후킹 후 인자 값을 가져온 다음 이 인자 값을 StringClass로 출력 시 송신 시 암호화될 평문 데이터를 얻을 수 있고, Dofinal 메서드 리턴 값을 가져오면 수신 시 암호화된 데이터가 복호화된 상태로 출력된다.
해당 코드는 단순히 doFinal을 호출하는 모든 데이터를 가져온다.
1
2
3
4
5
6
7
8
9
| Java.perform(function () {
var Cipher = Java.use('javax.crypto.Cipher');
var StringClass = Java.use('java.lang.String');
Cipher.doFinal.overload('[B').implementation = function (data) {
console.log("[+] doFinal Input:", StringClass.$new(data));
return this.doFinal(data);
};
});
|
스크립트 실행 시 암호화가 된 부분과 되지 않은 부분이 보인다. 그 이유는 복호화하는 데이터도 암호화 과정과 동일하게 Dofinal 메서드를 사용하기 때문이다. 복호화된 데이터는 Dofinal 메서드 리턴 값이 평문이기에 이를 가져와야 한다. 당연히 암호화가 될 데이터 값은 dofinal 인자값이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| Java.perform(function () {
var Cipher = Java.use('javax.crypto.Cipher');
var StringClass = Java.use('java.lang.String');
Cipher.doFinal.overload('[B').implementation = function (data) {
var mode = this.opmode.value; // 1: ENCRYPT, 2: DECRYPT
var ret = this.doFinal(data);
if (mode === 1) {
console.log("[+] [ENCRYPT - Plaintext Input]:", StringClass.$new(data));
} else if (mode === 2) {
console.log("[+] [DECRYPT - Plaintext Output]:", StringClass.$new(ret));
}
return ret;
};
});
|
수정한 스크립트 적용 시 암호화, 복호화된 데이터 모두 정상 출력된다.

4. 결론#
안드로이드 암호화 클래스는 대표적으로 javax.crypto.cipher 가 있다. 이는 DoFinal 메서드로 데이터 암호화, 복호화를 수행하며 해당 메서드 후킹 시 복호화된 데이터 수집이 가능하다.
다만, 모든 애플리케이션 암호화 로직이 javax.crypto.cipher 클래스를 사용하는 건 아니다. 커스텀된 암호화 로직을 이용할 수도 있다. 이 경우 정밀한 리버싱 과정이 필요하지만, 대부분의 경우 데이터 암, 복호화 하는 메서드 하나만 후킹하면 되니 상황에 맞춰 분석 후 후킹하면 된다.