취약점분석/Reversing

[Reversing]콜링 컨벤션(Calling convention) -2 (64 bits)

poiri3r 2025. 6. 27. 21:54

이번 포스팅은 저번 1편에 이어서 64비트에서의 함수호출 규약과 32비트와의 차이에 대해서 포스팅 해보겠습니다.

32비트 콜링컨벤션
64비트 콜링컨벤션

저번에 포스팅할 때 썼던 함수호출규약표 입니다. 저번에 32비트안의 콜링컨벤션 사이에서는 스택 정리 주체에 따라 cdecl과 stdcall의 구분이 가능했었는데, 64비트 콜링컨벤션을 보면 전부 caller로 같은 걸 볼 수 있습니다.

 

32비트와 64비트의 가장 큰 차이점은 인자 전달 방식입니다. 32비트 방식은 스택기반이고 64비트는 레지스터 기반입니다.

스택 접근은 메모리 접근이라 메모리 버스나 paging등 속도에 저하되는 부분들이 생기지만 레지스터는 CPU내부에 있어서 속도가 훨씬 빠릅니다

 

두번째 차이점으로 64비트에서는 단일 규약으로 통일하였습니다.

32비트에서는 여러가지 호출 규약이 존재하는데(cdecl, stdcall 외에도 fastcall, thiscall 등이 있음) 규약이 섞여 오류가 발생하기도 하고, DLL/라이브러리 호환에서도 문제가 발생합니다. 이 문제를 해결하기 위해서 64비트에서는 레지스터 우선 사용 + caller가 스택 정리를 하는 기본 틀을 유지하면서 플랫폼별 약간의 차이만 존재합니다.

 

64비트 방식을 살펴보기 전에 인자 전달 방식을 자세히 살펴보기 위해 add.c를 좀 수정해보겠습니다.

int add(int a, int b,int c,int d, int e) {
    return a + b + c + d + e;
}

int main() {
    int result = add(1, 2, 3, 4, 5);
    return 0;
}

*인자 전달 방식을 자세히 보기 위해 인자를 5개로 늘렸습니다.

1.MS x64

먼저 64비트 방식으로 컴파일 하기 위해서 Developer Command Prompt가 필요합니다.

(*visual studio의 설정을 바꿔서 asm파일을 분석해도 가능합니다)

해당 prompt를 사용하려면 visual studio가 설치된 상태에서 시작메뉴에 developer라고 검색하면 나오는 Developer PowerShell for VS를 실행하면 됩니다.

해당 프롬프트를 킨 뒤 add.c가 있는 파일로 이동해주었습니다.

컴파일은 다음 명령어를 입력해주면 가능합니다.

cl /Fa addMs64.asm add.c

/Fa는 어셈블리 파일을 출력해주는 명령어입니다.

기본적으로 intel형식으로 출력해주고 생성된 asm 파일을 열어보면 다음과 같습니다.

왼쪽은 add함수 오른쪽은 main 함수

먼저 add함수부터 어셈블리어를 살펴보겠습니다.

add PROC
; 레지스터에 담겨온 인자들을 스택에 저장
mov	DWORD PTR [rsp+32], r9d   ; 4번째 인자 d → [rsp+32]에 저장
mov	DWORD PTR [rsp+24], r8d   ; 3번째 인자 c → [rsp+24]
mov	DWORD PTR [rsp+16], edx   ; 2번째 인자 b → [rsp+16]
mov	DWORD PTR [rsp+8], ecx    ; 1번째 인자 a → [rsp+8]

; a와 b를 더함
mov	eax, DWORD PTR b$[rsp]    ; eax = b (rsp+16)
mov	ecx, DWORD PTR a$[rsp]    ; ecx = a (rsp+8)
add	ecx, eax                  ; ecx = a + b

; c, d, e를 계속 더함
mov	eax, ecx                  ; eax = a + b
add	eax, DWORD PTR c$[rsp]    ; eax += c (rsp+24)
add	eax, DWORD PTR d$[rsp]    ; eax += d (rsp+32)
add	eax, DWORD PTR e$[rsp]    ; eax += e (rsp+40)

; 결과는 eax에 담겨 리턴됨
ret	0
add ENDP

 

인자가 많아져서 함수가 조금 길어졌지만 천천히 살펴보도록 하겠습니다.

위에 표에서 봤듯이 MS x64버전에서는 RCX,RDX,R8,R9 레지스터를 사용합니다. (int형이 4바이트이므로 eax,edx,r8d,r9d사용)

인자가 5개이므로 하나가 더 필요한데 하나는 메인함수에서 스택 메모리에 직접 저장하여 사용합니다.

저번에 ORW작성할 떄 해봤듯 이번 add함수 이전 call func_add 이전 r9d.r8d,edx,eax 값에 대한 세팅을 다 해뒀을 것입니다.

간단하게 add로 값을 다 더한뒤 eax에 저장해서 return 했습니다.

 

다음 main 함수 부분을 살펴보겠습니다.

main	PROC
//메인함수
sub	rsp, 72					; 00000048H
mov	DWORD PTR [rsp+32], 5
mov	r9d, 4
mov	r8d, 3
mov	edx, 2
mov	ecx, 1
call	add
mov	DWORD PTR result$[rsp], eax
xor	eax, eax
add	rsp, 72					; 00000048H //스택정리
ret	0

 

먼저  MS x64에서의 특징 한가지는 Shadow Space라고 하는 여유공간 확보입니다.

Shadow Space란 호출자가 스택에 미리 확보해주는 32바이트(4*8바이트)입니다.

callee가 필요할 때 1~4번째 인자를 스택으로 백업할수 있도록 여유공간을 확보해두며, 함수에 실제로 인자가 몇개 들어가는지에 관계없이 32바이트의 공간을 확보해둡니다.

sub rsp, 72

 

 sub rsp, 72로 스택 공간을 확보해줍니다 (shadow space 32바이트 + 로컬변수 40바이트)

 

mov	DWORD PTR [rsp+32], 5
mov	r9d, 4
mov	r8d, 3
mov	edx, 2
mov	ecx, 1

 

인자의 오른쪽부터 왼쪽으로 바로 스택과 레지스터에 저장해둡니다.

MS x64에서는 인자 4개까지 레지스터에 저장하기 떄문에 5는 스택에 직접 저장하고 있습니다. 

 

add	rsp, 72					; 00000048H //스택정리

 

함수 호출자인 main함수에서 add를 통해 스택정리를 해주는 모습입니다.

아까 rsp에서 72를 뺐으니 다시 72를 더해 스택포인터를 원상복구해줍니다.

크게 어려운 내용 없이 32비트체제에서 형태만 조금 바뀐 모습입니다.

 

2.System V AMD64 (SYSV x64)

이제 유닉스에서 계열에서 표준으로 사용하는 콜링 컨벤션에 대해 살펴보겠습니다.

보통 리눅스에서 쉽게 접하게 되며, 레지스터에 6개까지 저장할 수 있습니다.

저희가 저번에 ORW에서 사용했던 코드들이 sysv 기반이고, syscall table에 있는 표를 통해 인자에 들어가는 값들을 확인할 수 있습니다.

아까 함수 인자를 5개로 늘린 add 대신 저번 포스팅에 사용했던 코드를 가져와서 컴파일해주었습니다

gcc -O0 -S -masm=intel -o addsysv.s add.c

 

왼쪽은 add함수 오른쪾은 main함수입니다.

딱 봐도 이전 콜링컨벤션이랑 큰 차이 없이 비슷한 형태를 띄고 있습니다.

사용하는 레지스터만 조금 바꼈는데 add부터 빠르게 살펴보겠습니다.

//add함수
push    rbp               ;
mov     rbp, rsp          ; 

mov     DWORD PTR -4[rbp], edi
mov     DWORD PTR -8[rbp], esi //edi,esi에서 인자값을 꺼내 저장

mov     edx, DWORD PTR -4[rbp] 
mov     eax, DWORD PTR -8[rbp] //스택에 저장된 값을 edx,edx에 꺼냄
add     eax, edx               //+연산     

pop     rbp               
ret

 

main를 보겠습니다.

//main
push	rbp
mov	rbp, rsp
sub	rsp, 16 //지역변수 16바이트 확보(add가 아니라 main)
mov	esi, 2
mov	edi, 1
call	add
mov	DWORD PTR -4[rbp], eax
mov	eax, 0
leave //(main함수가 callee로써 스택포인터 정리)
ret

메인함수에서 다른 콜링컨벤션들과 다른 부분은 레지스터로만 인자를 전달하기 때문에 call add이후에 add rsp를 통해 스택포인터를 복구하는 명령어가 없는 점 입니다.

(*sub rsp와 leave부분도 add함수에 대한 스택포인터 확보와 정리가 아닌 main함수 자체에 대한 스택확보와 정리입니다.)

그래서 엄청 어셈블리 코드가 깔끔??한 느낌을 주고 있습니다.

 

가면 갈수록 다 설명했던 부분들이라 딱히 덧붙일 말들이 없네요 ..

내용들을 처음 보시는 분들은 크게 어렵지 않으니 직접 해보면 이해가 잘 되실것이라 생각합니다.

이상으로 콜링컨벤션에 대한 포스팅을 마치겠습니다. 읽어주셔서 감사합니다~