안드로이드 앱 디컴파일 해보기

의도한건 아닌데 하다보니 졸지에 안드로이드 앱을 디컴파일까지 하게됐다. 오늘 한일을 정리해보자.

1. 맥에 안드로이드 SDK 설치하기

일단 안드로이드 SDK부터 설치하자. 전에는 직접 다운로드 받아서 ~/dev 폴더에 넣고 관리했었는데, HomeBrew에도 android-sdk 모듈이 있었다!. 난 홈브루 매니아니까 홈브루로 고고!!

$> brew install android-sdk
$> brew info android-sdk
android-sdk: stable 24.3.3
Android API libraries and developer tools
https://developer.android.com/index.html
... 
You may need to add the following to your .bashrc:
export ANDROID_HOME=/usr/local/opt/android-sdk
...

설치된 정보를 보니 24.3.3 버전이다. 그리고 ~/.profile 파일을 열어서 아래와 같이 ANDROID_HOME과 필요한 PATH를 잡아준다.

// .profile 
export ANDROID_HOME=/usr/local/opt/android-sdk
export PATH=$PATH:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools 

2. 자바 최신 버전 설치하기

다음으로 해야할 일은 자바 최신버전을 까는 일이다. 아마 맥에 자바를 따로 설치하지 않았다면 옛날버전일 것이다.

$> java -version
java version "1.6.0_65"

역시나 오랜된 1.6 버전이다. 혹시나해서 홈브루엔 자바가 없나? 했는데 역시나 없다. 그냥 오라클 사이트가서 다운로드 받아 설치한다. 설치가 끝났다면 확인해보자.

$> java -version
java version "1.8.0_60"

올레~!! 됐네 됐어!! 그나저나 난 왜 자바 최신버전까지 깐거지? 그건 뒤 이어 설치할 디컴파일 툴들이 자바 1.7이상에서 돌기 때문이다. 이왕 설치한김에 최신버전~ ㅎㅎ 필요에 따라서는 JAVA_HOME을 설정해야될지도 모르겠다. Mac에서 자바홈은 아래 위치에 있다고 하는데 맥에선 패키지로 인스톨하다보니 JAVA_HOME을 딱히 안해도 되더라~ 오히려 했다가 패스가 안잡혀서 오류가 났었다.

export JAVA_HOME=/usr/libexec/java_home

실제 맥에 JDK가 설치되는 위치는 아래와 같다. 자세한 내용은 문서를 참고하자.

/Library/Java/JavaVirtualMachines/jdk<major>.<minor>.<macro[_update]>.jdk

3. 디컴파일 툴 설치하기

디컴파일 툴은 2가지가 있다. 참고링크: http://visu4l.tistory.com/412

  • dex2jar : https://code.google.com/p/dex2jar/
  • JD-GUI : http://jd.benow.ca/

dex2jar 파일은 apk 파일을 디컴파일해서 jar 파일로 변환해준다. 그리고 JD-GUI는 앞에서 변환한 jar 파일을 열어서 실제 디컴파일된 소스를 볼때 유용하다.

4. 폰에 설치된 앱 가져오기

필요한 도구는 모두 갖춰놨으니 이제 실제 폰에 있는 안드로이드 앱을 가져다가 디컴파일해보자! 일단 폰에 있는 apk 파일을 가져오려면 폰을 USB로 맥에 연결하고 개발자 옵션을 켜야한다. 일단 여기까지 했으면 폰이 제대로 PC에 연결됐는지 확인해보자. 이때 adb 라는 디버깅 도구를 이용한다.

$> adb devices
List of devices attached
d22849b7    device

그런데 adb가 없을수도 있다. 왜냐면 디버깅 도구는 기본 SDK에 포함되어 있지 않고 별도로 설치해야한다. 뭐 간단히 명령어 한줄이면 된다.

$> android update sdk --no-ui --filter 'platform-tools'

여튼 adb 명령어는 매우 유용한다. 연결된 폰이 하나라면 바로 쉡(shell)로 접속할수도 있다.

$> adb shell
shell@ks01lteskt:/ $ ls -al
drwxr-xr-x root     root              1970-02-06 11:46 .system
drwxr-xr-x root     root              1970-02-06 11:46 acct
drwxrwx--- system   cache             2015-09-01 20:29 cache
dr-x------ root     root              1970-02-06 11:46 config
lrwxrwxrwx root     root              1970-02-06 11:46 d -> /sys/kernel/debug
drwxrwx--x system   system            1970-02-06 11:46 data
-rw-r--r-- root     root          255 1970-01-01 09:00 default.prop
drwxr-xr-x root     root              2015-07-29 16:34 dev
...
..

오호라! 굉장한데? 하면서 막 폰속을 서핑하지만 정작 중요한 순간에는 퍼미션이 없다고 나온다. 특히 설치된 앱들이 모여있는 /data/app 폴더는 볼수가 없다.

shell@ks01lteskt:/ $ ls -al /data/app
opendir failed, Permission denied

아… 어쩌지?… 어쩌지… 라고 생각하면서 또 잽싸게 써핑해보니, 역시나 있다 있어! 방법이 다 있어!!

설치된 팩키지 목록

일단 설치된 폴더의 파일 목록을 볼수가 없으니 꼼수를 이용해야한다. 먼저 설치된 패키지가 뭐가 있는지 검색해보자.

$> adb shell pm list packages
adb shell pm list packages
...
package:com.sec.android.app.mediasync
package:com.sec.android.app.sbrowsertry
package:com.sec.android.app.controlpanel
package:com.android.printspooler
package:com.google.android.syncadapters.calendar
... 
..

그런데 오지게 많이 나온다. 일일이 확인하기 어려우니까 적당히 필터하면서 원하는 앱의 패키지 명을 찾는다.

$> adb shell pm list packages | grep <필터할 글자>

설치된 패키지의 실제 경로 확인

그리고 원하는 앱의 패키지명을 찾았다면 그 앱의 실제 경로를 확인해본다. 예를 들어 폰에 멜론앱이 설치되어 있다면 아래와 같이 실제 경로를 확인할수있다.

$> adb shell pm path com.iloen.melon
package:/data/app/com.iloen.melon-7.apk

앱에 있는 apk 파일 다운로드

이제 PC로 다운로드하자!!

$> adb pull /data/app/com.iloen.melon-7.apk

이렇게 추출해낸 apk 파일은 zip으로 압축된 녀석이라 그냥 확장자만 바꿔서 압축을 풀어도 내용을 볼수있지만 실제 소스코드는 컴파일되어 있기 때문에 보기 어렵다. 이제 이 .apk 파일을 디컴파일해보자. 필요한 도구는 앞에서 다 설치해놨다.

5. 소스코드 디컴파일

디컴파일 방법은 또 쓰기 귀찮으니까 링크를 참고하자! http://visu4l.tistory.com/412

다른점이 있다면 도구들이 버전업이 되서 이름이 바뀐것 밖에 없다. 맥에서 할라면 당연히 .bat 파일이 아니라 .sh 파일을 실행시켜야된다. 뭐 이런식이다.

$> sh d2j-dex2jar.sh com.iloen.melon-7.apk

자바 버전이 1.7이상이라면 특별한 문제없이 잘 동작한다. 결과물은 파일명 뒤에 -dex2jar.jar 라는 접미사가 붙는다.

마지막으로 JD-GUI를 실행시켜서 위 파일을 열어서 확인한다. 대부분의 소스코드가 디컴파일되서 java 파일을 볼수있다. 그런데 안드로이드는 java 파일만있다고 해석할수있는게 아니다.
AndroidManifest.xml 파일도 같이 봐야한다.

6. XML 디컴파일

xml 파일을 디컴파일 하려면 apktool을 설치해야한다. 설치는 알아서들.. 점점 귀찮아진다.

http://ibotpeaches.github.io/Apktool/install/

사용법은 이렇다.

$> apktool d com.iloen.melon-7.apk

덧,

사실은 이 모든 것들이 Appium 프로젝트를 만들다가 벌어진일이다.
젠장알~~ -_-;.. 뭐한거냐?
아직 시작도 못했다. ㅎㅎ

썸머노트에서 이미지 업로드 할때 흔히 발견되는 nodejs 에러들

썸머노트에서 이미지 업로드하기

썸머노트에서 이미지를 서버에 올리려면 일단 아래와 같이 별도의 onImageUpload 핸들러를 구현해야한다.

var $editor = $('#editor');
var editor = $editor.summernote({
 ... 옵션 생략 ... 
 onImageUpload: function(files) {
    sendFile(files[0]);
  }
});

위에서 사용한 sendFile 함수는 아래와 같이 구현한다. 이때 ajax 옵션에 주의하자. 별거아닌 옵션을 실수로 잘못주면 하루종일 이유도 모르고 헤맬수있다. 특히 contentType은 없어야한다. 파일 업로드라고 종종 contentType을 “multipart/form-data”로 설정하는 경우가 있는데, 본래 XMLHttpRequest 객체는 파일 업로드를 지원하지 않기 때문에 파일 업로드를 위해서는 XHR2와 FormData 객체를 이용해야한다. 참고로 XHR2는 거의 모든 최신 브라우저에서 사용할수있다.

function sendFile(file) {
  var data = new FormData();
  data.append("file", file);

  $.ajax({
    type: "POST",
    url: "/api/upload/image",
    type: 'POST',
    data: data,
    contentType: false,
    processData: false, // Don't process the files
    success: function(data) {
      $('#editor').summernote("insertImage", data.url);
    }
  });
}

간혹 no multipart boundary was found 라는 오류가 발생하면 바로 컨텐츠 타입을 의심해보면 된다.

업로드 서버 구현하기

이제 파일을 업로드를 해줄 서버를 구현해보자. 노드로 구현할때 나는 주로 쓰는 multer라는 모듈을 사용한다. 사용법 또한 무지 간단하다.

var router = express.Router();
var multer = require('multer');
var upload = multer({ 
  dest: 'uploads/'
});
router.post('/upload/image', upload.single('file'), function(req, res){
  console.log(req.file);
});

하지만 간단하다고 생각할때 아래와 같은 에러를 만나게 된다.

위 두 에러는 ajax로 파일을 업로드할때 파일내용을 request body에 써버 보내게 되는데 바로 그 바디의 크기 제한에 걸려나 나는 문제다. 이럴땐 당황하지 말고 아래와 같이 설정한다.

app.use(bodyParser.urlencoded({limit: '5mb', extended: false, parameterLimit: 10000}));

물론 node 앞딴에 Nginx 같은 스태틱 서버를 두는 경우엔 nginx 설정에도 업로드 사이즈를 설정해야한다.
관련글은 검색하면 쉽게 찾을수있다.

http {
#...
    client_max_body_size 100m;
#...
}

nginx 와 socket.io 연동

nodejs를 이용해 socket.io를 사용하는 경우 보통 포트번호가 다음과 같이 들어가기 마련이다.

somewhere.com:9000

하지만 포트 번호가 노출되는것이 영~ 깨림찍한 경우엔 앞딴에 nginx 나 apache 를 두고 뒷딴의 9000포트로 프록시를 하는 것이 보통이다. 개인적으로는 아파치도 좋치만 nginx를 더 자주쓰고 있다. nginx 를 쓴다면 아래와 같이 설정을 추가한다.

server {
    listen 80;

    root /app/node-server/dist/public;
    index index.html index.htm;

    # Make site accessible from http://localhost/
    server_name somewhere.com;

    location / {

      // A: Proxy 패스를 설정하고 싶은 경우, 
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header X-NginX-Proxy true;

      proxy_pass http://somewhere.com:9000/;
      proxy_redirect off;

      // B: socket.io 사용시 아래설정 필수!!
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection "upgrade";
      proxy_http_version 1.1;

    }
    ... 이후 생략 ...
}

특히 socket.io를 이용하는 경우 B 설정은 필수다. B설정이 뭘해주는지는 솔직히 귀찮아서 찾아보진 않았다. 하지만 저 설정을 하지 않으면 ws:// 프로토콜로 웹소켓과 통신할때 핸드쉐이킹이 제대로 안되서 에러가 난다.