발자취
시스템 보안 06. 쉘코드 기본 개념 및 연습문제 본문
1. 기본 개념
1. 쉘코드
프로세서 맞춤형 기계어 명령어들의 집합이다.
(1) 특징
- 메모리 안에서 실행됨
- 어셈블리언어를 사용해 생성됨
- 메모리 내에서 성공적인 실행을 위해서 쉘코드에 있는 명령어들의 수는 최소화되어야 함
(2) 쉘코드를 생성 과정
C프로그램 → 어셈블리 프로그램 → 쉘코드
쉘코드를 만들 때 가장 중요한 포인트는 어셈블리어 코드를 잘 작성하는 것이다.
우리는 쉘코드를 직접 만드는 것이 아니라, 사실상 어셈블리어를 작성하는 것이다.
2. 올바른 어셈블리 코드 작성법
(1) Call 명령어에서 NULL 바이트 제거하기
Call 명령어를 사용하다보면 NULL 바이트가 발생하는 경우가 종종 있다.
NULL 바이트는 문자열의 끝을 의미하기 때문에 어셈블리 코드를 복사하는 과정에서 문제가 생길 수 있다.
따라서 어셈블리 코드에 NULL 바이트가 존재하지 않도록 작성해주는 것이 중요하다.
NULL 바이트를 제거하는 방식은 다음과 같다
어셈블리 코드에서는 상대적인 주소 체계를 사용하기 때문에, 음의 주소로 만들어서 역방향으로 동작하도록 만들어줄 수 있다.
이렇게 만든 음의 주소를 2의 보수로 표현하면 NULL 바이트가 사라진다.
(2) Mov 명령어에서 NULL 바이트 제거하기
mov eax, 0x4
mov 명령어를 위와 같이 사용하면 문제가 발생할 수 있다.
4바이트 사이즈의 레지스터 eax에 1바이트 사이즈의 값인 0x4를 넣어주면 나머지 3바이트에 NULL 바이트가 채워지게 된다. (eax = 0x00000004)
따라서 이런 경우에는 4바이트 레지스터가 아닌 1바이트 레지스터를 사용해줘야 한다.
eax, ebx, ecx, edx의 1바이트 레지스터는 al, bl, cl, dl이므로 이 레지스터들을 사용해준다.
3. 쉘을 생성하는 쉘코드
execve() 시스템콜을 "/bin/sh"과 함께 사용하여 쉘을 생성하는 쉘코드를 만들 수 있다.
(1) execve 시스템 콜
int execve(const char *filename, char *const argv[], char *const envp[]);
filename이 가리키는 프로그램을 실행한다.
argv는 프로그램에 전달된 인자 문자열들의 배열, envp는 프로그램에 전달된 환경 문자열들의 배열.
argv, envp 모두 NULL 포인터로 종료되어야 한다.
첫번째 인자인 filename은 ebx, 두번째 인자 argv는 ecx, 세번째 인자 envp는 edx에 들어간다.
4. 방어책
(1) 스택에서 실행되지 않도록 설정한다
(2) 쓰기와 실행이 동시에 이뤄지지 않도록 설정한다
(3) Address Space Layout Randomization (ASLR) 주소 체계 사용
- ASLR이란? exe 파일을 로드할 때 주소 체계를 계속하여 랜덤하게 바꾸는 기법이다.
쉘코드 주입 공격에서 가장 중요한 것은 쉘코드 시작 주소를 예측하는 것이기 때문에, 이 기법을 사용하면 쉘코드 시작 주소를 예측하기 어려워 공격을 성공할 가능성이 적어진다.
▶ ASLR 회피 방법
이 기술도 주소 체계를 완전히 랜덤하게 바꿔주지 않는다. 정해진 규칙에 따라 주소체계를 바꾸는 것이기 때문에 공격을 여러 번 거듭하여 시도해보면서 규칙을 알아내 다음에 변경될 주소를 알아낸다면 쉘코드 주입 공격을 성공시킬 수 있다.
2. 연습문제
0. 준비 단계
#!/usr/bin/perl
open(in, "<$ARGV[0]") || die "cannot open $ARGV[0] file";
open(out, ">$ARGV[1]") || die "cannot open $ARGV[1] file";
while(<in>)
{
@element = split();
$len = length($element[1]);
$cnt = 0;
printf out "\"";
while($cnt < $len) {
$str = substr($element[1],$cnt,2);
printf out "\\x%s", $str;
$cnt += 2;
}
printf out "\" ";
}
close(in);
close(out);
convert.pl
convert.pl 파일을 다운로드 받아준다.
코드 인젝션 전 쉘코드가 잘 동작하는지를 확인하기 위해 독립적으로 실행되도록 살짝 바꾼 코드이다.
1. /usr/bin/who 프로그램을 어셈블리 코드로 구현하시오. 어셈블리 프로그램의 이름은 who.s로 정하시오.
1. who.c 파일 만들기
#include <stdio.h>
const char shellcode[]=;
Int main()
{
(*(void (*)()) shellcode)();
return 0;
}
이렇게!
2. 어셈블리 코드 who.s 파일 만들기
BITS 32
xor eax, eax
push eax
push 0x
push 0x
push 0x
mov ebx, esp
push eax
mov edx, esp
push ebx
mov ecx, esp
mov al, 11
int 0x80
인자가 없는 명령어의 경우 위와 같은 형태의 쉘코드를 가지게 된다.
인자가 없는 명령어의 쉘코드를 만들 때에는 5, 6, 7 부분에 해당 프로그램의 Hex값을 넣어 사용한다.
프로그램 명을 Hex 값으로 표현하여 쉘코드에 포함해줘야 하는데, 이때 리틀 엔디안 기법에 따라 /usr/bin/who을 거꾸로 작성하여 넣어줘야 한다.
/ohw
/nib
/rsu
이 순서로 스택에 push 해줘야 스택에서 꺼내올 때 순서대로 /usr/bin/who가 되어 나온다.
https://www.rapidtables.com/convert/number/ascii-to-hex.html
ASCII to Hex | Text to Hex Code Converter
From Binary Decimal Octal Hexadecimal Text To Binary Decimal Octal Hexadecimal Text Character encoding ASCII Unicode UTF-8 UTF-16 UTF-16 little endian UTF-16 big endian Windows-1252 Big5 (Chinese) CP866 (Russian) EUC-JP (Japanese) EUC-KR (Korean) GB 18030
www.rapidtables.com
위 사이트를 이용하면 쉽게 프로그램명의 Hex값을 알아낼 수 있다.

알아낸 Hex 값은 위와 같다 (0A는 Enter 때문에 생긴 공백 문자이므로 무시해주면 된다)
이제 위 Hex값을 포함한 who.s 쉘코드를 만들어준다.

앞에서 구한 /usr/bin/who 프로그램의 Hex 값을 넣어준 최종 형태의 who.s 쉘코드 모습이다.
[3] eax를 0으로 초기화
[4] 스택에 eax(=0)를 push
[5, 6, 7] 순서대로 ohw/, nib/, rsu/의 Hex값을 스택에 push함
[8] esp(=스택의 최상위값)을 ebx에 넣음 (=filename인 /usr/bin/who를 ebx에 넣음)
[9] 스택에 eax(=0)를 push
[10] esp(=스택의 최상위값)을 edx에 넣음 (=0을 edx에 넣어줌. 환경변수를 0으로 설정해준 것)
[11] 스택에 ebx(= filename인 /usr/bin/who)를 push
[12] esp(=스택의 최상위값)을 ecx에 넣음 (ecx가 ebx와 0을 포함한 값을 가짐)
[13] al에 0x11을 복사함
[14] execve() 시스템콜 실행

스택은 위와 같은 모습을 갖게 된다.
execve() 함수는 filename, argv[](인자), enpv[](환경변수) 총 3개의 인자를 갖고, ebx에 filename을, ecx에 인자를 (argv[0], argv[1] 등을 모두 포함한 argv), edx에 환경변수(여기서는 별다른 설정없이 0을 넣어줌)을 가진다.
ecx가 약간 헷갈리는데, argv[0]는 filename이고 argv[1]부터가 사용자 입력값이기 때문에 이 모든 것을 포함한 값을 ecx가 가지게 되는 것이다.
3. 쉘코드 생성하기
# nasm who.s
# ndisasm -b32 who > dump_w

4. dump_w에 있는 쉘코드를 C프로그램에서 실행 가능한 형태로 변환하기
# perl convert.pl dump_w out_w
# cat out_w

cat 명령어로 출력한 out_w의 내용을 그대로 복사하여 who.c의 3번 라인의 ‘shellcode[]=‘ 뒤 빈공간에 붙여넣어준다.

이렇게!
5. who.c 컴파일 후 실행
이렇게 완성한 who.c를 컴파일하고 실행한다.
# gcc -z execstack -o whoS who.c
# ./whoS

실행 결과는 위와 같다

원래 /usr/bin/who 프로그램의 실행 결과이다.
같은 결과가 나온 것을 통해 쉘코드가 제대로 생성된 것을 확인할 수 있다.
2. /usr/bin/whoami 프로그램의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.

Hex값은 다음과 같다.
4바이트씩 끊어야 하는데 딱 떨어지지 않을 때, hw//처럼 /를 한번 더 덧붙이면 된다.

위 Hex값을 가지고 작성한 어셈블리 코드 whoami.s다.

위 어셈블리 코드를 가지고 쉘코드를 만들었다.

위 쉘코드를 whoami.c 코드에 붙여넣기 해준다.

컴파일 후 실행해보면 본래의 프로그램 /usr/bin/whoami와 같은 기능을 하는 것을 확인할 수 있다.
3. /bin/cat /etc/passwd 프로그램의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.
이 프로그램은 위 두 예제와 다르게 인자가 있기 때문에 어셈블리 코드의 구조가 약간 다르다.

우선 Hex값을 구해줬다.

위에서 구한 Hex값으로 코드를 작성해줬다.
인자가 있는 프로그램은 어셈블리 코드를 작성할 때 이렇게 해주면 된다.

쉘코드를 만들어준 뒤,

passwd.c에 복붙해준다.

컴파일 후 실행해주면 다음과 같은 결과가 나온다.

쉘코드를 실행한 후의 스택의 구조는 다음과 같다.
4. /bin/cat /etc/hosts 프로그램의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.

Hex값을 구해줬다.

위 Hex값을 이용해서 어셈블리 코드를 만들어줬고,

어셈블리 코드를 통해 쉘코드를 만들었다.

위 쉘코드 내용을 복사하여 hosts.c에 붙여넣어줬다.

컴파일 후 실행해줬다.
5. /bin/ps 프로그램의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.

Hex값을 구해줬다.

구한 Hex값으로 어셈블리 코드를 만들어준 뒤,

어셈블리 코드로 쉘코드를 만들어줬다.

컴파일 후 실행해줬다.
6. /bin/ps -aef 프로그램의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.

-aef도 당황하지 않고 앞선 방법대로 거꾸로 적어서 Hex값을 구해주면 된다.

-aef도 인자로 들어가기 때문에 위와 같은 구조를 갖는다.

위 어셈블리 코드를 가지고 쉘코드를 만든 뒤,

쉘코드를 ps_aef.c 파일에 복붙해줬다.


컴파일 한 뒤 실행한 결과는 다음과 같다.
7. link와 chmod 시스템 콜의 어셈블리 코드, 쉘코드를 작성하고 실행하시오.
link와 chmod는 간단한 시스템 콜이다.
7-1. link
구글에 "link system call"이라고 검색했을 때 가장 처음에 나오는 사이트에 들어가보면 하드링크에 관한 개념임을 확인할 수 있다. 기존 파일을 복사하기 때문이다.
https://man7.org/linux/man-pages/man2/link.2.html

link 함수는 oldpath, newpath를 인자로 받아 지정한다.
우리는 /root/a라는 파일을 복사(하드링킹)하여 /root/b라는 파일을 만들 것이다.

우선 시작 전에 a, b의 존재여부를 확인해보았다.
touch 명령어를 통해 a라는 파일을 생성해주었다.

인자로 넣어줄 /root/a와 /root/b의 Hex값을 구해줬다.

어셈블리 코드를 작성해주었다.

쉘코드를 만들고

link.c에 복사해줬다.

컴파일 후 실행해보면 a, b가 존재하게 된 것을 확인할 수 있다.
(Segmentation fault는 신경쓰지 않아도 된다.)
7-2. chmod

구글에 "chmod system call" 쳐보면 인자는 pathname, mode 두개를 갖는다는 것을 확인할 수 있다.
https://man7.org/linux/man-pages/man2/chmod.2.html
그리고 권한을 새로 주면 기존의 권한이 리셋된다.
권한은 8진수로 표현하기 때문에 우리가 8을 곱해줘야 한다.

우리가 설정해줄 권한인 read by group은 0004인데 우리는 8을 곱해서 32를 넣어줘야 한다.

우선 실습 시작 전, a의 권한부터 확인해보았다.
rw-r--r-- 권한을 갖고 있다.
우리는 ---r-----로 만들어줄 것이다.

[6] cl에는 mode값, 즉 권한 값을 준다. (null 바이트 자체가 쉘코드에 포함되지 않도록 미리 초기화 후, 한 바이트에만 넣어줬다)
[8, 9] push로 pathname(=/root/a)을 스택에 넣어줬다.
[11] al에는 chmod의 시스템콜 번호인 15를 넣어줬다.

위에서 작성한 어셈블리 코드롤 쉘코드를 만들어줬다.

쉘코드를 chmod.c 코드에 복붙해줬다.

컴파일 후 프로그램을 실행한 뒤, 파일 a의 권한을 다시 확인해보면 잘 변경된 것을 확인할 수 있다.

실행 후 스택의 모습은 다음과 같다
'3-2 > 시스템보안' 카테고리의 다른 글
| 시스템 보안 07-1. 코드 재사용 공격 (0) | 2023.12.15 |
|---|---|
| 시스템 보안 05-2. 버퍼 오버플로우 연습문제 (0) | 2023.12.14 |
| 시스템 보안 05-1. 버퍼 오버플로우 공격 (0) | 2023.11.03 |
| 시스템 보안 04. gdb 디버거 (0) | 2023.10.31 |
| 시스템 보안 03. 리눅스 시스템 보안 (0) | 2023.10.04 |