리눅스환경에서 실행됩니다
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 와 printf 는 rax 를 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 문처럼 분기를 할 수 있게 만들어야 합니다.
위 사이트에서 c언어 한땀한땀 써보면서, 실시간 어셈블리를 볼 수 있습니다.
만약 ld 하고 실행하는데 에러가 나오면, 아무 바이너리파일이나 ldd 명령어로 확인해서
ld 라이브러리를 바꿔주시면 됩니다 (ld 명령어 옵션 (위에잇음))