IT 그리고 정보보안/Knowledge base

기계어코드 작성

plummmm 2021. 4. 15. 21:23
반응형

BOF 공부를 할 때 가장 신경쓰이고 이해하기 어려웠던 부분이 바로 셸코드(Shell code) 이다.

보통 처음에 공격 실습을 할 때에는 미리 작성되어있는 셸코드를 주면서 nop 갯수 맞추고 셸코드를 알맞게 넣어야 함

 

도대체 이 알 수없는 기계어들은 무엇이고, 왜 집어넣어야 하는지?

그리고 이걸 사람이 작성한 것이 맞는지, 맞다면 어떤 방식으로 작성을 하였고

왜 굳이 기계어로만 이루어져야 하는지.

 

BOF 처음 공부하는 초심자라면 누구나 이에 대한 의문이 생기지 않을까.

오늘은 그 의문에 대한 해답을 찾기 위해 셸코드 작성 방법에 대해 포스팅 하려고 함.

  

일단 셸코드는 말 그대로 셸을 따내는 코드이다.

기계어로 제작이 되어 있는데, 우린 지금 기계어 작성법도 모르는 상태이다.. 그러므로

셸코드 작성법을 공부하기 전에 일단 먼저 기계어 작성법 부터 공부를 해보자.

 

IT 분야 처음 공부하면 단연 C언어를 배우기 마련이다.

C언어에서 가장 기본적으로 배우는 것이 뭔지 다들 알 것임.. Hello world!

 

우리가 처음 배울 때는 printf 함수를 사용하여 출력하는 것을 배웠는데,

사실은 printf 함수는 내부적으로 write()라는 시스템콜 함수를 사용한다.

즉, printf는 write 함수를 좀더 편리하게 사용하기 위한 라이브러리 함수이다.

 

우리는 기계어 코드를 제작해야 하므로, printf 보다는 

원론적인 write함수를 써서 출력해보겠다.

write함수 개형은 아래와 같음.

 

 

 

그럼 위 레퍼런스대로 작성해본다.

 

 

 

작성하고 gcc로 컴파일할 때 보면 평소랑 뭔가 다르다. -static 옵션이 들어가 있는데,

정적 링크로 바꾸어주는 옵션이다.

-static 옵션을 주어야 write 함수의 내부까지 들여다 볼 수 있다.

옵션을 안주면 라이브러리를 뒤져서 분석해야 함

 

이제 gdb로 디스어셈블을 해보자.

우분투로 진행하고 있었는데 gdb로 디스어셈블하니까 뭐 좀 다르게 나오더라.

그래서 레드햇 9.0 환경에서 다시 실습하였음.

 

 

 

메인함수의 프롤로그, 그리고 그뒤에 스택 프레임을 잡아주고

push 연산으로 write함수의 인자값을 받아오는 모습이 보인다.

<main+28>의 0x808ef48는 Hello world! 라는 문자열의 첫번째 주소일 것이다.

 

그럼 이제 write함수 안으로 들어가보자.

 

 

인터럽트로 시스템콜을 거는 부분이다.

write() 함수는 시스템콜이 사용되는 함수이다.

 

int 0x80 바로 위에 mov eax, 0x4 는 무엇일까?

바로 시스템 콜로 불러올 함수이다. 근데 4?

이건 시스템 콜 테이블에서 4번으로 설정되어 있는 write() 함수를 불러온다는 뜻이다.

/usr/include/asm/unistd.h 혹은 /usr/src/linux/include/asm-i386/unistd.h 파일에 기록되어 있다.

 

그리고 그 위에 mov명령이 연달아 나와 있는 건 write함수의 인자들이 레지스터로 복사되는 것이다.

 

 

 

mov eax, 0x4에 브레이크포인트를 주고 레지스터의 값을 확인하니

ecx에는 Hello world!\n

edx에는 15

ebx에는 1

이렇게 인자값이 ebx, ecx, edx 순서로 들어가 있었다.

이제 이걸 토대로 어셈블리로 코딩을 할 것이다.

 

잠깐.!!

기계어코드 작성할 때 AT&T의 문법을 따라야 한다고 한다. 나도 이거 적다가 알았다..

Intel로 작성했는데 하도 컴파일이 안되길래... 뭐가 문젠가 싶어 구글링을 하고 나서 알았다..

그래서 AT&T 방식으로 어셈블리코드를 다시 작성하였다. 

솔직히 연산 순서만 틀리지 다른거 별로 없다. 착오 없기 바람.

 

 

 

확장자가 [파일명].s 이다. 레지스터에 mov연산으로 들어가는 값들은 write 함수에 인자들이다.

이렇게 적고 컴파일해서 실행시키면 c언어로 짠 프로그램과 같은 결과가 나온다.

즉, 쓸데없는 군더더기를 제거하고 코어가 되는 부분만 걸러내어 다시 어셈블리 코딩을 하는 것이다.

 

여기서 나는 세그먼트 폴트 오류가 나지 않았는데.. 대부분 난다고 한다.

현재 작성되어있는 코드에는 리턴 어드레스 값이 설정되어 있지 않은데.. ret명령으로 

설정되어 있지도 않은 리턴어드레스를 참조하여 글로 쩜푸를 하려고 하니.. segment fault 오류가 뜬다고한다.

그래서 exit(0) 함수를 사용하여 정상종료할 수 있도록 해주어야 한다.

 

그러면 exit함수도 어셈블리 코드로 작성을 해야겠군.

위에서 한거처럼 하면 어려울 것 없다.

 

아까 eax에 시스템콜 함수의 고유번호가 들어갔었고

ebx, ecx, edx 순서로 인자가 들어갔었다.

exit()함수의 고유번호는 1번이다. 그리고

exit()함수는 exit(0) 로 인자를 1개만 가지므로 ebx에 0을 넣는 연산을 하면 될 것이다.

 

mov $0x01, %eax

mov $0x00, %ebx

int 0x80

 

을 아까 작성된 소스에 추가시키면 될 것이다.

 

요렇게.

그럼 이제 완성된 어셈코드를 가지고 기계어 코드로 변환을 할 준비가 되었다.

변환할 때는 objdump 를 이용해서 하겠음.

 

 

다른 부분은 우리가 볼 필요 없는 부분이고, main 함수 부분만 보면된다.

딱 화면을 보니까 뭐할 때 쓰는 녀석인지 감이 오지 않는가.

왼쪽에 나와 있는 부분이 기계어 코드이다. 

어 그럼 우리가 작성한 어셈코드 그대로 기계어로 나오는데.. 그럼 이제 기계어코드 작성이 끝났느냐?

이렇게 쉽게 되면 걱정도 안한다.ㅎㅎㅎ

 

자 다른 부분은 각설하고 2번 라인의 mov $0x80482f4, %ecx를 보자.

아까 어셈 코드 작성할 때 ecx 레지스터에 뭐가 들어간다고 했었나 ?

write 함수로 출력할 문자열이 저장된다고 하였다.

 

근데 objdump 뜬 결과는 고정된 주소값이 나와 있다.

주소값이 저렇게 고정으로 나와서 ecx에 저장되는게 의미가 있을까.

 

잘 생각해보자. 내가 셸코드로 요구하는 문자열이 코드가 삽입될 프로그램에 존재한다면,

문자열이 어디 저장되는지 대충 예상할 수 있을 것이다.

근데 그럴 가능성이 있나? 없다. 절대 저 주소에 hello world! 가 있을 턱이 없다는 말 ㅎㅎ

 

그래서 저 고정된 주소는 말그대로 쓸모없는 주소라 이말이다.

그럼 이제 우리는 대책을 마련해야 할 것이다.

 

어떻게?

 

문자열을 직접 스택에 push하고 문자열 시작 주소값을 ecx로 pop해주면 된다.!

이 때 call 과 jmp 명령을 사용한다.

 

일단 call 명령을 이용하여 다시 코드를 구성해 보겠음.

 

 

func 이라는 함수가 추가되었다.

왜 추가 시켰을까?

일단 call func 뒤에 실행될 명령은 .string " Hello world!\n" 일 것이다.

call 명령이 실행되면 ret가 일단 쌓일 텐데, 

그 ret는 .string " Hello world!\n" 로 인해 Hello world 문자열의 시작 주소가 될 것이다.

이해가 감? RET(리턴 어드레스)는 함수가 끝나고 다음에 실행될 부분이 들어있는 주소이므로

당연히 문자열의 시작 주소가 들어 있을 거라는 말.

 

CALL명령이 수행하는 것이 함수가 끝나고 실행될 부분의 EIP를 리턴어드레스에 저장하고 JMP를 수행하는 것이니까

위 코드에서 CALL을 수행하면 pop 연산이 나올 때 까지 ret가 스택 최상위를 가르키고 있을테지. 

(프롤로그도 수행하지 않았으니 SFP가 저장되어 있지도 않고)

그럼 자연스레 pop %ecx로 ret에 들어있는 문자열 시작 주소가 ecx에 저장이 될 것이다.

 

이렇게 하면 덤프떠도 고정된 주소로 애먹지 않는다.

 

그럼이제 다시 objdump를 뜬 결과를 함 보자.

 

 

??

또 문자열이 안보인다.

근데 main 부분에 call명령 밑에부분에 뭔가 잔뜩 적혀있다.

프로그램과 전혀 관련없는 이상한 어셈블리어들과 함께 

기계어 코드가 아래처럼 적혀있다.

 

48 65 6c 6c 6f 77 20 77 6f 72 6c 64 21 0a

 

이 부분을 아스키 코드를 보고 변환하면 원래 적었던 문자열이 나온다.

저기 나와 있는 전체 코드를 한번 나열 해보 겠음.

 

\xe8 \x0f \x00 \x00 \x00 \x48 \x65 \x6c \x6c \x6f \x77 \x20 \x77 \x6f \x72 \x6c \x64 \x21 \x0a

 

\xba \x0f \x00 \x00 \x00 \x59 ​\xbb \x01 \x00 \x00 \x00 \xb8 \x04 \x00 \x00 \x00 \xcd \x80

 

\xb8 \x01 \x00 \x00 \x00 \xbb \x00 \x00 \x00 \x00 \xcd \x80 \x90 \x90

 

일단 문자열 부분은 컴파일러에서 알아서 아스키문자로 인식하고 코드를 자동으로 변환한다고 하니.. 문자열 부분은

그냥 헥사 코드가 아닌 문자열로 바꾸어 보자.

 

\xe8 \x0f \x00 \x00 \x00 "Hellow world!\n"

 

\xba \x0f \x00 \x00 \x00 \x59 ​\xbb \x01 \x00 \x00 \x00 \xb8 \x04 \x00 \x00 \x00 \xcd \x80

 

\xb8 \x01 \x00 \x00 \x00 \xbb \x00 \x00 \x00 \x00 \xcd \x80 \x90 \x90

 

(hellow 가 된 이유는 오타가 난줄 모르고 그대로 컴파일 해서..)

 

먼저 맨 첫부분에 있는 \xe8 \x0f를 보자.

\xe8은 CALL을 나타내는 부분이다.

그리고 \x0f 은 10진수로 변환하면 15이다. call 호출 후 15바이트 만큼 떨어진 곳에서

연산이 실행된다는 말이다. (\x59부터 \xba 까지 세어보면 정확하게 15바이트가 맞다.)

 

그럼 여기서 하나의 쟁점이 생긴다. 코드에 삽입한 문자열의 길이가 달라지면 CALL되는 위치 또한 바뀔수 있다는 말인데..

그럼 이 문자열을 CALL 호출과 관련 없도록  하면 되지 않을까? 그러면 원하는 문자열을 바꾸는데 깔끔할 것이다.

 

어떻게 바꿔야 할까?

문자열을 뒤로 빼버리면 될 것이다.

 

코드를 좀 수정해 보겠음.

 

jmp 연산을 이용하여 문자열이 call 연산 보다 뒤로 가도록 코드를 수정했다.

이 상태에서 라면 CALL 수행 위치가 문자열에 영향을 받지 않을 것이다.

자 다시 objdump를..

 

 

아까와 다르게 call 부분에 \xe8 \xdd \xff \xff \xff 가 되어 있다.

우리가 사용하는 대부분의 CPU는 리틀 엔디언 방식으로 저장되므로.. 0xffffffdde8 로 읽으면 되는데

 

e8은 아까 call 연산을 나타내는 부분이라고 했고..

그럼 ffffffdd 이건?? 요건 아까 call이 실행되는 부분을 나타내는 정수값 자리에 0x0f 대신에 있는 거다.

그럼 대충 감이 오지 않는가. 0xffffffdd 를 정수로 변환 시켜보면 -35가 나온다.

 

현재 call이 있는 위치에서 35바이트만큼 -(마이너스) 하면 call되는 위치가 나온다.

이 방식으로 하면 문자열의 길이에 상관 없이 call 명령이 실행되는 부분은 고정될 것이다.

 

자.. 이제 다 완성이 되었지 싶다.

 

\xeb \x1e \xba \x0f \x00 \x00 \x00 \x59 \xbb \x01 \x00 \x00 \x00 \xb8 \x04 \x00 \x00 \x00

\xcd \x80 \xb8 \x01 \x00 \x00 \x00 \xbb \x00 \x00 \x00 \x00 \xcd \x80

\xe8 \xdd \xff \xff \xff "Hellow world!\n"

 

이게 완성된 기계어 코드이다.

잘 돌아가는 테스트를 해봐야한다.

 

해보면 잘돌아간다.

다음에는 기계어 코드 작성 요령으로 셸코드 작성법에 대해 포스팅하겠음.

 

 

참고자료

http://hackerschool.org/~research/bbs/data/lecture_member/1070558265/sc_making.txt 

http://www.hackerschool.org/HS_Boards/data/Free_Board/lecture.pdf

 

반응형

'IT 그리고 정보보안 > Knowledge base' 카테고리의 다른 글

RTL (Return-to-Libc) Attack  (0) 2021.04.15
Shellcode 작성  (0) 2021.04.15
버퍼 오버플로우 (Buffer Overflow) 개요  (0) 2021.04.15
Metasploit - Meterpreter  (0) 2021.04.15
Metasploit - Auxiliary 모듈 활용  (0) 2021.04.15