본문 바로가기

리버싱!

가상 함수 테이블(vtable) 공부 2 - 자세히 분석

자세히 분석해보기

#include <stdio.h>
class Parent {
public:
	virtual void show1() {
		printf("this is parent1\n");
	}
	virtual void show2() {
		printf("this is parent2\n");
	}
	virtual void show3() {
		printf("this is parent3\n");
	}
};
class Child : public Parent {
public:
	virtual void show1() {
		printf("this is child1\n");
	}
	virtual void show3() {
		printf("this is child3\n");
	}
};
int main() {
	printf("main");
	Parent* p = new Parent;
	Child* c = new Child;
	Child* c2 = new Child;
	p->show1();
	p->show2();
	p->show3();
	c->show1();
	c->show3();
	c2->show1();
}

다음처럼 출력이 되는 프로그램이다.

IDA로 보면

기준이 되는 포인터를 잡고 +8 +16등 연산을 한다. 어셈블리로 보면

rbp+190에 임시로 정해준 var_188, var_168, var_148을 더해 call을 하는것을 알수있다.

소스코드와 비교할때 var_168은 c의 객체주소값, var_148은 c2의 객체 주소값일것이다. 

그리고 call하기전에 rcx에 값을 넣어주고 call을 하는데 내가 기존에 알고있던 내용은

RDI : 첫번째 인자
RSI : 두번째 인자
RDX : 세번째 인자
RCX : 네번째 인자
R8 : 다섯번째 인자 -> 추가된 범용 레지스터
R9 : 여섯번째 인자 -> 추가된 범용 레지스터

이거여서 왜 네번째 인자에만 값을 주는가에 대해 궁금했었는데 찾아보니 위의 인자방식은 6개의 레지스터를 사용하고 더있을시 스택을 이용하는데 이방식은 리눅스만 사용하고 윈도우의 경우 4개의 레지스터를 사용하고 더있을시 스택을 이용한다.

RCX : 첫번째 인자
RDX : 두번째 인자
R8 : 세번째 인자 -> 추가된 범용 레지스터
R9 : 네번째 인자 -> 추가된 범용 레지스터

그래서 윈도우의 경우 rcx는 첫번째 인자가 되므로 call을 하기전 rbp+190+객체주소값을 인자로 넣어준다. 즉

call과 인자로 같은값을 (rbp+190+객체주소값)(rbp+190+객체주소값) 하는것같다.

동적디버깅

여러 객체들의 show함수를 호출하는 장면이다.

parent show1 호출시 레지스터값

parent show2 호출시 레지스터값

child1 show1 호출시 레지스터값

child2 show1 호출시 레지스터값

child2 show2 호출시 레지스터값

 

모두 RAX 주소의 값을 CALL 하기때문에 먼저 Parent rax값인 0x7FF7F249AC30을 보면

요련함수가 있는것을 볼수있다. 주소계산은 현재 RIP값+6F2(리틀엔디안)+5를 하면 실제 주소가 나오게 된다.

Child rax값인 0x7FF7F249ACA0값을 보면 이함수를 콜해준다

rax+4는 show2, rax+8은 show3가 될것이다.

 

두 RAX 주소값인 함수의 포인터 배열로 이루어져 있는것을 알수있다. 여기가 Vtable인가보오

값을 보면 parent와 child 두개의 vtable이 있고 각각의 show1 주소는 다른것을 볼수있다. 하지만 show2의 경우 오버라이트를 안했기때문에 두개모두 같은 주소를 참조하는것을 알 수있다. 

child1과 child2의 경우 둘다 parent의 상속을 받고있기에 vtable을 공유한다. child1 과 child2의 rax 값을 보면 같은것을 알수있다.

궁금증1. 왜 rcx에 vtable 포인터를 넣고 call을하는가

rax을 콜하기전 연산을 보면 rax에 vtable포인터를 넣고 그포인터의 값을 rax에 넣고 call을하게 되는데 rcx에 vtable포인터를 넣게 된다. show부분의 어셈블리를 보면 rsp+8에 넣고 전혀 사용하지 않는데 this포인터 사용을 위해 넣는건가 라고 생각했다.

virtual void show1() {
		printf("this is child1\n");
		this->show2();
	}

코드를 다음처럼 바꾸고 어셈블리를 보면

중간의 필요없는 부분을 제외하고 중요한 부분을 보면

rsp+8에 vtable pointer값 저장 rbp+20이된다. push 두번으로 스택에 두개의 값이 박히고 rbp는 rsp+20의 값으로 변경

537A2FFBA0, 537A2FFAC0

 

 

CALL하기전에 RCX에 VTABLE포인터를 넣고 CALL을 했는데 실제 함수에서는 전혀 쓰이지 않는게 궁금했다.

This 포인터 사용으로 인한 인자 전달이라 생각했고 소스를 짜서 분석해봤다.

virtual void show1() {
		printf("this is child1\n");
		this->show2();
	}

show1 부분을 다음과 같은 코드로 바꿔서 진행했다.

실제 함수 진입시 프롤로그 전상태

RSP+8에 RCX 넣은상태->매개변수를 넣어준다.

PUSH RBP

PUSH RDI 한 상태 그림은 잘못됐지만 RBP RDI를 PUSH 했기에 RSP값도 변화가 된다. FA48이 될것이다.

SUB RSP E8 한 상태

lea RBP [RSP+20] 한 상태

이전에 백업해둔 RCX값을 사용하는것을 알 수 있다. THIS포인터 용도가 맞는것같다.

정리하면, 바이너리에서 클래스별로 vtable 의 주소를 가져오는 시점은 클래스가 선언되는 시점인것같다.

클래스가 선언되는 시점에 vtable 주소를 가져와 가지고 있다가, 클래스 멤버 가상함수를 호출할 때에 활용된다.