본문 바로가기
Reversing

[Reversing] (Assembly) 어셈블리프로그래밍 - 별찍기

by snwo 2021. 5. 31.

리눅스환경에서 실행됩니다

 

nasm 컴파일러를 이용해 컴파일 할껍니다.

 

sudo apt-get install nasm
nasm -f elf64 파일명.asm -o 파일명.o 	#64bit 파일
nasm -f elf 파일명.asm -o 파일명.o	#32bit 파일

ld 파일명.o -o 파일명 -lc-dynamic-linker /lib64/ld-linux-x86-64.so.2

(ld 파일은 되는걸로 골라서 쓰자. 저게 안될수도 있다.)

 

섹션

우리가 만들 ELF 파일 섹션헤더에는 많은 헤더가 있는데,

.data, .text 섹션이 핵심이다.

.text 는 우리가 작성한 코드가 컴파일되어 들어가고

.data 에는 초기화된 전역변수,

.bss 에는 초기화되지 않은 전역변수 등이 들어간다.

 

nasm 으로 코딩할 때, 

전역변수인 a 는 초기화되지 않았기에 .bss 에 저장,

문자열상수인 "%d" 는 data 섹션에 넣고,

main 함수의 코드는 text 섹션에 넣으면 됩니다.

시스템 콜

64bit 바이너리에서는 syscall 로 시스템 콜을 하고

32bit 바이너리에서는 int 0x80 으로 시스템 콜을 합니다.

chromium.googlesource.com/chromiumos/docs/+/HEAD/constants/syscalls.md

위 사이트에서 어떤 함수들이 있는지 확인할 수 있습니다. (x86_64 는 syscall, x86 은 int 0x80)

 

어셈코딩 실력을 늘리기 위해, 입력을 받을 때는 scanf 로 받고,출력할 때는 write syscall 로 출력해 봅시다.

코딩

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main(void){
    int i,j,n;
    scanf("%d",&n);
    for(i=0;i<n;i++){
        for(j=0;j<=i;j++){
            printf("*");
        }
        printf("\n");
    }
}
cs

 

별찍는 간단한 코드입니다.

printf 함수는 write(1,"*",1); 또는 write(1,"\n",1); 으로 대체합시다.

 

작동 잘되구요, 이제 어셈으로 바꿔봅시다.

 

어셈 명령어

 

주로쓰게될 명령어는 mov, cmp, jmp (ja, jae) 이정도가 되겠네요.

 

mov (dest), (src) : 첫번째 오는 operand 에 두번째 operand 를 저장합니다.

cmp (operand1), (operand2) : 두 개의 operand 를 비교해서 flag 를 설정한다. 이 flag 를 보고, jmp 할지 말지 결정한다.

 

ja (레이블) : a>b 이면 레이블로 점프한다.

jae (레이블) : a>=b 이면 레이블로 점프한다.

 

시스템콜 호출인자

write(1,"*" 이나 "\n" 의 주소,1) 이렇게 호출하면 printf 처럼 화면에 출력해준다.

 

첫 번째 인자는 FD 로, 표준 출력1로 줘야 콘솔에 출력되고 : rdi = 1

 

두 번째 인자는 buf 로, 문자열의 주소를 준다. : rsi = "*" 또는 "\n" 의 주소

 

세 번째 인자는 count 로, 문자열의 개수를 준다. (1) : rdx = 1

 

rax 는 write 의 syscall number 인 1 로 설정

 

 

변수

 

1
2
3
4
segment .data
   star:    dw '*'
    enter:    dw '\n'
    d_fmt:    db "%d",0
cs

 

data 섹션에 문자열 상수인 * 와 개행문자 \n 을 선언한다.

dw -> define word 로 2바이트를 사용한다. 0x00XX 이렇게 저장되어 문자열이 null 문자로 끝나게 된다.

(xx 는 문자의 아스키코드값)

 

db -> define byte 으로 1바이트를 사용한다. 문자열을 선언할 때, db 로 선언한다고 한다. 마지막에 0 을 붙이는건

문자열이 null 로 끝나기 위함이다.

 

segment 와 section 은 같다고 한다. 암거나 써도 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
segment .text
global _start
_start:
    push rbp
    mov rbp,rsp
    sub rsp, 0xc
roop:
    ;외부 반복문
roop2:
    ;내부 반복문
end:
 
    add rsp,0xc
    leave
    ret
cs

 

가장 먼저 호출되는 함수인 _start 에 써주자.

 

push rbp; mov rbp,rsp ~~ leave; ret 으로 스택 프레임을 형성하고,

 

sub rsp,0xc 로 변수 세 개를 사용하기위해 스택을 뺀다.

 

add rsp,0xc 로 마지막에 다시 스택을 정리한다.

 

그리고 roop, roop2, end 레이블로 i 사용하는 반복문, j 사용하는 반복문, 반복문 밖에 대한 레이블을 지정해준다.

(jmp 레이블 이렇게 사용)

 

지역변수는 rbp ~ rsp 범위에 있게 된다. rbp > rsp 이니까, rbp 에서 4 바이트씩 빼서 변수를 사용하자 

(int 는 4byte)

 

rbp-0xc, rbp-0x8, rbp-0x4 -> 차례로 n,i,j 이렇게 사용할 것이다.

 

extern scanf

코드 맨 위에 이렇게 작성해야 scanf 함수를 사용할 수 있다.

scanf("%d",&n) 이므로, rdi="%d" 의 주소, rsi = n (rbp-0xc) 의 주소

그리고 scanf 와 printfrax 를 0으로 세팅하고 호출해야한다.

 

n의 값이 아니라, n 의 주소를 전달해야 하므로,

lea 명령어를 사용한다.

mov 와 차이점

mov rsi, DWORD PTR [rbp-0xc]

lea rsi, [rbp-0xc]

 

위 둘은 다르다. 먼저 [] 대괄호를 사용하면, InDirect 모드로 메모리를 참조한다.

rbp-0xc 값 (0x1234) 이 아닌, rbp-0xc 값 (0x1234) 가 가리키는 위치에 있는 값(0x79)을 가져오는 것이다.

 

이 때, 대괄호 앞에 가져올 값의 크기를 지정해줘야 한다.

int 는 4byte  이므로, DWORD PTR [rbp-0xc], 근데 우리는 NASM 문법이므로, PTR 은 지우고

DWORD [rbp-0xc] 이렇게 참조한다. 하지만 lea 명령어는 값을 참조하지 않기 때문에 DWORD 를 붙이면 안된다.

 

다시 본론으로 돌아오면, mov 명령어 rsi 에 0x79 값이 들어가겠지만,

lea 명령어는 역참조를 하지 않고, 대괄호 안의 값을 rsi 에 넣을 것이다 (0x1234)

 

 

이제 왜 lea 를 쓰는지 알겠죠?

 

1
2
3
4
    lea rsi, [rbp-0xc]
    mov rdi, d_fmt
    mov rax,0
    call scanf
cs

이렇게 scanf 함수를 호출합니다.


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
37
38
39
40
41
42
43
44
45
46
47
48
49
extern scanf
 
segment .data
    star:    dw '*'
   enter:    dw 0x0a
    d_fmt:    db "%d",0
 
segment .text
global _start
 
_start:
    push rbp
    mov rbp,rsp
    sub rsp, 0xc
and rsp,0xffffffffffffff00
    lea rsi, [rbp-0xc] ;n
    mov rdi, d_fmt
    mov rax,0
    call scanf
    mov DWORD [rbp-0x8],0 ; i
roop:
    mov eax, DWORD [rbp-0xc]
    cmp [rbp-0x8], eax
    jae end
    mov DWORD [rbp-0x4],0
roop2:
    mov eax,DWORD [rbp-0x8]
    cmp DWORd[rbp-0x4],eax
    jg roop_enter
    mov rdx,1
    mov rsi,star
    mov rdi,1
    mov rax,1
    syscall
    add DWORD [rbp-0x4],1
    jmp roop2
roop_enter:
    mov rdx,1
    mov rsi,enter
    mov rdi,1
    mov rax,1
    syscall
    add DWORD [rbp-0x8],1
    jmp roop
 
end:
    add rsp,0xc
   mov eax,60
xor rdi,rdi
syscall
 
cs

 

 

+ 코드수정, 64bit 컴파일 할 때, rsp 하위 0.5 바이트를 0으로 설정해줘야 에러가 안난다.

그래서 and 명령어 위에 추가

+ 그냥 return 하면 segfault 나서, exit 으로 종료해야한다.

 

(일반적인 바이너리 보면, __libc_start_main 에서 main 함수를 호출하고,

exit 까지 해주기 때문에 segfault 가 안난다. 근데 여기서는 사용하고있지 않아서

직접 exit 해준다.)

 

급발진해서 코드를 다 짜버렸습니다.

레이블을 이용해 c 언어 if 문처럼 분기를 할 수 있게 만들어야 합니다.

 

godbolt.org/

 

Compiler Explorer

 

godbolt.org

위 사이트에서 c언어 한땀한땀 써보면서, 실시간 어셈블리를 볼 수 있습니다.

만약 ld 하고 실행하는데 에러가 나오면, 아무 바이너리파일이나 ldd 명령어로 확인해서

ld 라이브러리를 바꿔주시면 됩니다 (ld 명령어 옵션 (위에잇음))

 

성공