강의정리/Z0FCourse_Re

[x64] Chapter 6 - DLL/6.5 PrintArray.md

우와해커 2020. 1. 29. 11:01

dll.dll의 symbol을 보면 PrintArray가 두개가 있음

 

오버라이딩 형태라 파라미터와 반환 값을 알 수 있음
void __cdecl PrintArray(char * __ptr64 const,int)
Parameters: const char*, int

 

void __cdecl PrintArray(int * __ptr64 const,int)
Parameters: const int*, int

 

첫번째 파라미터로 둘다 포인터 주소를 받는다.
함수 이름을 보고 함수을 추측해본다면 첫번째 파라미터는 주소는 배열의 시작 주소 일 것이다.

 

두번째 파라미터는 확실하게 알지 못하지만 배열의 크기 또는 얼마나 출력할지 정하는 값일 것으로 추측한다.
리버싱할 때 중요한 점은 항상 큰 그림을 기억해야 한다는 것이다.
문제 해결이나 교육적인 추측을 할 때 도움이 될 수 있기 때문에 이것을 기억하십시오.

 

 

 

Lies and Deception

 

PrintArray 함수 내부를 보면 시작부터 EDX를 사용하고 있으며
함수 접두사로 _cdecl이 붙어있다. 그래서 호출규약이 cdecl이라고 생각할 수 있지만 아니다!
이것은 fastcall이다.

 

TEST EDX, EDX : EDX는 fastcall에서 두번째 파라미터로 이용된다.
MOV RSI, RCX : RCX는 현재 스코프에서 초기화되지 않았지만 fastcall에서 첫번째 파라미터로 이용된다.

 

64dbg는 실제로 그것이 fastcall임을 알려줍니다.
x64dbg는 함수에 전달되는 파라미터를 볼 수 있도록 하며 다른 호출 규칙을 지닌 파라미터를 볼 수 있도록 합니다.
일반적으로 당신이 어떤 호출 규칙이 사용될지 선택할 수 있지만 x64dbg는 fastcall만 처리 할 수 있도록 합니다.

(보장되진 않지만) 레지스터 창에서 파라미터와 호출 규칙을 볼 수 있습니다.

 

오른쪽 레지스터 패널의 중하단을 보면 Defalut(x64 fastcall)표시를 확인할 수 있다.
이 패널에 표시되는 파라미터 수를 변경할 수도 있다.
따라서, fastcall을 사용하고 있다고 확신할 수 있다. __cdecl 접두사 외에는 이것이 cdecl이라는 힌트는 없다

 

 

PrintArray(Int)

 

우리는 이제 PrintArray 함수가 fastcall을 사용하고 있음을 알고 있습니다.
또한 어떤 유형의 파라미터를 사용하는지 알고 있습니다.

이제 우리는 그것이 어떻게 작동하는지, 무엇을하는지, 파라미터가 무엇인지, 무엇을 리턴 하는지를 알아야합니다.
데코레이팅되지 않은 이름에 따르면 이 함수는 아무것도 반환하지 않아야 합니다 (void).
파라미터로 interger를 사용하는 PrintArray 함수로 시작해 봅시다. integer는 일반적으로 사용하기 쉽기 때문에 이것으로 시작하기로 선택했습니다.

 

* XOR: 같으면 0 다르면 1

 

TEST EDX, EDX
Jle DLL.7FFA467DLEC7
처음 두 줄은 TEST EDX, EDX에서 EDX가 0이 아닌지 확인합니다. EDX가 0보다 작거나 같으면 dll.7FFA467D1EC7로 점프하여 반환합니다.
점프 위치는 맨 왼쪽의 주황색 화살표로 표시됩니다.

 

MOV QWORD PTR SS:[RSP+0x10], RSI

그런 다음 RSI는 RSP + 0x10으로 이동합니다. 이것은 아마도 RSI를 보존하기 위해 수행됩니다.

 

PUSH RDI
그런 다음 RDI가 스택으로 푸시됩니다. 이것은 아마도 RDI를 보존하기 위해 수행됩니다.

 

SUB RSP, 0X20
이것은 스택을 설정하는 프롤로그 함수의 일부입니다.

나는 이것이 RSI와 RDI가 보존되고 있음을 거의 확인했기 때문에 이것을 지적하고 있습니다.
보존은 거의 항상 프롤로그 내에서 또는 프롤로그 주변에서 수행됩니다.

 

MOV QWORD PTR SS:[RSP+0x13], RBX
RBX가 저장 중일 수 있습니다. RSI 및 RDI와 마찬가지로 fastcall에서는 비 휘발성으로 간주되므로 저장해야합니다.

 

MOV RSI, RCX
이것은 우리가 관심있는 함수의 "실제"부분의 시작 인 것 같습니다. RCX는 첫 번째 파라미터입니다.
RSI는 이제 함수에 전달 된 첫 번째 파라미터를 보유합니다.
레지스터 챕터를 기억한다면 RSI가 종종 소스 포인터로 사용된다고 언급했습니다.

 

XOR EBX, EBX
EBX를 0으로 채웁니다.

 

MOVSXD RDI, EDX
EDX를 RDI로 이동합니다. MOVSXD는 "부호 확장을 사용하여 dword를 qword로 이동"의 줄임말입니다.
부호 확장은 부호 (양수 또는 음수)를 유지하면서 비트 수를 늘리는 것을 말합니다.
우리가 여기서 정말로 관심을 가지는 것은 RDI가 이제 EDX에 있던 것과 동일하다는 것입니다.
EDX는 두 번째 파라미터입니다.

 

여기에 경험이 도움이됩니다. RDI와 RSI가 설정되어 있다는 사실은 그들이 일종의 루프에서 사용될 것이라고 생각합니다.  아마도 RSI에 있는 모든 것을 반복하는 루프 일 것입니다. 보장 할 수는 없지만 이 함수의 목적은 배열의 모든 것을 출력하는 것처럼 보입니다.

 

MOV EDX, DWORD PTR DS:[RSI + RBX * 4]
RSI + RBX * 4에 있는 것은 EDX로 이동합니다.

RSI는 우리가 알고있는 첫 번째 파라미터의 배열입니다 (물론 배열의 시작 주소).
RBX는 지금 0입니다. RBX에 4를 곱하면 0이됩니다.

지금은 이것이 무의미 해 보이지만, 살펴보면 이것이 루프임을 알 수 있습니다.
우리는 이것을 다시 다룰 것입니다. 지금은 배열의 첫 번째 요소 (RSI)를 EDX에 넣었습니다.

 

CALL ...
이 기능을 파헤 치면 이전 연구에 따르면 이것이 std :: cout일 가능성이 높습니다.
좋아, 그러나 첫 번째 파라미터는 EDX (또는 RDX)가 아닌 RCX를 통과하지 않습니까?
dll.sub_7FFA467D20B0의 함수로 들어가면 실제로 RCX를 파라미터로 전달 된 것처럼 사용하지 않고 무시하는 것을 알 수 있습니다.
그러나 EDX를 파라미터처럼 취급합니다. 이것이 일부 컴파일러 최적화라고 가정합니다.
std :: cout은 RCX로 함수를 호출하므로 컴파일러는 std :: cout을 호출 할 때 RCX를 그대로 두기로 결정했기 때문에
RCX를 다른 레지스터에 저장할 필요가 없습니다. 이렇게하면 몇 가지 명령어들이 저장됩니다.

 

CALL ...
이전 연구에 따르면 이것이 std :: endl임을 알 수 있습니다.

 

INC RBX
이것은 RBX를 1씩 증가시킵니다. RBX가 0이고 4를 곱한 것을 기억하십시오.
이 연산의 결과는 배열에 대한 오프셋으로 사용되었습니다.

이제 RBX가 0 인 이유가 이해가되고 RBX는 루프의 반복을 유지합니다.
그런 다음 RBX를 사용하여 루프의 반복 횟수 인덱스에 4를 곱한 배열의 요소에 액세스합니다.
당신이 배열을 출력하는 프로그램 작성한 경험이 있다면 이해가 될 것입니다.
루프가 반복 될 때마다 루프 인덱스 (iteration count)에서 배열의 요소에 액세스합니다.
(예 : array [2], 여기서 2는 루프의 현재 반복입니다). 정수가 4 바이트라는 사실을 설명하기 위해 루프 반복에 4를 곱합니다.

 

CMP RBX, RDI
이것은 RBX와 RDI를 비교합니다. RBX는 루프의 현재 반복을 보유하며 RDI는 함수에서 두 번째 파라미터를 보유하도록 설정되었습니다. 두 번째 파라미터는 루프 카운터와 비교됩니다.
이 정보는 이전 정보와 가정과 함께 전달 된 두 번째 파라미터가 인쇄 할 최대 요소 수 또는 배열 크기라고 생각합니다.

 

JL dll.7FFA467D1EA0
루프의 시작으로 점프합니다.

 

루프가 계속됩니다. 배열에 있는 모든 것을 인쇄합니다 [RSI + RBX * 4]. RBX * 4를 지적하고 싶습니다.

이것은 실제로 매우 중요합니다.
이는 어셈블리에서 개별 바이트에 액세스 할 수 있기 때문에 수행됩니다.

이것은 정수 배열이며 각 정수는 4 바이트입니다.
명령 MOV EDX, DWORD PTR DS : [RSI + RBX * 4]는 루프의 반복에 해당하는 배열의 요소를 EDX로 이동합니다.
이것이 두 번째 반복 인 경우 반복은 1입니다 (이 루프는 0에서 시작 함). 1 * 4는 4입니다.
따라서 정수가 4 바이트이므로 두 번째 요소 인 array + 4에 있는 모든 항목에 액세스합니다.
루프가 완료되면 두 번째 파라미터로 지정된 요소 수가 인쇄됩니다.

 

MOV RBX, QWORD PTR SS:[RSP + 0x30]
MOV RSI, QWORD PTR SS:[RSP + 0x38]
ADD RSP, 0x20
POP RDI
RET

 

마지막으로 에필로그가 진행됩니다.
이 함수는 RAX가 설정되지 않았기 때문에 리턴 값이 없는 것 같습니다.

 

 

PrintArray Conclusion

PrintArray가 배열의 요소를 두 번째 파라미터에 지정된 수까지 인쇄한다고 결정했습니다.
이 함수는 두 가지 파라미터를 사용합니다. 첫 번째 파라미터는 배열이고 두 번째 파라미터는 인쇄 할 요소 수입니다.
두 번째 파라미터는 배열의 크기 일 수도 있지만 결국 동일하게 작동하므로 실제로 중요하지 않습니다.

 

 

PrintArray (Char)

 

이제 char *를 매개 변수로 사용하는 PrintArray 함수를 살펴 보겠습니다.
이 함수는 다른 함수와 거의 동일합니다. 내가 지적하고 싶은 유일한 차이점은 배열의 요소에 액세스하는 방법입니다.
문자는 한 바이트에 불과하기 때문에 정수 버전의 PrintArray처럼 [RBX + ESI * 4]를 사용하여 배열의 요소에 액세스 할 필요가 없습니다.
대신 [RBX + ESI]만으로 요소에 액세스 할 수 있습니다. 다시, RBX는 루프 반복 카운터입니다.

 

 

 

코드로 구현해보기

#include <iostream>
#include <Windows.h>

//void PrintArray(char/int array[], int sizeOfArray/ElementsToPrint);
typedef void(WINAPI* IPrintArray_char)(char[], int);	//?PrintArray@@YAXQEADH@Z
typedef void(WINAPI* IPrintArray_int)(int[], int);		//?PrintArray@@YAXQEAHH@Z

int main()
{
	HMODULE dll = LoadLibraryA("DLL.DLL"); //Load our DLL.

	if (dll != NULL)
	{
		//Char PrintArray
		IPrintArray_char cPrintArray = (IPrintArray_char)GetProcAddress(dll, "?PrintArray@@YAXQEADH@Z");
		if (cPrintArray != NULL) {
			char myArray[10] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J' };
			cPrintArray(myArray, 10);
		}
		else { printf("Can't load the function."); }

		//Int PrintArray
		IPrintArray_int iPrintArray = (IPrintArray_int)GetProcAddress(dll, "?PrintArray@@YAXQEAHH@Z");
		if (iPrintArray != NULL) {
			int myArray[10] = { 0,1,2,3,4,5,6,7,8,9 };
			iPrintArray(myArray, 10);
		}
		else { printf("Can't load the function."); }
		
	}
}

 

Final Notes
리버싱의 짤릿함 중 하나는 모든 것을 파악하고 수집 한 모든 정보와 퍼즐을 결합하는 것입니다.
이제 휴식을 취하세요~