기본 콘텐츠로 건너뛰기

라벨이 ReverseEngineering인 게시물 표시

리버스엔지니어링바이블 - 03. C++ 클래스와 리버스 엔지니어링

C++에서는 멤버 변수, 함수 캡슐화와 상속등의 개념 때문에 리버싱하기가 어려워진다. 풍부한 코딩 경험과 다양한 분석 경험으로서의 감이 필요하다. 풍부한 코딩 경험은 독자의 몫이지만 감을 전수해주고자 한다. 최초로 등장하는 함수부터 디스어셈블 된다. 예를 들어 코드 상에서 상위에 클래스 선언과 이후에 클래스 함수의 선언이있을 경우 디스어셈블코드에서는 클래스 함수의 내용이 온다. 02장에서 말한바와 같이 클래스의 멤버를 사용할때는 ecx에 클래스이 포인터를 넣는다. ecx를 기준으로 오프셋을 더해 사용한다. 이것이 this콜이다. 클래스를 지역 변수로 선언하는 경우 스택변수 esp,ebp를 기준으로 접근하지만 전역 변수를 선언하는 경우 dword xxxx 와 같은 주소에 접근하여 사용한다. 전역 변수로 선언한 클래스의 경우 .data 섹션에 위치하는 것을 알 수 있다. 객체를 동적으로 생성해서 사용하는 경우 객체의 크기 만큼 스택을 확보하는 것이 아니라 new로 할당받은 메모리(heap 영역)를 저장하고, 해당 값을 불러와 오프셋을 더해 사용한다. 생성자와 소멸자의 경우 02장에서 말한바와 같이 메모리 할당 후에 cmp, jz와 같은 흐름을 통해 메모리 할당이 성공하면 생성자를 호출하고 아니면 0을 대입하는 형태를 띈다. 그리고 생성자의 리턴값(코드 상에서는 존재하지 않지만 실제로는 존재함)을 불러와 객체의 멤버접근에 사용하는 형태를 띈다. 디스어셈블코드상에서 여러 변수를 0으로 초기화하고 memset(), malloc()이  흐름이 나타난다면 90%이상 생성자라고 봐도 무방하다. 생성자의 경우 뒤부분에 위치하며, 메모리를 해제하거나 close 관련 api를 호출하는 형태를 띈다. 캡슐화의 경우 개발상에서는 구분가능하지만 디스어셈블에서는 접근시에 차이점이 없기 때문에 구분할 수 없다. 다형성의 경우(virtual 함수가 존재하는 클래스) 별도의 테이블이 생성된다. 그 테이블의 포인터가 객체의 가장 앞에 오게 된다. 따라서 ...

리버스엔지니어링바이블 02. C 문법과 디스어셈블링

언어에도 패턴이 있듯이 어셈블리에도 패턴이 있다. 긴 어셈블리라도 패턴을 익히면 쉽게 해석할 수 있다. 컴파일러가 자동으로 생성해내는 코드를 필터링 해야한다. 기본적인 예로 함수의 진입점과 종료점에서 나타나는 코드가 있다. push ebp mov ebp, esp 진입시 mov esp, ebp pop ebp 종료시 진입시 push하는 레지스터는 함수내에서 사용하겠다는 의미이다. 기존의 값을 보관하기 위한 과정이다. 종료시에 void 형태의 함수를 제외하고는 eax에 결과값을 넣는다. 함수의 호출 규약에는 4가지가 있다. 어셈블리를 보고 호출 규약을 판단할 수 있다. 1.__cdecl 함수 외부에서 스택을 보정하는 것이 특징이다. 보정의 크기를 통해 변수의 크기를 확인할 수 있다. 2.__stdcall 함수 내부에서 스택을 보정한다. ret x 형태를 가진다. x 크기만큼 스택을 보정하면서 함수를 빠져나온다. Win32API의 방식이다. 3.__fastcall 인자 전달을 스택을 이용하지 않고 ecx와 edx 레지스터를 사용한다. 레지스터를 사용하기 때문에 빠르다. 2개 이하의 인자를 갖는 빈번히 사용되는 함수에 사용하기 적합하다. 함수호출 이전에 ecx, edx에 값을 넣는 형태이면 __fastcall 이다. 4.__thiscall C++ 클래스에서 사용되는 방법이다. 현재 객체의 포인터를 ecx에 전달한다. 전달된 ecx를 기준으로 오프셋을 더해 멤버함수나 변수를 사용한다. 스택처리 방법은 __stdcall과 동일하다. 다만 클래스에서 사용된다는 특징이 있다. if문 패턴 비교후 점프 패턴을 띈다. 예. cmp      jz 반복문 비교후 점프 패턴에 더해 반복적으로 수행하기 위해 다시 돌아오는 코드 카운터를 증가시키는 코드가 있다. 예. label 1 add ~~~               cmp  ...

리버스엔지니어링바이블 01. 리버스 엔지니어링만을 위한 어셈블리

어셈블리는 한 번에 한가지 동작만을 한다.(더하기, 빼기, 옮기기). 특별히 여러 동작을 수행하는 opcode의 경우 자주사용되는 명령어 sequence를 묶어 하나로 만들어 놓은 것이다. 어셈블리는 익히는 것은 IA(Intel Arhchitecture)-32를 배우는 것을 의미한다. 레지스터는 CPU가 사용하는 변수이지만 사용 용도가 정해져있다. 하지만 바뀔 수도 있고 바꿀 수도 있다. EAX : 산술 계산과 리턴값 저장 EDX : EAX와 비슷하지만 리턴값을 저장하지 않는다. ECX : 루프문의 카운터 EBX : 여분의 레지스터 ESI,EDI : source에서 destination 으로 데이터를 옮기는 연산 바이트 저장 순서를 엔디언이라고 한다. 사람이 읽는 대로 저장하면 빅엔디언, 반대로 저장하면 리틀엔디언 이다. 인텔은 리틀엔디언을 사용한다. 연산은 레지스터 간에만 일어난다. 메모리끼리는 연산을 할 수가 없다. 코드 내에서 __declspec(naked)를 선언하면 컴파일러에 의한 코드를 삽입하지 않는다. 어셈블리 코딩을 할 경우 __asm을 선언한다. 함수 호출지 인자는 오른쪽에서 왼쪽으로 스택에 push된다. 인자가 push 되기 전에 리턴주소를 먼저 push하기 때문에 ebp+4 에는 리턴주소가 위치한다. 함수가 호출되어 지역변수공간을 할당할 때는 sub esp, 50h 와 같은 형태를 띈다. 지역변수는 스택에 할당한 순서대로 들어가기 때문에, 소스코드에서 가장위에 적은 변수가 가장 아래쪽에 쌓인다. 따라서 ebp에 가까울 수록 가장위의 지역변수다. ebp-4는 첫번째 지역변수이다. 가장 아래쪽에 쌓이긴 때문에 +를 통해 뒤쪽의 메모리주소를 가르켜야할꺼같지만, 스택은 메모리에서 메모리에서 반대로 자라기 때문에 -이다.

C++/MFC

클래스는 오프셋을 기반으로 멤버함수를 호출한다. 클래스를 동적으로 생성하는 경우 크기만큼 스택을 움직이는 것이 아니라, 동적 메모리함수를 불러 결과값을 받아와 저장해서 사용한다. 클래스의 멤버함수는 컴파일시에 주소가 결정되기 때문에 함수를 부르는 형태를 보면 객체의 주소값을 받아와 바로 콜하는 형태를 갖는다. 가상함수가 있는 경우 객체의 가장 앞에 Vtable 포인터가 생성된다. C++에서 멤버함수를 호출할 때는, 객체의 포인터인 this 변수가 따라간다. 매번 this가 넘어가기 때문에 묵시적이고, 스택이아니라 레지스터를 통해 넘어간다. IDA에서 __this 가 그 의미이다. gcc 컴파일러의 경우 this가 스택으로 넘어간다. 생성자와 소멸자는 선택 가능한 함수이다. 클래스 생성시 가장 먼저 호출된다. 동적으로 생성될때 동적 메모리 할당의 결과값이 유효하면 생성자 호출, 틀리면 0을 삽입한다. 이 때문에 IDA에서 코드블럭을 보면 좌우로 갈라졌다가 다시 합쳐지는 형태를 갖는다. 소멸자도 마찬가지이다. Vtable은 .rdata 에 저장된다.