안녕하세요 마무입니다! 이번 포스트는 "어셈블리어란?", "어셈블리어를 배우는 이유", "16진수 사용하는 이유", "쉘코드란"에 대해서 자세히 알려드리려 합니다.
-----목차-----
1. 16진수를 쓰는 이유
2. 어셈블리어를 배우는 이유
i) 쉘코드란
3. 총 정리
---------------
입니다.
다른 정보들은
리눅스 독학 페이지 : https://mamu2830.blogspot.com/p/blog-page_13.html
네트워크 독학 페이지 : https://mamu2830.blogspot.com/p/blog-page_15.html
에 있을수도 있습니다!
1. 16진수를 쓰는 이유
기계어와 2진수, 16진수, CPU 아키텍처를 설명하게 앞서 이해를 돕기 위해 gcc란 아주 유명한 리눅스 컴파일러를 이용해 C언어 코드를 컴파일하고 설명해보겠습니다.
자신이 이미 컴파일한 프로그램이 있다면 그걸 사용해도 되지만, 이 포스트를 보는 초심자분들은 당연히 없을테니 제가 밑에 적은 Hello world 코드를 따라하시고 컴파일하시면 됩니다.
참고로 현재 제 운영체제는 'Kali'며 버전은 밑에 사진과 같습니다.(굳이 똑같은 버전이 아니여도 됩니다, 그냥 참고만 하세요)
이렇게 매우 간단한 C언어 코드를 말이죠!
그리고 이렇게 "gcc first.c"만 실행해보면
이렇게 "a.out"이란 기계어 파일이 생깁니다.
이 이름은 소스코드를 컴파일할 때 파일이름을 지정하지 않으면 생기는 기본 이름입니다.
리눅스를 사용하시다 보면 은근 많이 접하게 되는 디폴트 이름이죠!
프로그램 이름을 지정하고 싶으면
gcc [소스코드.c] -o(output) [원하는 이름]
이렇게 하시면 됩니다.
저는 "gcc first.c -o first"이렇게 만들어보겠습니다.
사진을 보시면 사이즈도 완전히 똑같고 "a.out"과 "first" 이름만 다른 걸 알 수
있습니다.
이제 만들어진 기계어 파일(프로그램)의 구조가 어떻게 돼 있는지 확인해볼텐데요
평범한 파일처럼 vim이나 more, tail, head 등 문자열을 읽는 명령어를 사용하면 2진수로 이루어진 기계어이기 때문에
이렇게 확인할 수 없습니다.
이러한 기계어를 확인할 수 있는 리눅스 명령어들이 다양하게 있지만, 그 중 우린 제일 유명한 'objdump'란 프로그램을 사용해볼 겁니다.
한번
objdump -D(Disassemble-all) [프로그램 이름]
이렇게 실행해보겠습니다.
-D(Disassemble-all)이란 뜻처럼 엄청나게 많은 어셈블리어와 16진수로 표현된 기계어가 모두 나옵니다.
헉! 우리가 가볍게 작성한 헬로워 월드 5번 반복 출력 프로그램이 저정도로 많은 어셈블리어로 변한다니....
라고 생각했다면 음.. 절반만 맞췄습니다.
사실 저 엄청난 코드 중에서 대부분은 저희가 만든 'first'란 프로그램을 실행시키 위해 세팅돼 있는 코드들입니다. 실제로 저희가 <main>에 구현한 코드는 <main>: 이렇게 따로 있을겁니다.
스크롤을 내리며 찾아도 되지만, 당연히 비효율적이라 앞으로는 리눅스에서 가장 유용한 명령어 중 하나인 'grep'을 같이 사용해서
objdump -D(Disassemble-all) [프로그램] | grep -A(After text number) [숫자] main.:
이렇게 사용할 겁니다.
여기서 main.: 에서 '.'은 정규표현식의 .를 의미하며, 아무 한자리 문자를 의미합니다
예를 들어
main1:
main2:
mainc:
main!:
main>:
.....
이런식으로 말이죠.
그래서
"objdump -D first | grep -A 20 main.: "이란
first라는 프로그램에 관련된 모든 기계어를 16진수와 어셈블리어로 표현해주고, 그 내용중에서 main.: 조건에 충족하는 문자열 다음부터(After text number) 20줄을 읽어라
라는 뜻입니다.
한번 실행해볼까요?
제가 위에서 설명한 것처럼 결과가 나온 걸 볼 수 있습니다!
아무 한 문자라는 정규표현식 '.'에 충족하는 것이 main>이 있어서 그 결과를 보여준 걸 알 수 있죠.
이제 시각적 자료를 모두 구했으니 본격적으로 기계어, 2진수, 16진수, 어셈블리어 등등 자세히 설명해보겠습니다!
컴퓨터 공학을 공부하다보면 한번 쯤 가지게 되는 원초적인 의문이 있습니다
1. 컴퓨터는 ON, OFF, 즉 2진수를 사용할 수 밖에 없다는 것은 알겠는데 왜 대부분의 컴퓨터 공학에서 16진수로 사용할까?
2. 기계어는 2진수라고 알고 있는데, 왜 네트워크나 시스템에서 데이터를 보면 다 16진수지? 16진수도 기계어인가?
이 두가지의 의문을 해소할 수 있는 대답은
1바이트(8비트)를 2진수로 표현하면 총 8개의 0과 1이 나오는데, "1234567890"이란 10개의 숫자만 문자열로 처리한다 해도 2진수로 표현하면 총 80비트(80개의 0과1)가 나온다는 것이죠.
그럼 우리가 일상에서 쓰는 글들은 2진수로 읽으려면 얼마나 내용이 길지 상상이 안가죠...?
그래서 우린 16진수를 사용 하는 겁니다. 왜냐면 1글자를 2진수로 표현하면 8개의 자리가 필요하지만, 16진수로 표현하면 겨우 2개의 자리로 압축되기 때문입니다.
그래서 우리가 위에서 objdump를 했을 때 나오는
를 보면 나오는 55, 48, 89, e5...와 같은 숫자들이 모두 1바이트를 표현한 16진수라는 것을 알 수 있죠.
이처럼 보기 편하기 위해서 16진수라는 것을 컴퓨터 분야에서 사용하는 것이죠.
위 objdump 결과 맨 왼쪽에 나오는 113a: 113d: 1141: 114a: ... 이것들도 모두 메모리의 위치를 16진수로 표현한 겁니다. 왜냐? 2진수로 표현하면 도저히 읽기 힘들정도로 길어지기 때문이죠.
32비트 프로세서(CPU)는 2의 32승(0~4,294,967,296)개의 메모리 주소를 사용할 수 있고,
64비트 프로세서(CPU)는 2의 64승(1.84467441 x 1019)개의 메모리 주소를 사용할 수 있는데요 32비트 프로세서만 해도 4,294,967,296개의 메모리 주소를 2진수로 표현하면... 왜 읽기 힘들정도로 길어진다는 지 아시겠죠? ㅎㅎ
그래서 정리하자면
당연히 실제 기계어는 2진수지만 가독성을 위해 16진수로 표현한 것이다, 프로그램에서 16진수로 표현해도 기계어다.
2. 어셈블리어를 배우는 이유
자 그럼 한번 쯤 갖는 원초적인 의문이 또 있죠.
1. 왜 시스템 해킹을 할 때 어셈블리어를 공부해야하나?
이에 대한 대답은
위에서 우리는 가독성을 위해 2진수 기계어를 16진수로 표현한다고 했습니다.
그러나 당연히 16진수도 보자마자 뜻이 이해가 안되죠? 그래서 각 CPU회사들은 CPU가 사용하는 16진수(진수) 기계어에 1대1로 매칭되는 '어셈블리어'를 만들어서 사용합니다.
위에서 준비한 objdump 사진을 보시면
55 // push %rbp
48 89 e5 // mov %rsp, %rbp
....
c9 // leave
c3 // ret
이렇게 특정 16진수와 어셈블리어가 매칭되는 것을 볼 수 있죠!
이렇듯 어셈블리어는 16진수와 어셈블리어가 대부분 1대1로 매칭되기 때문에, 언어에 구애받지 않고 직접적으로 CPU가 어떤 활동을 하는 지를! 인간이 보고 이해할 수 있습니다
우리가 사용하는 수 많은 컴파일 프로그래밍 언어(C, C++, java)들은(python, javascript와 같은 스크립트 언어도 결국은 계어로 CPU에 전달돼 실행됩니다) 결국 컴파일러가 프로그래밍 언어를 기계어로 변경해주는 데요, 위에서 말했듯이 기계어는 1대1로 어셈블리어 변경이 가능하기 때문에 프로그램의 기능을 확인할 수 있습니다
정말 사기 기술이죠!! 보안을 위해 오픈소스가 아닌 이상 회사는 프로그램 코드를 비공개합니다. 제 3자인 경우 프로그램 소스코드를 모르면 정확한 프로그램 알고리즘을 이해할 수 없죠
물론!!! 정확한 소스코드는 모르지만 그 소스코드가 기계어로 변한 뒤 CPU에 지시하는 행위를 우린 어셈블리어로 변경해서 이해할 수 있기에 이론상 같은 기능을 하는 다른 소스코드를 만들 수 있죠
그래서 이러한 어셈블리어의 특징을 이용해서 생겨난 분야가 바로 기계어로 된 코드를 어셈블리어로 변경 후 원 소스코드를 비슷하게 구현하는 기술이 바로 '리버싱'이며
위에서 설명했듯이 '소스코드는 모르지만, 그 행위를 알 수 있다'란 장점 때문에 시스템 해킹을 할려면 당연히 알면 좋죠!
물론 개발자 스스로가 만든 프로그램이 이상 행위를 했을 때, CPU에서 어떻게 동작하는 지를 알기 위해 어셈블리어로 보고 이해 할 수도 있습니다(물론 오래 걸리고 귀찮기에 디버깅 툴을 사용할 겁니다)
i) 쉘코드란
중요한 것은 어셈블리어는 기계어로 1대1로 매칭되기 때문에, CPU에 직접적인 명령을 입력할 때도 중요하게 사용됩니다
무슨 소리냐? 해킹에 사용되는 코드들은 대부분 보안상 프로그래밍 언어나 시스템에서 막혀있습니다.
그렇지만 'CPU'에 직접적으로 명령을 내린다면...?
그렇죠 예상이 되실겁니다. 바로 기계어를 CPU에 직접 전달하면 되는거죠
그리고 이러한 기계어를 직접 인간이 외워서 만드는 건 당연히 너무 힘들고, 1대1로 매칭되는 어셈블리어로 해킹 코드를 짜서 기계어로 변경해 사용하는 겁니다.
이러한 해킹 코드를 '셸코드(보통 셸을 얻기 위해 사용하는 코드라해서)'라고 합니다.
3. 총 정리
i) 리눅스에서 gcc를 이용해 컴파일 하는 방법은
gcc [소스코드.c] -o(output) [원하는 이름]
ii) objdump를 이용해 프로그램의 어셈블리어나 기계어를 쉽게 확인하는 명령어는
objdump -D(Disassemble-all) [프로그램] | grep -A(After text number) [숫자] [코드 문자열]:
iii) 기계어는 2진수 아닌가? 왜 16진수로 표현되는가? 왜 16진수를 사용하는가?
기계어는 2진수지만 가독성을 위해 16진수로 표현한 것이다, 프로그램에서 16진수로 표현해도 기계어다.
iv) 어셈블리어를 왜 공부하는 가?
기계어와 어셈블리어는 1대1로 매칭되기에 소스코드를 몰라도 그 기능을 알 수 있으며, 해킹할 때 쓰이는 코드는 프로그래밍 언어나 OS에 막혀있기에 어셈블리어를 통해 기계어 코드를 만들어 CPU에 전달한다
그리고 그 해킹용 기계어 코드를 쉘(셸)코드라 한다
포스트를 읽어셔서 감사하며! 도움이 되셨다면 따뜻한 댓글 및 팔로우 클릭을 해주시면 포스트 퀄리티 향상에 큰 도움이 됩니다!
다음에는 본격적으로 어셈블리어 언어에 표현되는 명령어와 레지스터 종류, 메모리 주소등에 대해서 다뤄보겠습니다!
댓글 없음:
댓글 쓰기
#1 여러분들이 소중한 시간을 투자해 달아주시는 따뜻한 댓글들은 저에게 정말 큰 힘이 됩니다!
#2 저의 각 포스트들은 엄청난 노력과 시간 투자를 통해 만들어진 포스트들로, 무단 복제나 모방하는 것을 금지합니다.
#3 저의 포스트에도 틀린 정보가 있을 수도 있습니다. 그럴 경우 친절한 말투로 근거와 함께 댓글로 달아주시면 정말 감사하겠습니다!