[리버싱] 어셈블리 기초 (1): https://slumpdev.tistory.com/entry/%EB%A6%AC%EB%B2%84%EC%8B%B1-%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC-%EA%B8%B0%EC%B4%88
[리버싱] 어셈블리 기초 (2): https://slumpdev.tistory.com/entry/%EB%A6%AC%EB%B2%84%EC%8B%B1-%EC%96%B4%EC%85%88%EB%B8%94%EB%A6%AC-%EA%B8%B0%EC%B4%88-2
어셈블리 기초에서 어세블리의 기본 문법과
에코 프로그램 구현 및 반복문 구현을 공부했다.
위에서 배운 내용을 토대로
구구단 출력 프로그램을 구현해보면서
중첩 반복문을 어셈블리로 구성하는 방법에 대해 공부해보려 한다.
✔️ 어셈블리로 구구단 구현하기
참고1 (Linux system call table) : https://faculty.nps.edu/cseagle/assembly/sys_call.html
참고2 : https://youtu.be/2wFN9BG3wWw
참고3: https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md
참고4: https://colinch4.github.io/2021-06-11/Calling_Convention/
참고5: https://cccding.tistory.com/23
참고6: https://itsaessak.tistory.com/m/243
참고7: https://velog.io/@jnary/Assembly-%EB%8D%A7%EC%85%88%EB%BA%84%EC%85%88-%EC%9D%8C%EC%88%98
어셈블리로 구구단 구현을 위해 많은 사이트를 참고했다.
우선 Linux system call table과 친해지기 위해서 수시로 참고했고,
다른 여러 사이트 들에서 어셈블리로 프로그램을 구현하는 방법과
어셈블리의 연산 과정이 어떻게 이루어지고 어떤 결과를 가져오는지
공부하고 또 직접 노트에 써가면서 계산해보고 코드를 바꿔보면서
다양한 방법으로 시도했다. 그리고 그 과정에서 카카오 서버가 화재나면서
동시에 티스토리가 퍼-엉 하는 사태가 나는 바람에 글을 이제야 올린다.
이전 글을 참고해가면서 반복문으로 구현하다가 안돼서
다른 사이트를 참고해서 직접 코드를 쳐보고 수정해보고 고쳐보는 방식으로
공부하면서 구현해보았다.
위에서부터 차근차근 살펴보게 되면,
메모리의 구조에서 bss, data, text 그리고 스택과 힙이라는 것이 존재한다.
실제 코드가 구현되는 text 부분부터
상수에 대해서 정의를 해주는 data 부분과
초기화되지 않은 영역에 변수를 정의해주는 bss 영역,
그리고 스택과 힙에 대한 영역으로 메모리 구조가 이루어져있다.
그래서 다음과 같이 .bss 영역에 i와 j와 ixj 라는 이름으로
resb는 1바이트 만큼을 이야기하는데 해당 크기만큼으로 선언해준다.
이외에도 resw는 2바이트를 의미하는 등이 존재한다.
그리고나서 data 영역에 우리가 출력할 문자를 가리키는 포인터를 선언하는데,
*, =, tab, linefeed 이렇게 4가지를 이용해서 구구단을 출력해보려고 한다.
밑에 ascii 코드 표를 보게 되면, 0x0a가 Line Feed를 의미하고
0x09가 HT라고 Horizantal Tab을 의미한다.
그 다음 text 영역에 코드가 시작되는 부분을 의미하는 _start를 담아주고,
_start 부터 메인 코드를 작성하기 시작한다.
우선 잘 사용하지 않는 레지스터인 r10에 ascii로 된 값 '1'을 넣어준다.
아스키 코드는 다음 사진이 깔끔해서 주로 참고했는데,
뒤에 나올 값들이 hex 값으로 받는 것도 있고 dec으로 받는 것이 있다.
계속 참고해서 보면 된다!
반복문의 loop를 돌기 위해서는 rcx라고 하는 레지스터에 값을 담아주어야 하는데,
rcx는 카운터 레지스터로 보통 루프문을 사용할 때 같이 사용해주는 레지스터이다.
반복할 횟수인 9를 담아주고 그 다음 again1이라는 call 부분을 작성해준다.
i라는 상수에 값을 담아주기 위해서는 r10에 값 1을 가져와서 mov한다.
직접 i에 접근해서 mov 명령어로 값을 담아주게 되면
위와 같이 연산 크기가 정해지지 않았다는 에러를 발생하게 되므로
r10에 담아준 값을 가져와서 직접 i에 담아주게 되는 것이다.
그리고나서 기존 rcx를 남기기 위해서 push를 하고,
동일한 작업을 수행해주면서 이번에도 잘 사용하지 않는 r9 레지스터에
'1' 값을 담아준다. 그리고 again2라는 로직을 통해 구구단을 출력한다.
again2에도 push rcx를 하게 되는데, 지금까지의 구조로 보게 되면
c언어에서 for(int i=0; i<9; i++) { for(int j=0; j<9; j++) } 와 같은 형태처럼도
볼 수 있을 것이다. 다음은 again2에서 문자들을 출력하기 위해 각각의 함수에 대해
call 호출을 통해서 들어갔다가 나왔다가를 반복하게 된다.
이전 글에서도 볼 수 있듯이 call을 통해 호출된 함수에 들어갔다가
RET 를 통해 복귀 주소로 다시 나오게 되는 과정을 볼 수 있었는데
그 과정을 반복하면서 프로그램을 실행시켰을 때 원하는 문자를 출력하기 위해
다음과 같은 과정을 수행하는 것이다.
마지막에는 r9를 증가시키면서 기존에 push로 집어넣은 rcx가 pop 명령을 통해
지정한 곳인 rcx로 전달되는 것을 확인할 수 있다.
9번의 반복, 한 사이클이 끝나게 되면 개행을 출력하면서 다음 줄로 넘어가게 되고,
r10을 증가시키면서 다시 again1부터 반복하여 다음 숫자의 구구단 연산을 수행하게 된다.
이제 출력하는 함수들을 살펴보면, eax에 먼저 4를 담아주는 모습을 확인할 수 있다.
이전에 rax에 1을 담아주면 write을 했던 것처럼 연산을 수행하여 값을 출력하기 위해
eax에 4를 담아주어 sys write에 해당하는 syscall을 불러오게 된다.
ebx에 unsigned int 형으로 1 값을 넣어주고,
ecx에 문자로 무엇이 들어갈지 선언해준다.
나머지 출력 함수도 해당 부분 외에 모두 동일한데,
출력하고 싶은 값은 j, star, i, equal, ixj, tab, lf에 대해서 값을 넣어주게 된다.
그리고 edx에 1만큼의 크기를 사용한다고 값을 넣어준다.
마지막에 나오는 int 0x80 은 인터럽트 제어를 의미하며,
해당 사진을 보면 Programmed exception for system calls라고 되어있다.
즉, eax에 4를 담아주면서 불러온 sys write에 대한 system call을 종료(중지)하는 것이다.
그리고나서 복귀 주소로 복귀하기 위한 ret를 작성해준다.
다른 부분은 모두 동일하기 때문에 넘어가서 printIxJ 함수를 살펴보려고 한다.
해당 함수는 사이트를 참고해서 동일하게 만들었음에도 불구하고 어려워서
직접 노트에 계산해가면서 공부하였다.
연산을 위한 레지스터와 해당 레지스터에 연산을 수행하기 위한 베이스 레지스터를
가져오게 되는데, 각각 '1'이라는 값을 받는다. ascii 1이라는 값은 hex값으로 0x31을 뜻하는데,
sub 연산을 수행하면서 모두 1로 초기화되는 것을 확인할 수 있다.
이 과정에 맞는 ascii 혹은 십진수를 넣어줘야지 만약에 '1'에 쌩으로 0 값을 sub하게 되면
segemetaion fault라던지 floating point exception 이라던지 하는 오류를
nasm 컴파일 할 때 마주할 수 있다.
그리고 나서 bl에 mul을 수행해주는데, mul은 위에서 참고한 사이트에서와 같이
mul al, bl을 하게 되면 ax에 값이 담아지게 되어 1이라는 값이 담아지게 된다.
그 다음 중요한게 10 값을 받아준 bl로 나눗셈을 하게 되는데,
이 과정에서 출력 값이 정해진다고 보면된다.
div 연산 같은 경우는 ah에는 ax % 10에 대한 나머지가 들어가고,
al에는 ax / 10에 대한 몫이 들어가게 된다.
예를 들어서 5x5 연산의 값이 25인데, 10으로 나눈 몫은 2가 되고 나머지는 5가 된다.
즉, AL에 2가 들어가고 AH에 5가 들어가서 ixj 주소에 2, ixj+1 바로 윗 공간에
5가 들어가는 것으로 이해하였다. 그렇게 ixj와 ixj+1 공간만큼인 ixj를 출력하면
25가 정상적으로 출력되는 것을 확인할 수 있다.
div 값을 9로 주었을 때의 출력 결과이다.
똑같이 5x5를 예시로 들었을 때의 값인 25를 9로 나누게 되면,
25를 9로 나눈 몫 2와 나머지 7이 각각 AL과 AH에 들어가게 되어
결과적으로 ixj 공간에 2와 7이 들어가서 27이라는 값을 잘못 출력하게 된다.
그 과정에서 몫이 0이 되면 나머지 자체를 출력하면 되기 때문에 cmp 비교 문과
je(jump equal)로 향하게 된다.
이렇게 수행되고 나면, 다음과 같이 정상적으로 구구단을 출력할 수 있게 된다.
✔️ 매크로를 사용하여 구구단 코드 줄이기
참고: https://velog.io/@kyoung99u/Assembly-%EB%A7%A4%ED%81%AC%EB%A1%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0
코드를 줄일 수 있는 방법이 없을까하여 찾아보던 중
매개변수를 받아 사용할 수 있는 어셈블리의 매크로를
해당 사이트를 참고하여 공부하면서 코드를 줄여보았다.
일단 기존 코드의 문제는 같은 내용의 출력 함수를 여러개 선언하는데,
다 다른 함수의 위치로 갔다가 복귀하는 방법이 같고
해당 내용의 ecx 값만 다르다는 것을 확인할 수 있기 때문에
매개변수를 받아서 매개변수만 다르게 하는 함축 코드를 작성하고 싶었다.
기존에 같은 함수를 text 영역에 macro로 선언하여 매개변수를 1개 받아
나머지 j부터 lf까지 매크로로 받아주도록 변경하였다.
이 과정에서 매크로는 %macro ~ %endmacro 로 선언해주면 되고,
%macro 뒤에는 매크로의 이름과 받을 매개변수의 개수를 작성하고
로직에서는 매개변수의 순서를 %1, %2 등과 같이 코드를 구성할 수 있다.
아 그리고 기존에 loop로 반복해주었던 구문을 작성하면,
기존 구문과는 다르게 loop가 127바이트 이상으로 점프할 수 없다는
short jump out of range라는 오류가 발생하기 때문에
dec ecx, jnz again(1,2) 라고 작성해주어서 정상적으로 컴파일을 수행하면 된다.
(참고: https://stackoverflow.com/questions/12136480/short-jump-out-of-range)
여러 사이트를 참고하다보니 잘못된 개념이 섞인 경우도 있었고,
잘못된 개념으로 계산을 하다가 시간을 잡아먹기도 했다.
그리고 제대로 이해한게 맞는지 아닌지는 잘 모르겠지만
해당 공부 내용을 통해서 조금 더 어셈블리에 익숙해지고,
프로그램이 돌아가는 원리와 과정에 대해서 이해할 수 있었다.
화이팅 💪
'보안 > 리버싱' 카테고리의 다른 글
[리버싱] 어셈블리의 재귀 (0) | 2022.10.17 |
---|---|
[리버싱] 어셈블리 기초 (2) (0) | 2022.10.14 |
[리버싱] 어셈블리 기초 (1) (0) | 2022.10.14 |
[리버싱] 레지스터와 스택 프레임 (0) | 2022.10.12 |
리버싱 기초 공부. 01 (0) | 2021.09.29 |