본 포스팅에서는 dalvik 바이트코드를 메모리에서 변조하는 방법을 다룹니다.


필자의 테스트 환경

- galaxy S5 

- Android 4.4.2

- Snapdragon 805 (32bit) 


"안드로이드 후킹, 메모리 변조 : frida framework를 활용한 dex 함수 후킹" 포스트에서 사용한 apk를 그대로 사용합니다.

우선, ida로 classes.dex를 연 다음 check_file 함수를 찾습니다. su 파일 경로를 인자로 전달받아, 파일이 존재하면 true를 리턴하는 함수입니다.

이 함수를 무조건 false를 리턴하게 하면 되겠지요.

현재 바이트 파싱이 big endian으로 되어있기 때문에 바이트를 뒤집어서 보셔야 합니다.

각 바이트들이 무엇을 의미하는지는 http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html 에 자세하게 설명되어 있습니다.

함수의 마지막 줄을 보면 v0을 리턴하게 됩니다. 참고로 0이 true이고 1이 false입니다.

v0이 0또는 1로 설정되어 리턴하는 것을 보실 수 있습니다.

첫 줄을 보면, hex 0x12는 4비트를 변수에 설정하는 opcode입니다.

보통 0x12 0xXY 형태로 사용합니다. 

X는 설정할 값이고, Y는 변수입니다.

Y=0이면 v0, Y=1이면 v1, Y=2이면 v2 이런 식입니다.

우리는 v0 값을 false로 설정할 것이므로, 0x12 0x10 으로 메모리를 변조하면 됩니다.

0x12 0x10 수행 후, 0x00 0x0F 를 통해 리턴을 하게 하면 무조건 false를 리턴하게 되겠지요.


제, classes.dex 메모리를 ptrace를 통해 변조하면 됩니다.

classes.dex 파일에서는 check_file 함수의 offset이 0x132C60이었는데, 메모리에 매핑될 때는 앞에 부가적인 데이터들이 삽입됩니다. (0x28 바이트 고정)

이 데이터들을 건너뛰면, 해당 함수의 offset은 "dex" signature offset으로부터 0x132C60에 위치하게 됩니다. 

아래 그림은 0x76958000 를 덤프한 그림입니다. (메모리 덤프 방법은 http://codetronik.tistory.com/118 참조하세요.

아래 코드와 같이 dex 메모리를 변조할 수 있습니다.

ptrace에서 메모리를 read/write할 때, 사이즈 단위가 long이므로 (테스트한 단말기 환경에서는 4바이트), 아래의 PtraceWrite와 같은 패치 코드를 나눠서 write할 수 있는 함수를 작성해야 합니다.


정상적으로 패치가 되면 아래 화면처럼 null이 나타납니다.



본 포스팅에서는 FRIDA 프레임워크를 통한 후킹 방법을 설명합니다.

FRIDA를 설치하기 위해선 반드시 ROOT 권한이 필요합니다. 각자 환경에 맞는 루팅을 진행해주시면 됩니다.

 

필자의 테스트 환경

- galaxy S5 

- Android 4.4.2

- Snapdragon 805 (32bit) 

 

설치 준비물은 아래와 같습니다.

- Python 3.7

- frida-server-xx.y.zz-android-arm.xz (https://github.com/frida/frida/releases 에서 각자 환경에 맞는 최신 자료를 다운로드)

(갤럭시탭A 2017은 14.2.13버전 적합)

 

1. 단말기에 설치

adb shell 접속 후, /system의 read-only 권한을 쓰기 권한으로 변경하여야 합니다.

모든 권한은 항상 ROOT입니다.

 

mount -o rw,remount /system 

(에러 발생 시 mount -o rw,remount / )

/system/priv-app 에 xz파일 압축 해제

mount -o ro,remount /system

 

단말기를 재부팅한 후에 서버를 구동합니다.

/system/priv-app/frida-server &

(remote 시 /frida-server -l(엘) 아이피 &

 

2. 윈도우에 설치

파이선 설치는 가능하면 d:\python와 같은 짧은 경로로 설치합니다.

설치가 끝나면 아래 명령어를 실행합니다.

c:\>pip install frida

c:\> pip install frida-tools // frida.exe를 설치해주며(필수), 최신버전 frida도 함께 설치해줌

(특징 버전 설치: pip install frida==14.2.13)

설치가 정상적으로 되었다면 frida-ps -U 로 안드로이드 내의 프로세스가 보이는지 확인합니다.

(remote 시 frida-ps -H 아이피)

 

이제 모든 준비가 끝났습니다. 이제부터 실전 예제를 통해 후킹 방법을 설명합니다.

제가 준비한 예제는 다음과 같습니다. apk를 단말기에 설치합니다.

 

- 루팅 감지

소스 : https://github.com/codetronik/RootChecker

codetronik.rootchecker.apk
다운로드

 

 


 

후킹에 앞서, dex 디컴파일러로 dex의 구조를 파악해봅니다.

무료 디컴파일러 중에선 jadx가 유용합니다.

 

jadx로 열어보면 위와 같은 소스코드가 보입니다. frida로 하나씩 후킹해보도록 하겠습니다.

후킹 소스의 기본 틀은 아래와 같습니다.

 
 
후킹 코드들은 Java.perform(function(){ 후킹 코드 }); 와 같은 형태로 작성하면 됩니다.

execute_su() 함수는 su가 실행이 되면 true를 리턴합니다. 아래 코드로 우회할 수 있습니다.

단말기에서 앱을 실행 후, 후킹 소스를 실행하여 실행결과를 비교해보면 화면의 check execute su : YES에서 NO로 바뀐 것을 확인해볼 수 있습니다.

check_su_files() 함수는 su 파일이 존재하면 해당 경로를 리턴하고, 없으면 null을 리턴합니다. 마찬가지로 return null을 하면 간단히 우회할 수 있으나 임의의 경로를 인자로 넘기는 방법도 있습니다.

후킹할 함수에 인자가 있는 경우, 적절하게 overload를 해주시면 됩니다.  

 Byte[]   [B
 String  java.lang.String
 int  int
   

  

 

팁: 소스에서 Java.perform(function( ~~ ){}); 이외는 삭제하고, 아래와 같이 실행하면 frida가 앱을 실행하면서 후킹을 합니다.

앱 띄우면서 중단없이 후킹

frida -H 아이피 -f 앱이름 -l 소스파일명 (원격)

frida -U -f 앱이름 -l 소스파일명 (usb)
 

있던 프로세스에 attach

frida -H 아이피 앱이름 -l 소스파일명 

android 11에서 spawn시 Error: VM::AttachCurrentThread failed: -1가 발생할 경우 쉘에서 아래 커맨드를 입력

setprop persist.device_config.runtime_native.usap_pool_enabled false

커널 4.6에서 커널 4.5까지 존재했던 crypto_hash 관련 함수가 사라졌습니다.

참조(사라진 함수..) : http://elixir.free-electrons.com/linux/v4.6-rc1/ident/crypto_alloc_hash

계속 사용하고 싶다면 비동기(crypto_ahash), 동기(crypto_shash) 중에 선택하여 사용하여야 합니다.

본 예제에서는 동기를 다룹니다. (kernel 4.10.0-38 에서 테스트 되었습니다.)



커널같은 대규모 프로젝트에서, 같은 변수나 함수명을 사용하는 경우가 많은데 이 경우 전역선언 시  static으로 선언하지 않으면 컴파일러는 같은 이름 중 어떤걸 사용해야 할 지 몰라 에러를 뱉어냅니다.

오류 LNK1169 여러 번 정의된 기호가 있습니다. ConsoleApplication15 d:\ConsoleApplication15\Debug\ConsoleApplication15.exe 1

오류 LNK2005 "int a" (?a@@3HA)이(가) ConsoleApplication15.obj에 이미 정의되어 있습니다. ConsoleApplication15 d:\ConsoleApplication15\ConsoleApplication15\j.obj 1

이를 해결하려면 변수나 함수 앞에 static 을 붙이면 됩니다. 그럼 해당 소스 내에서만 사용이 가능해집니다.


함수 내에서 static 변수를 사용할 경우

출력결과

1

2

3

1

1

1




ring 3에서 작성된 fopen, ptrace 등은 내부적으로 libc.so->syscall 방식으로 호출되는데, 직접 호출하면 libc.so를 거치지 않습니다. (후킹 등에 안전)

플랫폼 등에 따라 사용되는 레지스터가 달라질수 있으므로 gdb, ida 같은 툴에서 libc.so 열어 확인해야 합니다.

해당 플랫폼은 i386인데 ebx, ecx, edx, esi 순서대로 들어가고 있네요. 다른 머신에서의 libc도 확인해봤는데 레지스터 순서가 같은 것으로 보아 고정일 가능성이 큽니다. (아닐 수도 있으니 댓글 달아주시면 감사..추후 수정 사항이 생기면 수정하겠습니다.)

upx의 syscall 매크로를 보면, 거의 저 순서대로 들어가는 것을 확인할 수 있습니다.

https://github.com/upx/upx/blob/master/src/stub/src/include/linux.h 



https://sourceforge.net/p/predef/wiki/Architectures/


gcc에서는 이정도면 될듯하다.

intel계열

#if defined(__i386__)

#if defined(__x86_64__)

arm 계열

32bit

#if defined(__thumb__)

#if defined(__arm__)

64bit

#if defined (__aarch64__)


ida에서 __usercall로 해석되는 함수의 경우는 2~3개 정도의 인자를 불특정 레지스터에 저장하고 사용하기 때문에
C에서 제공하는 calling convention으로 함수를 정의할 수 없었습니다.
(있다면 댓글로 자비좀 베풀어주세요.. )

어쩔 수 없이 naked형태로 후킹 함수를 구현하였습니다.


후킹 대상인 __usercall 함수는 4개의 인자를 사용하고 있습니다.
int __usercall 대상함수(int a1@<edx>, int a2@<esi>, int a3, int a4)
호출 시 레지스터는 유지하고, a3, a4만 push합니다.
호출이 끝나면 push한 만큼 esp를 복원해주고(hook함수 종료 시 mov esp, ebp를 해주니 복원할 필요가 없을듯하나 습관적으로 해주는것이 좋을듯) , 리턴값이 담긴 eax를 백업합니다.
esi와 edx는 __usercall 함수 내부에서 retn전에 pop을 해주기 때문에 따로 백업하지 않아도 됩니다.

이후에 각종 파라미터를 빼내는 함수를 호출하는데, __stdcall로 선언했으므로 따로 스택정리는 하지 않았습니다. 

 pTrampoline는 jmp 다음 명령어를 가리키도록 해주면 됩니다.



다른 컴파일러로 개발할 일 생기면 추가 예정


 

 Visual C

 

GCC 

 

 Bit

32 

64 

32 

64 

 INT 

 4

 LONG

 4

 FLOAT

 4

8

 DOUBLE

 4

8

8

8


멀티 소스로 개발시 LONG형이 문제가 될 수 있다. 특히 long 으로 리턴받는 api사용 시 주의.


8바이트를 쓰고자 한다면, int64_t / __int64_t 로 선언하면 어느 쪽에서도 확실하게 8바이트를 사용할 수 있다.

ScanBase64Function.dll


메모리에서 base64의 인코딩 및 디코딩 테이블을 검색한 후, 해당 테이블 주소를 text섹션에서 참조하는 코드 주소를 찾아냅니다.

char MimeBase64[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' };   char(혹은 int) DecodeMimeBase64[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, -1, 0, 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,-1,-1,-1,-1,-1, -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 };  


사용 방법 : 플러그인 메뉴에서 Scan을 클릭하면, 검색 결과를 로그에 출력합니다.

속도가 매우 느립니다. (-_-)


실행 결과 예시: 

jscript.dll의 0x3F1E8630에서 base64 테이블을 검색하였습니다. encode용도일 가능성이 있습니다.

해당 주소를 6개의 코드에서 참조하고 있습니다.


개선해야할 내용

- 테이블을 사용하지 않는 BASE64 함수 검색

- encode / decode 용도 정확하게 구분하기

- 패커 (더미다 등) 적용시 확인해보지 못하였음




Windows 10 기준으로,

C:\Program Files\NPKI\yessign\USER 디렉토리를 보면 공인인증서가 있습니다.

signCert.crt / signPri.key

signPri.key에서 개인키를 추출하고 RSA구조체로 변환할 것입니다.


1. SEED 대칭키 / IV 구하기

1) 개인키 파일에서 salt와 iteration 값을 추출합니다. PKCS#8 규격에 맞춰서 추출합니다.

# 8바이트 salt 추출하기 (20번째 바이트에서 8바이트 추출)

BYTE bySalt[8] = { 0, };

memcpy_s(bySalt, 8, (BYTE*)byFileBuffer + 20, 8);

# iteration 값 추출하기 (30번째 바이트에서 2바이트 추출)

WORD wIteration = _byteswap_ushort(*((WORD*)byFileBuffer + 15));

빅 엔디언을 리틀 엔디언으로 변경해줍니다.


2) KEY와 IV만들기

공인인증서 개인키는 SEED-CBC-128로 암호화되어 있습니다.

복호화할 KEY와 IV를 PBKDF1규격에 따라 만듭니다.

공인인증서 패스워드에 salt를 붙인다음 iteration 값 만큼 SHA1하며, 최종 값에서 첫 16바이트를 KEY로 사용합니다.


최종 HASH값에서 뒤 4바이트를 잘라낸 뒤 SHA1하여 DIV를 만듭니다. DIV에서 앞 16바이트를 잘라서 IV로 사용합니다.


2. SEED로 개인키 복호화

1에서 구한 키와 IV로 SEED-CBC-128 복호화하면 ASN.1 DER형태의 바이너리가 나옵니다.

복호화 할 오프셋은 파일의 36번째부터 끝까지입니다.


3. RSA 구조체로 변경하기

ASN.1을 PKCS8 구조체로 변환 후 RSA구조체로 변환합니다.

4. 개인키로 평문 암호화 


+ Recent posts