dreamhack, Lazenca 같은 사이트를 보면 pwnable공부 첫 시작은 shellcode이다.
처음 pwnable을 공부했을 때는 왜 쉘코드를 먼저 배우는지 이해하지 못했다.
그냥 인터넷에 있는거 가져다 쓰면 안 되나? 이런 생각을 했었던 것 같다.
지나고 보니 왜 쉘코드를 먼저 배우는지 알 것 같다.
pwnable을 하기에 앞서 쉘코드를 통해 배울 수 있는 게 매우 많다.
컴파일 과정, 어셈블리, 함수 호출 규약, 32bit와 64bit의 차이점 등등
또한 CTF에서는 일반적인 쉘코드로는 문제가 풀리지 않아 직접 짜야할 때가 있다.
그래서 나도 shellcode부터 정리를 시작하려고 한다.
먼저 shellcode를 공부하기 전에 어셈블리, 레지스터 관련해서 예전에 정리해둔 이 글을 읽고 오자.
Linux 기준으로 설명하겠다.
Shellcode?
- shellcode란 기계어로 작성된 작은 프로그램이다.
- 일반적으로 어셈블리어로 작성한 후 기계어로 변경한다.
- 만약에 해커가 rip(pc)레지스터를 자신이 작성한 쉘코드로 옮길 수 있으면 해커가 원하는 어셈블리 코드가 실행되게 할 수 있다.
- 따라서 공격을 수행할 대상의 아키텍처와 운영체제, 쉘코드의 목적에 따라 다 다르게 작성된다.
System Call
- shellcode는 어셈블리 수준에서 사용자가 원하는 동작을 수행하기 위해 system 함수를 사용한다.
- 이러한 system 함수를 사용하기 위해서는 미리 정의된 system call 번호를 사용해 커널의 system call을 호출할 수 있다.
- 아래처럼 /usr/include/x86_64-linux-gnu/asm/unistd_64.h(32bit는 unistd_32.h)를 열어보면 정의된 system call 번호를 알 수 있다.
System Call Arguments
- 이러한 시스템 함수를 호출할때는 인자를 레지스터에 저장한다.
- system call 번호는 eax, rax에 저장하게 되고, 인자의 경우 아래와 같은 순서로 저장하게 된다.
호출
- 인자도 모두 세팅이 되었다면 system call을 호출해야 하는데, 호출 명령의 경우 32비트와 64비트간 차이가 있다.
- 32비트의 경우 int 0x80 명령을 사용하여 인터럽트를 발생시켜 system call을 호출한다.
- 64비트의 경우 syscall 명령을 사용하여 system call을 수행한다.
execve("/bin/sh",["/bin/sh"], NULL) in 64bit
- 가장 기본적이고 많이 사용하는 /bin/sh를 실행시키는 shellcode를 작성해보자.
BITS 64
section .text
global _start
_start:
xor rdx, rdx // rdx 0으로 초기화
mov qword rbx, 0x68732f6e69622f2f // rbx에 //bin/sh 문자열 넣기
shr rbx, 0x8 // rbx 우측 8bit 쉬프트 연산 -> //bin/sh이 /bin/sh로 변함
push rbx // 스택에 rbx 값 push
mov rdi, rsp // rdi에 rsp값 저장
push rax // rax 스택에 push
push rdi // rdi 스택에 push
mov rsi, rsp // rsp값 rsi에 저장
mov al, 0x3b // execve syscall 번호 저장
syscall
- nasm으로 기계어를 뽑아내어 c코드를 작성하여 실행해 볼 수 있다.
// 컴파일 할 때 -z execstack 옵션을 꼭 주자!
int main(void)
{
char shellcode[] =
"\x48\x31\xd2" // xor %rdx, %rdx
"\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" // mov $0x68732f6e69622f2f, %rbx
"\x48\xc1\xeb\x08" // shr $0x8, %rbx
"\x53" // push %rbx
"\x48\x89\xe7" // mov %rsp, %rdi
"\x50" // push %rax
"\x57" // push %rdi
"\x48\x89\xe6" // mov %rsp, %rsi
"\xb0\x3b" // mov $0x3b, %al
"\x0f\x05"; // syscall
(*(void (*)()) shellcode)();
return 0;
}
- 헷갈릴만한 것들을 설명하면, //bin/sh문자열을 넣을 때 hex값으로 넣는데 순서가 거꾸로이다. 이것은 Little endian방식으로 메모리에 값이 저장되기 때문이다.
- 그 후 1번째 인자인 rdi에 rsp값을 넣게 되는데, 이것은 현재 /bin/sh문자열이 스택의 끝부분, 즉 rsp가 가리키고 있는곳에 존재하기 때문에 rsp값을 넣어주게되면 문자열 포인터가 넘어갈 수 있기 때문이다.
- 위 처럼 execve함수의 첫번째 인자는 문자열 포인터이기 때문에 레지스터에 그냥 문자열을 때려박고 호출하면 평생 호출이 안된다.
- 위와 같이 스택에 문자열이 push되면 rsp가 문자열을 가리키고 있기 때문에 rsp가 포인터(문자열의 주소) 역할을 해줄 수 있다.
- 현재 rdi에 문자열 주소가 들어있으므로 이것을 push하고 rsi에 넘겨주면 ["/bin/sh"]배열이 된다.
- 그리고 execve의 syscall number인 0x3b를 rax에 세팅하고 syscall을 호출하게 되면 성공적으로 쉘을 획득 할 수 있다.
'Pwn > Stack-Based' 카테고리의 다른 글
[Pwn] Frame Faking(Fake EBP) (0) | 2024.01.03 |
---|