Frida Process, Thread, Memory, Module

DOUIK·2022년 7월 27일
0

Android

목록 보기
3/7

Int64 / Uint64

Int64와 Uint64는 인자는 전달한 값을 숫자로 변환하는 객체
new Int64(v) 또는 int64, uint64 형태로 사용, 해당 객체의 인자로 문자열, 16진수 형태의 값이 전달되면 10진수로 변환해 연산을 수행할 수 있음. 특정 함수를 후킹하거나 포인터 주소에 대한 연산이 필요할 때 주로 사용

여기서 주의할 것은 해당 객체는 숫자를 전달할 경우 오브젝트 형태로 변환하며, 일반적인 숫자와 타입이 다르다는 것을 명심해야 한다.

함수

add(rhs), sub(rhs), and(rhs), or(rhs), xor(rhs)

변환된 10진수 값에 대해 산술 연산을 수행합니다.

shr(n), shl(n)

변환된 10진수 값에 대해 n 비트만큼 쉬프트 연산을 수행할 수 있습니다.

compare(rhs)

인자로 전달된 값과 비교합니다. 같으면 결과값이 0, 다르면 1이 나옴

toNumber()

Int64 또는 Uint64를 사용한 정수 객체를 숫자 타입으로 변경합니다.

toString([radix = 10])

변환된 10진수의 값을 문자열 형태로 변환합니다. radix는 진수를 나타내며, 기본값은 10입니다.

Int64 예시

Int64와 Uint64는 음수 표현 가능 여부의 차이만 존재하기 때문에 예시 스크립트에서는 Int64 객체만을 사용함

Int64 객체

코드를 살펴보면 덧셈과 비교, 그리고 문자열 형태 변환을 하는 함수가 있음

  1. add 함수는 전달된 문자열 형태의 값을 Int64 객체 형태로 변환하고, 덧셈 결과와 변환된 타입을 출력합니다.
  2. compare 함수는 전달된 문자열 형태의 값을 Int64 객체 형태로 변환하고, compare 함수를 통해 두 개의 값을 비교합니다.
  3. convert 함수는 전달된 문자열 형태의 값을 Int64 객체 형태로 변환하고, toString 함수를 통해 특정 진수로 변환합니다.

Int64/Uint64 사용 예시

function add(num, num2) {
	console.log("type: " + typeof(num) + ", " + typeof(num2));
	var num_obj = int64(num);
	var num_obj2 = int64(num2);
	var sum = num_obj.add(num_obj2);
	console.log("add sum: " + sum + " type: " + typeof(sum));
}
function compare(num, num2) {
	var num_obj = int64(num);
	var num_obj2 = int64(num2);
	var result = num_obj.compare(num_obj2);
	if(!result) {
		console.log("Same.")
	}
	else {
		console.log("Different.");
	}
}
function convert(num) {
	var num_obj = int64(num);
	console.log("Decimal: " + num_obj.toString(10));
	console.log("Hexadecimal: " + num_obj.toString(16));
}
add("100", "200");
compare("100", "200");
compare("100", "100");
convert("1234");

NativePointer

인자로 전달된 문자열 값을 메모리 포인터로 변환하는 객체

"new NativePointer(s)" 또는 "ptr" 형태로 사용하며, 인자 s는 10진수 혹은 16진수 형태의 메모리 주소여야 합니다. Int64, Uint64 객체와 마찬가지로 다음과 같이 변환된 객체에 대해 연산을 수행할 수 있도록 다양한 함수를 제공합니다.

함수

isNull()

포인터가 NULL인지 검사합니다.

add(rhs), sub(rhs), and(rhs), or(rhs), xor(rhs)

포인터 값을 각 함수에 해당하는 산술 연산을 수행합니다.

compare(rhs)

전달된 인자 값과 정수 형태로 비교합니다.

toInt32()

NativePointer 타입을 signed 32-bit 정수로 캐스팅합니다.

toString([radix = 16])

포인터 값을 문자열로 변환합니다. radix는 진수를 나타내며, 기본값은 16입니다.

readPointer(), writePointer(ptr)

포인터 주소에서 NativePointer 형태로 값을 읽거나 씁니다. 이 때 포인터의 크기는 대상 프로세스의 포인터 크기로 가정합니다.

readS8(), readU8(), readS16(), readU16(), readS32(), readU32(), readShort(), readUShort(), readInt(), readUInt(), readFloat(), readDouble(), readS64(), readU64(), readLong(), readULong()

포인터 주소로부터 각 함수의 접미사를 타입으로 하여 값을 읽습니다. 예를 들어 readU16 함수는 포인터 주소로부터 부호 없는 16비트 값을 읽습니다.

writeS8(value), writeU8(value), writeS16(value), writeU16(value), writeS32(value), writeU32(value), writeShort(value), writeUShort(value), writeInt(value), writeUInt(value), writeFloat(value), writeDouble(value), writeS64(value), writeU64(value), writeLong(value), writeULong(value)

포인터 주소에 각 함수의 접미사를 타입으로 하여 값을 씁니다. 예를 들어 writeU16 함수는 포인터 주소에 부호 없는 16비트 값을 씁니다.

readByteArray(length), writeByteArray(bytes)

readByteArray 함수는 포인터 주소로부터 length 길이만큼 값을 읽고, writeByteArray 함수는 ArrayBuffer 타입의 배열을 포인터 주소에 씁니다.

readCString([size = -1]), readUtf8String([size = -1])

인자로 전달한 size 만큼 문자열을 읽어옵니다. 각 함수에 따라 ASCII, Utf-8 문자열 형태로 읽습니다. size 값이 -1일 경우, NULL 값이 나타날 때까지 읽습니다.

writeUtf8String(str), writeAnsiString(str)

인자로 전달한 문자열을 Utf-8, Ansi 문자열 형태로 포인터 주소에 씁니다. writeAnsiString 함수는 Windows 에서만 유효합니다.

Native Function

코드에서 지정한 주소를 실제 함수처럼 사용할 수 있는 객체. 인자로 함수의 주소와 반환 타입, 그리고 인자의 타입을 지정하면 해당 함수 호출 가능. 보편적으로 Frida 코드 내에서 라이브러리 함수를 직접 호출해야 하는 경우 Native Function을 사용함

new NativeFunction(address, returnType, argTypes[, abi])

지원 타입

  • void
  • pointer
  • int, uint, long, ulong
  • char, uchar
  • size_t, ssize_t
  • float, double
  • int8, uint8, int16, uint16, int32, uint32, int64, uint64
  • bool

C와 같은 타입을 지원하지만 모든 포인터 타입(int , char , void *, ...)은 pointer 타입으로 통일됨

Native Function 예시

var open = new NativeFunction(Module.getExportByName(null, "open"), "int", ["pointer", "int"]);
var write = new NativeFunction(Module.getExportByName(null, "write"), "ssize_t", ["int", "pointer", "size_t"]);
var close = new NativeFunction(Module.getExportByName(null, "close"), "int", ["int"]);

// Android
var filePath = "/data/data/com.google.android.calendar/test.txt";

// iOS
// var filePath = "/tmp/test.txt" 

var O_WRONLY = 1;
var O_CREAT = 0x40;
var fd = open(Memory.allocUtf8String(filePath), O_CREAT|O_WRONLY);
console.log("file descriptor: " + fd);
write(fd, Memory.allocUtf8String("Hello World\n"), 12);
close(fd);
console.log("done, check " + filePath);
  • open, write, close 세 개 라이브러리 함수를 NativeFunction 객체를 이용해 프리다에서 사용할 수 있는 함수로 정의
  • 정의한 함수를 기반으로 파일 생성하고 파일에 문자열을 씀
  • 안드로이드와 iOS 간 임시 디렉토리 경로가 다르기 때문에 환경에 맞는 임시 디렉토리 경로를 지정해주어야 함

정리
Int64 / Uint64 : 인자로 전달한 값을 숫자로 변환하는 객체
NativePointer: 인자로 전달된 문자열 값을 메모리 포인터로 변환하는 객체
NativeFunction: 코드에서 지정한 주소를 실제 함수처럼 사용할 수 있는 객체

Process

프로세스와 관련된 정보 조회할 수 있는 객체

  • PID, 페이지 등을 포함해 맵핑된 메모리 조회 가능

함수

Process.enumerateModules()

프로세스에 맵핑된 모든 모듈을 열거합니다.

Process.findModuleByAddress(address), Process.getModuleByAddress(address)

첫 번째 인자에 전달된 주소가 어떤 모듈에 할당된 메모리인지 알아낼 수 있습니다.

Process.findModuleByName(name), Process.getModuleByName(name)

첫 번째 인자에 전달된 모듈 이름의 주소를 가져옵니다.

Process.findRangeByAddress(address), Process.getRangeByAddress(address)

전달된 메모리의 페이지 크기와 권한을 알아낼 수 있습니다.

Process 예시

var modules = Process.enumerateModules()[0];

console.log("modules Base: ", modules.base);
console.log("modules Path: ", modules.path);

var modules_info = Process.findRangeByAddress(ptr(modules.base))

console.log("Protection: ", modules_info.protection);
  • enumerateModules 함수에서 반환한 객체의 첫 번째 값을 가져오고 객체의 base와 path 키에 해당하는 값을 출력함
  • enumerateModules 함수로부터 반환된 첫 번째 객체는 실행한 앱을 나타냄
  • findRangeByAddress 함수에 앞서 알아낸 첫 번째 모듈의 베이스를 인자로 전달해 더 자세한 정보인 메모리의 권한을 가져오고 출력함

Thread

현재 스레드에서 백트레이스를 출력할 수 있으며 실행을 일시적으로 중지시킬 수 있는 객체
앱 분석시 버그가 발생하거나 실행 흐름을 파악할 때 주로 백트레이스를 확인함

함수

Thread.backtrace([context, backtracer])

현재 스레드의 백트레이스를 제공합니다.

Thread.sleep(delay)

현재 스레드의 실행을 중지하기 위해 사용되며, 초 단위를 사용합니다.
앱에서 이용자와 상호작용이 필요할 때 스크립트의 실행을 일시적으로 중지시킬 때 사용함

Thread 예시

console.log('called from:\n' +
	Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n') + '\n');
  • backtrace 함수를 사용해서 앱의 백트레이스를 조회함

backtrace
버퍼가 가리키는 배열에서 호출 프로그램에 대한 역추적값을 반환함. 시스템에 문제가 발생했을 때 디버그를 좀 더 편리하게 도와줌
에러가 발생한 시점에 오기까지 어떤 과정을 거쳤는지 어떤 문제가 발생했는지 보여준다.

Memory

메모리와 관련된 행위 할 수 있는 객체이며, 메모리의 할당, 검색, 복사, 패치, 권한 변경을 할 수 있음

함수

Memory.scan(address, size, pattern, callbacks)

특정 주소부터 원하는만큼 메모리를 스캔하여 임의의 바이트를 찾습니다. scanSync 함수와 다른 점은 콜백 함수를 등록하여 특정 바이트를 찾았을 때와 스캔이 종료되었을 때의 코드를 구성할 수 있습니다.

Memory.scanSync(address, size, pattern)

특정 주소부터 원하는만큼 메모리를 스캔하여 임의의 바이트를 찾습니다.

Memory.protect(address, size, protection)

메모리의 권한을 변경합니다.

Memory.alloc(size[, options])

프로세스의 힙 메모리 내의 전달된 크기만큼 메모리를 할당하고 주소를 NativePointer 형태로 반환합니다.

Memory.allocUtf8String(str)

프로세스의 힙 메모리 내의 Utf8 형태의 문자열을 할당하고 주소를 NativePointer 형태로 반환합니다.

Memory 예시

var m = Process.enumerateModules()[0];

var pattern = "2f 6c 69 62" // "/lib"
var perm = 0;

Memory.scan(m.base, m.size, pattern, {
	onMatch: function(address, size) {
		console.log('Found match at ' + address);
        
		// get protection
		perm = Process.findRangeByAddress(address);
		console.log("Protection: " + perm.protection);
        
		// change protection
		Memory.protect(address ,4096, 'rw-');
	},
	onComplete: function () {
		console.log('Done');
	}
})

var string = Memory.allocUtf8String("Dreamhack");
console.log('Allocated string address: ' + string);
var pattern2 = "68 61 63 6b";   // "hack"
var result = Memory.scanSync(string, 0x100, pattern2);
console.log("Memory.scanSync() result: " + JSON.stringify(result));
  • Process.enumerateModules 함수를 이용해 앱의 베이스 주소를 가져오고, Memory.scan 함수를 이용해 앱 내에 “/lib” 바이트가 존재하는지 찾음
  • 해당 바이트를 찾으면 바이트가 위치한 메모리 주소의 권한을 확인하고 Memory.protect 함수를 통해 권한을 'rw-' 로 변경
  • Memory.allocUtf8String 함수를 이용해서 "Dreamhack" 문자열을 프로세스의 힙 메모리 내에 할당
  • Memory.scanSync 함수를 이용해 할당한 문자열 중 "hack" 문자열을 찾음
  • Memory.protect 함수들은 런타임에서 쓰기 불가능한 페이지의 권한을 변경하고 코드를 패치하거나 추가할 때 주로 사용함

Module

프로세스의 주소를 알아내는 용도로 사용하는 객체
해당 객체를 사용하면 편리하게 프로세스의 베이스 주소, 외부 함수 주소를 가져올 수 있음

함수

Module.findBaseAddress(name), Module.getBaseAddress(name)

인자로 전달된 모듈의 베이스 주소를 알아냅니다.

Module.findExportByName(moduleName|null, exportName), Module.getExportByName(moduleName|null, exportName)

외부 함수의 주소를 가져올 수 있습니다. 모듈 이름을 알 수 없는 경우 첫 번째 인자

Module 예시

var fopen = Module.findExportByName(null, "fopen");
console.log("fopen: " + fopen);

// iOS
var mName = "libsystem_c.dylib" 

// Android
var mName = "libc.so" 

var fopen_m = Module.findExportByName(mName, "fopen");
console.log("fopen with module: " + fopen_m);
  • findExportByName함수 첫 번째 인자로 null 삽입하고 fopen 함수 주소를 가져옴
  • 두 번째는 libc.so(공유 라이브러리) 모듈에서 fopen 함수 주소를 가져옴 참고

정리
Process: 프로세스와 관련된 정보를 조회할 수 있는 객체
Thread: 현재 스레드에서 백트레이스를 출력할 수 있으며, 실행을 일시적으로 중지시킬 수 있는 객체
Memory: 메모리와 관련된 행위를 할 수 있는 객체
Module: 프로세스의 주소를 알아내는 용도로 사용하는 객체

0개의 댓글