취약점분석/Reversing

[Reversing] 어셈블리어 실전 구조 분석 (if문과 반복문)

poiri3r 2025. 6. 13. 19:18

이번 포스팅은 if문과 반복문의 어셈블리어 구조 분석입니다. 실제로 자주 사용되는 문법인 만큼 어셈블리어를 익혀두면 도움이 많이 될거라 생각하고 또 어셈블리 명령어만 보는거랑 또 실제로 어떻게 사용되는지 보는게 와닿는게 다르다고 생각해서 준비했습니다! 

 

먼저 구조 분석하기 이전에 컴파일러 별 어셈블리어 구조에 차이가 있어서 간단하게 알아보고 가겠습니다.

어셈블리어 구조는 AT&T 스타일과 Intel 스타일로 두가지가 있습니다.

AT&T 스타일에는 대표적으로 GCC가 있고, Intel 스타일에는 MSCV(비주얼스튜디오)와 NASM이 있습니다.

구분 AT&T (GCC, Clang 등) Intel (MSVC)
명령어 순서 출발지 -> 목적지 목적지 <= 출발지
레지스터 표기 %eax, %ebx (%가 붙음) eax, ebx
상수값 표기 $5 5
명령어 접미사 movl, addq (l=32비트 q=64비트로 명령어가 처리하는 데이터 크기에 따라 구분)  접미사 x

 

상세하게 출력을 보면서 비교해보겠습니다.

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

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

해당하는 코드를 gcc와 visualstudio로 컴파일해서 어셈블리 코드를 열어보았습니다.

어셈블리 코드를 보는 방법은 챗지피티한테 물어보면 나오기도 하고 포스팅이 길어지기 때문에 생략하겠습니다.

 

먼저 gcc로 컴파일 한 메인함수 부분입니다.

어셈블리 명령어는 이따가 분석할거기 때문에 형태만 보면 됩니다.

제일먼저 코드를 봤을 때 저희가 알던 어셈블리에 비해 좀 복잡해보이는 느낌이 듭니다.

%와 $같은 접두어들이 붙어있고 명령어 뒤에도 q와 l이 붙어 있습니다. 

또 저번 포스팅에서는 피연산자가 순서대로 나온다고 했었는데 AT&T 스타일은 반대로 작성됩니다 

movl $4, %esi 명령어는 esi에 상수 4값을 넣는 명령어입니다.

 

그 다음 VisualStudio로 컴파일 한 메인함수 부분입니다.

중간에 디버거 호출 등이 끼어있어 복잡해보일 순 있습니다만 기본적으로 명령어나 상수에 접두어가 없고 저희에게 익숙한 형태의 모습입니다. 

보통 저희가 IDA에서 프로그램을 분석할 때에는 Intel형식으로 출력해줍니다.

ida에서 출력해주는 어셈블리어

 

일단 지금 크게 중요한 부분은 아니니 이정도 차이가 있다부분만 확인하고 넘어가도록 하겠습니다.

 

이제 if문과 반복문에서 어셈블리어 구조 분석을 진행해보겠습니다. 제가 초보기도 하고 이 포스팅을 보시는 분들도 저와 비슷할 것이라 생각하기 때문에 32비트 + Intel 형식으로 분석을 진행해보겠습니다.

 

if문 구조 분석

int Temp(int a)
{
int b = 1;
if (a==1){
	a++;
}
else{
	b++;
}
return b;
}
int main(int argc, char* argv[]){
	Temp(1);
}

 

a입력을 받아서 Temp함수에 전달한 뒤 a==1이면 a값을 1 증가시키고 아닐경우 b값을 증가시킨 뒤 b를 리턴하는 간단한 코드입니다.

.text:00401000 push ebp                  //기존 스택 프레임 값 보존
.text:00401001 mov ebp,esp               //새로운 스택 프레임 설정 
.text:00401003 push ecx                  //ecx의 값을 스택에 저장(b)
.text:00401004 mov dword ptr [ebp-4], 1  //지역변수 b=1로 설정, 주소는 [ebp-4]
.text:0040100B cmp dword ptr [ebp+8], 1  //a값([ebp+8]과 1을 비교
.text:0040100F jnz short loc_40101C      //a!=1이면 loc_40101C로 점프(else로 이동)
.text:00401011 mov eax, [ebp+8]          //if분기 처리
.text:00401014 add eax, 1                //eax에 1을 더함(a++연산)
.text:00401017 mov [ebp+8], eax          //증가된 값을 다시 a에 저장
.text:0040101A jmp short loc_401025      // if처리 끝난 뒤 return 코드로 점프
.text:0040101C loc_40101C: 
.text:0040101C            mov ecx, [ebp-4]   //b값을 ecx에 로드
.text:0040101F            add ecx, 1         //ecx에 1을 더함(B++)
.text:00401022            mov [ebp-4],ecx    //증가된 b값 저장
.text:00401025
.text:00401025 loc_401025: //함수 리턴 처리
.text:00401025            mov eax, [ebp-4]   //eax에 b값 저장
.text:00401028            mov esp, ebp       //스택프레임 정리
.text:0040102A            pop ebp            //이전 ebp값 복원
.text:0040102B            retn               //함수 호출자에게 복귀

어셈블리어로 보면 다음과 같습니다. 제가 전에 노션에 정리해둔 자료를 가져온거라 주석값을 같이 보시면서 이해하시면 될 것 같습니다.

먼저 지역변수 a와 b값은 [ebp+8], [ebp-4]에 저장합니다.

스택은 아래로 쌓이기 때문에 지역변수인 b 값은 [ebp-4]에 저장됩니다.

여기서 유의할 점은 매개변수인 a 값은 [ebp+8]에 저장이 되었는데 [ebp+4]값에는 리턴 주소가 저장이 됩니다.

0040100B에서 문자열을 비교한 뒤 다를 경우 (else문)  401025부분으로 점프 합니다. 그 후 리턴없이 401025부분으로 코드가 흘러갑니다.

같을경우 if (a==1)에서 a++연산을 수행한 뒤 jmp문을 통해 함수 리턴을 처리하는 401025부분으로 점프합니다. 

if문이 늘어날 경우 jmp를 통한 분기가 증가합니다. 

위 코드와는 관계없는 코드지만 IDA에서는 분기점을 보기 쉽게 나눠서 보여줍니다.

if문 분석에선 분기가 나눠져서 jmp를 통해 if-else를 처리하는 부분과 매개변수는 [ebp+]로 지역변수는 ebp[-]로 쌓인다는 것과 [ebp+4]에는 리턴주소가 담겨있다는 점을 알아두시면 좋을 것 같습니다.

 

반복문 구조 분석

*반복문은 for,while,goto 등이 있지만 기본 어셈블리 뼈대는 동일하므로, 트레이스 중인 코드가 반복문인 것 만 알아낼 수 있으면 된다.

int loop(int c)
{
	int d;
	for (int i=0; i<=0x100; i++)
	{
		c--;
		d++;
	}
	
		return c+d;
}

for문 구조는 다음과 같습니다.

인자를 하나 받아서 0x100회동안 반복하며 c--와 새로운 변수 d에 1씩 더하고 마지막에 c와 d를 더한 값을 리턴하는 구조입니다.

.text:00401000 push ebp                     // --
.text:00401001 mov ebp,esp                  // --
.text:00401003 sub esp, 8                   //지역변수 8바이트 확보(d랑 i)
.text:00401006 mov dword ptr [ebp-8], 0     //루프용 i(ebp-8)에 0대입 
.text:0040100D jmp short loc_401018         //조건 검사로 점프
.text:0040100F mov eax, [ebp-8]             //eax에 i대입
.text:00401012 add eax,1                    //i++
.text:00401015 mov [ebp-8], eax             //i저장
.text:00401018 cmp dword ptr [ebp-8], 100h  //i와 0x100 비교
.text:0040101F jg short loc_401035          //i가 100보다 크면 루프 종료
.text:00401021 mov ecx, [ebp+8]             //ecx에 인자값(c) 저장
.text:00401024 sub ecx, 1                   //a--
.text:00401027 mov [ebp+8], ecx             //감소된 값을 다시 ebp+8에 저장
.text:0040102A mov edx, [ebp-4]             //edx에 b저장
.text:0040102D add edx, 1                   //b++
.text:00401030 mov [ebp-4], edx             //감소된 값 저장
.text:00401033 jmp short loc_40100F         //루프 다시 시작(40100F)
.text:00401035 mov eax,[ebp+8]              // eax는 c값
.text:00401038 add eax, [ebp-4]             // eax에 d값을 더함 (return c+d)
.text:0040103B mov esp, ebp                 // 스택 정리
.text:0040103D pop ebp                      //이전 프레임 복원
.text:0040103E retn                         //함수 종료

먼저 변수가 총 3개로 매개변수 c와 지역변수 d와 i가 있습니다.

눈 여겨볼 어셈블리 코드는  다음과 같습니다.

.text:0040100D jmp short loc_401018         //조건 검사로 점프
.text:0040100F mov eax, [ebp-8]             //eax에 i대입
.text:00401012 add eax,1                    //i++
.text:00401015 mov [ebp-8], eax             //i저장
.text:00401018 cmp dword ptr [ebp-8], 100h  //i와 0x100 비교
.text:0040101F jg short loc_401035          //i가 100보다 크면 루프 종료

...

.text:00401033 jmp short loc_40100F         //루프 다시 시작(40100F)

...

 

수를 비교하는 cmp보다 i++하는 어셈블리 코드가 먼저 나오는데. 이 것은 원본 코드를 최적화 하는 과정에서 컴파일러가 for문을 do while형식으로 재구성하기 때문입니다. 반복문은 for,while상관없이 내부적으로 비슷하게 취급하고 컴파일했을 때 똑같은 어셈블리코드를 가질 수도 있습니다.

 

어쨌든 처음에 조건 검사문으로 점프한 뒤 i를 비교하고, i를 비교했을 때 0x100보다 크면 401035지점으로 jmp해 루프를 종료 시키고 아니라면 정상적인 함수 흐름으로 지나가면서 덧셈뺄셈을 한 뒤 40100F지점으로 점프 해서 i++을 진행합니다.

 

구문의 기능과 흐름을 위주로 설명하다보니, 스택포인터를 저장하고 스택 공간을 확보하는 push ebp, mov ebp,esp같은 부분에 대한 설명은 생략하였는데, 다음에 스택포인터 관련하여 포스팅을 따로 작성해보도록 하겠습니다. 

어셈블리어는 쉽지 않은것 같네요 ... 

이상으로 포스팅을 마치겠습니다.