HawkIS CTF [과유불급] Write-up
파일을 들어가본다.
먼저, 문제 풀기에 앞서 버퍼오버플로우(Buffer Overflow)가 뭔지 개념적으로 이해를 해야했다.
공부를 하는데 있어 드림핵(https://learn.dreamhack.io/60#1) 강의를 참고하였다.
Buffer는 데이터가 목적지로 이동되기 전에 보관되는 임시 저장소의 의미로 쓰인다.
주로 프로그램이 사용자에게 데이터를 입력받을 때, 사용자의 입력 데이터를 걸러서 받지 않고 미리 준비된 버퍼보다 더 많은 양의 데이터를 입력받을 때 발생하기도 한다.
또는 해커가 임의로 프로그램의 메모리의 값을 변조할 때도 쓰이게 된다.
오버플로우의 예시를 일부 발췌하였다.
-> 데이터의 처리속도가 다른 두 장치가 있을 때, 이 둘 사이에 오가는 데이터를 임시로 저장해 두는 것은 일종의 완충 작용을 합니다. 이해를 돕기 위해 키보드에서 데이터가 입력되는 속도보다 데이터를 처리하는 속도가 느린 프로그램이 있다고 가정하겠습니다. 사이에 별도의 장치가 없다면, 키보드의 입력 중에 프로그램에서 수용되지 못한 데이터는 모두 유실될 것입니다. abcdefgh를 입력했는데 프로그램에는 abef만 전달될 수도 있습니다.
<버퍼 오버플로우가 발생했을 때의 보안위협>
버퍼 오버플로우가 발생하는 버퍼 뒤에 중요한 데이터가 있다면, 해당 데이터가 변조됨으로써 문제가 발생할 수 있다.
예시를 들어보겠다.
1 // Name: sbof_auth.c
2 // Compile: gcc -o sbof_auth sbof_auth.c -fno-stack-protector
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 int check_auth(char *password) {
8 int auth = 0;
9 char temp[16];
10
11 strncpy(temp, password, strlen(password));
12
13 if(!strcmp(temp, "SECRET_PASSWORD"))
14 auth = 1;
15
16 return auth;
17 }
18 int main(int argc, char *argv[]) {
19 if (argc != 2) {
20 printf("Usage: ./sbof_auth ADMIN_PASSWORD\n");
21 exit(-1);
22 }
23
24 if (check_auth(argv[1]))
25 printf("Hello Admin!\n");
26 else
27 printf("Access Denied!\n");
28 }
위의 코드도 공부할 때 예시로 보았던 버퍼 오버플로우의 예제이다.
18행 > main함수 argv[1]를 check_auth 함수로 전달 후, 반환 값 받아옴
* 반환값이 0이 아니면 "Hello Admin!" 출력 *
* 0이라면 "Access Denied!" 출력 *
7행 > check_auth 함수는 16바이트 크기의 temp 버퍼에 입력받은 패스워드 복사 후, "SECRET_PASSWORD" 문자열과 비교.
11행 > strncpy 함수로 temp 버퍼 복사 시, temp의 크기인 16바이트가 아닌 소수로 전달된 password의 크기만큼 복사함.
* argv[1]에 16바이트가 넘는 문자열 전달 시, 모두 복사되어 스택 버퍼 오버플로우 발생 *
<강의 예제>
temp버퍼에 글자수에 맞게 입력했을 때는 반환 값이 0이기 때문에 Access Denied! 문자열이 출력되었다.
하지만, 나머지 글자 수를 입력하여 auth 버퍼까지 들어가니 오버플로우가 발생하여 Hello Admin! 이라는 문자열이 출력되었다.
다른 보안 위협으로는 데이터 유출이 있다.
1 // Name: sbof_leak.c
2 // Compile: gcc -o sbof_leak sbof_leak.c -fno-stack-protector
3 #include <stdio.h>
4 #include <string.h>
5 #include <unistd.h>
6 int main(void) {
7 char secret[16] = "secret message";
8 char barrier[4] = {};
9 char name[8] = {};
10 memset(barrier, 0, 4);
11 printf("Your name: ");
12 read(0, name, 12);
13 printf("Your name is %s.", name);
14 }
위 코드는 name에 12바이트의 문자열을 받고있다.
하지만 name은 8바이트 문자열이기에, 4바이트를 초과해서 입력을 받게 된다.
name과 secret 버퍼와의 사이에 barrier이라는 4바이트의 NULL이 존재하고, name에 12바이트 크기의 값을 입력하면 NULL byte를 모두 다른 값으로 변경하여 secret을 읽을 수 있다.
마지막으로, 실행 흐름 조작이 있다.
1 // Name: sbof_ret_overwrite.c
2 // Compile: gcc -o sbof_ret_overwrite sbof_ret_overwrite.c -fno-stack-protector
3 #include <stdio.h>
4 #include <stdlib.h>
5 int main(void) {
6 char buf[8];
7 printf("Overwrite return address with 0x4141414141414141: ");
8 gets(buf);
9 return 0;
10 }
함수를 호출할 때 ret에 돌아갈 주소를 저장하고 반환될 때 ret의 주소를 꺼내 원래 실행 흐름으로 돌아가게 된다.
만약 ret의 값을 조작하면, 프로세스의 실행 흐름이 조작할 수 있게 된다.
위 코드의 main 함수 반환 주소를 조작해본다.
<여기까지 개념공부>
---------------------------------------------------------------------------------------------------------------------------------
과제를 할 때는 우분투 환경을 이용하여 실행하였다.
먼저 우분투에 파일을 다운받아 준비한다.
C 소스코드를 보았다.
31행 name은 0x50만큼 할당이 되어있다.
10진수로 변환해보면 80바이트를 의미한다.
41행의 pizza는 79바이트이기 때문에 취약점이 아니다.
하지만 chiken은 0x100(256바이트)이다. 0x50(80바이트)를 훨씬 넘는 바이트이기 때문에 취약점이 발생한다.
코드를 실행시키면 위와 같은 글이 뜨는데,
h 입력 > i like hamberger! 출력
p 입력 > what do you want? 출력
c 입력 > i don't like chiken. 출력
뭐 다른 의미가 없는 것 같다.
먼저 이와 비슷한 문제를 보기 위해 드림핵 시스템 해킹 문제인 basic_exploitation_000 & basic_exploitation_001 문제를 보았다.(다 풀진 못했다. 시간이 남으면 라이트업 작성 조금이라도 해보겠다...)
다른 비슷한 문제들도 보았는데, 다들 사용하는 pwntools이 있고 공통적으로 디버깅 툴을 사용하는 것 같았다.
gdb라는 것인데, gdb는 GNU Debugger의 약자로 console 디버거라고 한다. gdb shell을 제공하고, 사용자는 그 shell에 명령을 입력함으로써 디버깅을 수행할 수 있다.
다른 프로그램 수행 중에 그 프로그램 내부에서 어떤 일이 일어나는지 보여주거나 프로그램이 잘못 실행되었을 때 무슨 일이 일어나고 있는지 보여주는 것이기도 하다. 주로 c나 c++로 짠 프로그램을 디버그할 수 있다고 한다.
먼저 수행하기 위해 다운을 받는다.
git clone http://github.com/pwndbg/pwndbg.git
다운 완료 후 start 명령어를 입력하면 프로그램 시작점부터 분석할 수 있는 정보를 볼 수 있다.
하지만 나는 이미 다운 받은 후이기 때문에, gdb를 실행하고 나서 파일을 start 할 수 있다.
시작 명령어 : gdb [파일명]
> 여기서 gdb -q [파일명]을 해주면, 쓸데없는 정보는 출력되지 않는다.
gdb 파일 실행 후 start를 한 모습이다.
명령어 : info func
- 함수 주소 출력 -
메인함수와 플래그가 있는 read_me가 있다.
여기서 메인함수의 크기와 read_me의 크기를 볼 수 있다.
[disass main]
read_me 스택 보기
[disass read_me] 명령어를 입력한다.
블로그 참조 : https://hackstoryadmin.tistory.com/entry/x64-BOFBuffer-Overflow
x64 BOF(Buffer Overflow)
오늘은 x64 즉 64 bit 환경에서의 buffer overflow에 대해 포스팅하겠습니다. 먼저 x86 vs x64에 대해 선행 지식이 필요합니다. 바이너리 하나를 받았는데 이제서야 풀이해보네요.ㅠ 먼저 바이너리를 확인
hackstoryadmin.tistory.com
위 블로그를 참조한 방법대로 read_me의 첫 주소를 익스해보려고 했다.
사실 뭔지 몰라서 이 방법 그대로 따라해보았다.
첫 주소 = 0x0000000000401214 를 리틀엔디언 방식으로 써준다.
ㅎㅎ 되지 않는다...
여기서 리틀엔디안이란?
78563412 >> 리틀엔디안 >> 0x78, 0x56, 0x34, 0x12
12345678 >> 빅 엔디안 >> 0x12, 0x34, 0x56, 0x78
--쉘코드 작성--
이 코드는 드림핵 문제를 풀면서 작성했던 코드이다. 여기서 shellcode라는 것을 사용하였는데,
context.arch = 'amd64'는 대상 아키텍쳐를 말하는 것이다.
아키텍쳐 종류에 맞게 작성해주면 된다.
context.arch = "amd64" #86-64 아키텍처
context.arch = "i386" #x86 아키텍처
context.arch = "arm" #arm 아키텍처
shellcraft 사용법 블로그 참조
https://baobob1024.tistory.com/106
pwntools shellcraft
http://docs.pwntools.com/en/stable/shellcraft/i386.html - 32bithttp://docs.pwntools.com/en/stable/shellcraft/amd64.html - 64bit 32bit binary에서 context(arch='i386', os='linux') 이거 써주면 shellcraft.i386.linux.open() 말고 바로 shellcraft.open(
baobob1024.tistory.com
(약간의 허튼 짓? 드림핵 문제를 응용하여 이번 과제 코드를 작성해보았다.)
암튼 다시 본론으로 가자면...
먼저 pwn 모듈을 임포트 시키고,
해당 문제가 있는 서버로 접속하기 위해 remote 해준다.
다음 adr 변수는 아까 gdb를 실행하고 info func 명령어 입력하여 함수 소스의 정보를 보았을 때, read_me의 값을 넣어준 것이다.
이제 코드를 작성할 때는 소스코드에서 보았던 Chiken의 C부분만 보고 작성을 할것이다.
p.recv()는 괄호 안에 데이터를 받을 크기만큼 작성해주는 것인데, 사진을 보다시피 괄호만 넣을시에는 C부분의 전체를 읽어온다는 뜻이기에, 그냥 저렇게 적어주었다.
(* 저번에 Addition hero 문제풀이 시 적었던 recvuntil은 문자열이 나올 때 그 다음부터 받겠다는 의미이다.
다음에 작성한 p.send(b'c')는 c의 부분의 문자열을 전부 전송하겠다는 뜻이다.
데이터를 받는 recv는 필요없는 데이터를 받을 때 사용하며, print p.recv()는 받은 값을 출력하는 것이다.
여기서 p.recv()는 최대 n바이트를 받는것이기에, n바이트를 모두 받지 않아도 에러를 발생시키지 않는다.
하지만 이와 비슷한 p.recvn()이 있는데, 이건 정확하게 n바이트를 받아야만 실행이 되며, 받지 못할 경우 에러가 발생한다.
(주로 print p.recv(1024)의 방식을 사용한다고 한다.)
마지막으로 익스플로잇 코드 맨 마지막에 작성하는 것인데, p.interactive는 쉘과 직접적으로 명령을 전송하고 수신하며 서버와의 상호작용 및 명령어를 전달한다.
이제 payload 부분만 작성하면 되는데...
위 사진에 보이는 페이로드는 드림핵 아까 말한 basic_exploitation을 풀 때 작성했던 것 그대로 가져와봤다.
여기서 python3는 문자열 앞에 바이트 형식으로 변환시켜주는 문자열 b를 꼭 작성해줘야한다.
여기서 132 숫자의 의미는 132바이트까지 입력할 문자를 만드는 것이다.
제일 헷갈렸던건 p64에 대한 것인데, 밑 블로그를 참조하여 흐름은 이해하였다. 파일의 컴파일 된 비트와 맞게 적어주는 것 같다. 확인 해보니 64비트로 컴파일된 파일이 맞는 것 같다.
명령어 : file [파일명]
일단 이대로 실행을 시켜보았다.
실행이 되긴 하지만,,, 문제에서 나왔던 파일 경로로 들어갈 수가 없다...
여기서 페이로드 값을 맞게 고쳐주어야 할 것 같다.
하지만 시간도 시간인데, 흐름만 이해한 상태라 드림핵의 스택 오버플로우 강의와 관련된 basic_exploitation 문제들도 풀어보았지만... 아쉽게 플래그를 찾아내지 못했다.
거의 다 온 것 같은데 제대로 뭔가 잘 안풀려서 아쉽기도 하고 조금 더 풀어보고 싶기도 하다. 포너블에 대해 열심히 공부해야겠다는 생각이 드는 과제다. 또한 오버플로우에 대해서 알게 되는 시간이 되어서 뜻깊었다.
신기하기도 하고 처음 접하는 것이기에 공부하다보면서 흥미가 생기는 것 같다. 앞으로 드림핵 강의도 참고하면서 공부하면 좋을 것 같다는 생각이 든다.
basic_exploitation의 라이트업을 쓰지 못해서 나중에 개인적으로 써 볼 생각이다!