이번 포스팅에서는 가장 기본인 hello world 프로그램을 MIPS아키텍쳐에서 리버싱을 해본 것을 다루어보도록하겠습니다.

추후 분기문, 반복문 등 기본적인 구문에 대해서 하나씩 추가해 볼 생각입니다.


MIPS에서 사용하는 레지스터에 대한 설명과 기본적인 명령어에 대한 설명은 아래 이곳을 참조하였습니다.

[참조] http://logos.cs.uic.edu/366/notes/mips%20quick%20tutorial.htm


1. C소스

몇 줄안되지만 리버싱해볼 프로그램을 먼저 살펴보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
//hello.c
#include <stdio.h>
 
int main(int argc, char *argv[]){
 
    printf("argc    = %d\n", argc);
    printf("argv[0] = %s\n", argv[0]);
    printf("argv[1] = %s\n", argv[1]);
 
    return 0;
}




메인함수에서 argc와 argv들을 출력해는 프로그램입니다.

./hello hi

[출력예시]

argc        = 2

argv[0]     = /root/hello

argv[1]     = hi


2. Disassemble Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
   0x00400640 <+0>:      addiu    sp,sp,-32
   0x00400644 <+4>:      sw     ra,28(sp)
   0x00400648 <+8>:      sw     s8,24(sp)
   0x0040064c <+12>:     move     s8,sp
   0x00400650 <+16>:     sw     a0,32(s8)
   0x00400654 <+20>:     sw     a1,36(s8)
   0x00400658 <+24>:     lui     v0,0x40
   0x0040065c <+28>:     addiu    v0,v0,2144
   0x00400660 <+32>:     move     a0,v0
   0x00400664 <+36>:     lw     a1,32(s8)
   0x00400668 <+40>:     jal     0x400500 <printf@plt>
   0x0040066c <+44>:     move     at,at
   0x00400670 <+48>:     lui     v0,0x40
   0x00400674 <+52>:     addiu    v1,v0,2160
   0x00400678 <+56>:     lw     v0,36(s8)
   0x0040067c <+60>:     lw     v0,0(v0)
   0x00400680 <+64>:     move     a0,v1
   0x00400684 <+68>:     move     a1,v0
   0x00400688 <+72>:     jal     0x400500 <printf@plt>
   0x0040068c <+76>:     move     at,at
   0x00400690 <+80>:     lui     v0,0x40
   0x00400694 <+84>:     addiu    v1,v0,2176
   0x00400698 <+88>:     lw     v0,36(s8)
   0x0040069c <+92>:     addiu    v0,v0,4
   0x004006a0 <+96>:     lw     v0,0(v0)
   0x004006a4 <+100>:    move     a0,v1
   0x004006a8 <+104>:    move     a1,v0
   0x004006ac <+108>:    jal     0x400500 <printf@plt>
   0x004006b0 <+112>:    move     at,at
   0x004006b4 <+116>:    move     v0,zero
   0x004006b8 <+120>:    move     sp,s8
   0x004006bc <+124>:    lw     ra,28(sp)
   0x004006c0 <+128>:    lw     s8,24(sp)
   0x004006c4 <+132>:    addiu    sp,sp,32
   0x004006c8 <+136>:    jr     ra
   0x004006cc <+140>:    move     at,at




gdb에서 확인한 Disassemble Code 전체 입니다. 부분부분 잘라서 확인해봅시다.


1
2
3
4
   0x00400640 <+0>:        addiu   sp,sp,-32
   0x00400644 <+4>:        sw      ra,28(sp)
   0x00400648 <+8>:        sw      s8,24(sp)
   0x0040064c <+12>:     move    s8,sp



첫 번째 라인을 보면 addiu sp,sp,-32 가 되어있습니다. addiu가 어떤놈인지 한번 볼까요?

addiu (add immediate unsigned) , 오버플로우 무시
[예시] addiu $s1, $s2, 100 => $s1 = $s2 + 100

위와 같이 addiu는 첫 번째 레지스터에 두 번째 레지스터와 세 번째 상수 값을 더하여 저장하는 명령어입니다.
따라서 addiu sp,sp,-32 는 sp = sp -32 정도로 표현해 볼 수 있겠네요.


sp는 느낌적인 느낌으로 stack pointer라는 것을 알 수 있죠?
따라서 이 한 줄에 의미는 스택의 크기를 32바이트만큼 증가 시키는, x86의 구문과 대비하면
sub esp, 0x20 와 같은 역할을 하는 놈이 될꺼에요.


두 번째 라인은 sw ra, 28(sp) 입니다. 너무 생소하기 때문에 하나씩 또 잘라서 봅시다.

sw (Store Word)

레지스터의 값을 메모리에 Word사이즈만큼(4byte) 저장하는 명령어 입니다.

여기서 주의 깊게 보아야할 부분은 

sw source_register, destination_memory 로 표현된다는 점이에요.


ra (Return Address)

말그대로 리턴 주소를 가지고 있는 레지스터입니다.


28(sp)

이런 표현은 sp + 28을 뜻합니다. +28을 스택포인터의 오프셋으로 사용해서 스택에 접근하겠다는 뜻이지요


그래서 종합적으로 두 번째 라인은 어떻게 해석되느냐!

현재 리턴 주소를 sp+28에 저장해라 ~ 라는 뜻이 되겠습니다.


세 번째 라인은 sw s8, 24(sp) 입니다.

두 번째 라인과 비슷한 형태로 저장할 값만 바뀌었네요, s8를 sp+24에 저장해라! 가 되겠죠?

s8은 mips에서 프레임 포인터 레지스터입니다. x86에서 ebp와 같다고 생각하시면 될거 같네요.


네 번째 라인은 move s8, sp에요! move는 말그대로 값을 이동시키는 명령어 이고 x86에서 mov와 같습니다.

그러니까 s8 = sp가 되겠죠?


그럼 종합적으로 위의 네 라인이 무슨 일을 했는지 봅시다!

스택의 크기를 증가 시켰고, 현재 리턴어드레스와 프레임 포인터를 메모리에 저장했습니다.

그리고 프레임포인터에 스택포인터 값을 넣어주었죠.


네, 맞습니다. 스택프레임을 구성하는 구문이지요? 익숙한 x86으로 보면

push ebp

mov ebp, esp

sub esp, 0x20

함수 시작과 동시에 이루어지는 이 것과 비슷하게 움직이고 있습니다.



이제 두 번째 부분을 볼까요?

1
2
3
4
5
6
7
   0x00400650 <+16>:    sw     a0,32(s8)
   0x00400654 <+20>:    sw     a1,36(s8)
   0x00400658 <+24>:    lui    v0,0x40
   0x0040065c <+28>:    addiu  v0,v0,2144
   0x00400660 <+32>:    move   a0,v0
   0x00400664 <+36>:    lw     a1,32(s8)
   0x00400668 <+40>:    jal    0x400500 <printf@plt>



여기서 새롭게 보이는 명령어는 lui, lw, jal 정도가 있네요? 나머지는 위에 본 것들라 수월할거에요

두 번째 부분 첫 째 라인은 sw a0, 32(s8) 입니다.

a0 ~ a3 레지스터는 서브 루틴을 호출 할 때 파라미터들을 저장하는 레지스터입니다.

그럼 자연스레, main함수의 파라미터들이 a0~a3에 위치하고 있다고 생각해볼 수 있겠네요,


여기서는 s8+32에 a0의 값을 저장하고 있네요, s8은 아까 위에서 ebp랑 같은 프레임 포인터라고 했죠?

직접 a0에 어떤 값이 들어 있는 지 한번 찍어보도록 합시다.

레지스터의 용도에 따르면 a0에는 메인함수의 첫 번째 파라미터인 argc가 들어 있어야할 거에요.


브레이크 포인트를 걸고 나서  hi라는 argument를 주고 실행을 한 후 값을 찍어보니까!


0x2가 나왔어요. 파라미터를 하나 주고(./hello hi) 실행 했으니까, argc가 2일테고... 오호, 그럼 a0에 argc(0x2)가 들어있는게 맞고, 

이 값을 스택에 저장해 주겠다는 뜻이 되겠네요.


같은 방식으로 a1의 값을 확인하면

argv[0]의 포인터가 들어있는 것을 알 수 있습니당


음? 그럼 a2에는 프로그램 실행 환경변수의 포인터 값이 들어 있어야 겠죠? 

확인해보니까 맞네요 ㅎ;;



여하튼! 다시 본론으로 돌아오면, 두 번째 라인에서는 s8+36에(스택에) argv[] 포인터 값을 넣어주었어요,,


세 번째 라인은 lui v0, 0x40 으로 lui연산을 하고 있습니다. lui연산은 해당 레지스터의 상위 두바이트에 값을 로드하는 명령어인데요,

따라서 v0 = 0x00400000 와 같이 표현해 볼 수 있겠습니다.


네 번째 라인에서는 addiu v0, 2144 이고,

v0 = v0+2144 니까

v0 = 0x00400000 + 2144

v0 = 0x400860 이 되겠습니다.


다섯 번째 라인에서는 move a0, v0

a0 = v0 이고, 네 번째 라인에서 연산한 결과를 a0에 넣어주고있어요, 0x400860에는 어떤 값이 들어 있는 것일까요?


오, 프로그램에서 처음으로 호출 하는 printf의 첫 번째 파라미터가 들어 있었네요,

위에서 a0~a3은 서브루틴에서 함수호출을 하기전 파라미터가 셋팅되는 레지스터라고 했었죠?

이제 printf를 호출 할 준비를 하나보네요.


여섯 번째 라인!

lw a1, 32(s8) 은 Load Word 명령어로 레지스터를 Destination으로 메모리에서 값을 로드해오는 명령어 입니다.

따라서, a1 = s8+32 가 될거고, s8+32에는 첫 번째 라인에서 값을 넣어준 argc가 들어 있을 거에요.


두 번째 부분의 마지막라인인 일곱 번째 라인은

jal 0x400500 <printf@plt> 네요, jal은 옆에 <printf@plt>라고 써있는 걸 보고 눈치 채셨겠지만,

x86에서 call과 비슷한 명령어 입니다. Jump and Link이고, 동작은 해당 함수 주소로 Jump! 하고 다음 명령어 주소를

ra레지스터에 셋팅해줘서 Link하는 명령어 입니다.


printf를 호출 하는데 첫 번째 파라미터에는 a0의 값 "argc     = %d\n"

두 번째 파라미터에는 a1의 값 0x2를 가지고 호출을 하게 되겠지요!


두 번째 이하 부분도 비슷한 형식으로 printf를 호출 하고 있기에 설명은 생략하겠습니다.


혹시 포스팅 중 부족한 부분이나, 틀린 부분은 피드백 주시면 감사하겠습니다. 질문도 좋아요~ :)



'Study > IoT' 카테고리의 다른 글

MIPS 리버싱 기초-2(if, bof)  (0) 2017.09.14
firmware 분석 환경 구축하기  (11) 2017.09.06

+ Recent posts