'secuinside 2017 snake'에 해당되는 글 1건
- 2017.07.22 [Secuinside CTF_2017] snake(reversing)
## Secuinside CTF_2017(snake, rev)
[Summary]
1. sub_4014DB()함수가 실제 게임을 수행하는 메인 함수이다.
2. 게임을 진행하면서 snake가 목표를 잡으면 점수가 올라가며 stage가 진행된다.
3. stage가 진행됨에 따라 v21의 값에 특정 연산을 진행한다.
4. 우리는 이 연산을 그대로 코드로 옮기고 실제 snake가 목표를 안잡더라도 잡은 것처럼
점수를 올리고 stage를 진행시켜가면서 v21에 올바른 값이 가도록 코딩을 한다.
5. 마지막으로 꼬리의 길이가 304보다 크면 또 다른 특정 연산을 수행하고 최종적으로 gameover이거나 flag를 뱉는다.
[Analysis]
snake문제를 실행시키면 아래 [그림 1]과 같이 메뉴가 나타나고 게임을 시작하면 [그림 2]와 같이 목표와 snake가
화면에 나타나며 snake가 목표를 잡을 때마다 점수가 오르고 stage가 특정 점수 조건
(10 * stage + previous_score - 7 <= new_score)을 만족할 때마다 다음 stage로 넘어간다.
[그림 1] 메인 메뉴
[그림 2] 게임 시작
게임 방식은 우선 저기 보이는 길다란 snake가 벽에 닿아서도 안되고 자기 자신의 몸에 맞아서도 안되며 0으로
보이는 목표물을 잡아먹어야 Score가 올라가는 형식으로 위에서 언급했듯이 Score가 특정 조건에 만족하면 다음
stage로 넘어가는 형식의 게임이다.
이제 이 게임을 IDA를 통해서 실제 game을 동작시키는 메인 코드인 sub_4014DB()함수를 보면 아래 [그림 3]과 같다.
[그림 3] Play Game (sub_4014DB()함수)의 첫 번째 중요 부분
위 [그림 3]의 소스코드를 간단히 분석해보면 실제 동적디버깅을 하면서 내부 함수를 분석하면 sub_40105A()
함수는 goal에 도착했는지를 체크하는 함수이고, sub_400DD6()함수는 goal을 잡았을 때 새로운 goal을
랜덤으로 만들어내는 함수이다.
그래서 실제로 44번째 줄에서 goal_check를 하고 만약 snake의 head가 goal을 잡았으면 if문 내부로 들어와
46번째 줄에서 새로운 다음 goal을 랜덤으로 생성한 후 48번째 줄에서 score가 다음 stage로 가기에 적합한 조건인지
판단을 한 후 만약 다음 스테이지로 넘어가도 되는 점수라면 50번째 줄에서 sub_4011A5()함수를 실행해 플래그가
decrypt되기 전의 값이 들어가 있는 v21배열에 특정 연산을 하여 decrypt를 하게 된다. 해당 코드는 아래
[그림 4]에서 확인할 수 있다. 그 이후는 snake의 이동속도를 증가시키는 루틴이므로 중요하지 않다.
[그림 4] encrypt된 flag 배열을 decrypt 하는 중간 과정 계산 식
위 [그림 4]가 중요하므로 간단히 분석해 보면 a1이 encrypt된 flag배열, a2가 현재 score, a3이 stage, a4가
previous_score이고 a2, a3, a4를 이용해 a1의 각 배열의 원소에 특정 계산을 통해 집어넣는다. 이 때의 계산식은
여러분이 자세히 보면 알 것이고 우리는 그대로 파이썬 코드로 옮기기만 할 것이다.
여기까지가 플래그를 건드리는 첫번재 단계이다.
여기 까지 분석을 했다면 그 다음 48번째 줄에서 다음 스테이지로 넘어가지 않는다면 단순히 점수를 올리고
끝이 난다. 그리고 이렇게 다음 Stage로 넘어갈지 아닐지 판단한 후에는 항상 sub_4010B6()함수를 이용해
kill되었는지 체크하고 만약 snake가 죽으면 exit_flag의 값을 1을, 살아있으면 0을 반환한다.
그 이후에 항상 반복문을 돌아갈때마다 아래 [그림 5]와 같이 snake의 꼬리의 갯수가 304보다 큰지 확인하는데
이 때 꼬리의 길이가 304보다 크면 또 encrypt된 flag가 들어 있는 v21의 값을 특정 연산을 거쳐 decrypt하게
되는데 이 코드는 아래 [그림 5]에서 확인할 수 있다.
[그림 5] Play Game (sub_4014DB()함수)의 두 번째 중요 부분
[그림 5]를 간단히 분석하면 v21 배열의 각 원소에 stage, score, previous_score값을 이용해 지들끼리 xor하고
더하고 빼고 이상한 연산을 한다. 우리는 위에서 설명한 첫 번째 단계와 똑같이 이를 그대로 파이썬 코드로 옮기면
된다. 자세히 들여다보면 변수명을 이해하기 쉽게 바꿔놓았기 때문에 이해가 갈 것이다. (여기서 이런 변수명의
역할을 알아내는 것이 어려울 수 있는데 나같은 경우는 그냥 동적디버깅으로 각 변수가 뭔 역할을 하는지 일일이
확인한다.)
이제 여기까지 다 끝냈으면 flag를 decrypt하는 두 번째 단계까지 온 것이다. 위 [그림 5]의 85번째 줄에서 exit_flag에
2의 값을 넣는 것을 확인할 수 있고 그럼 91번째 줄이 실행이 되면서 sub_4013C5()함수를 호출한다. 이 함수의
역할은 안에 들어가보면 아래 [그림 6]과 같이 생겼는데 게임을 성공적으로 Clear했을 때 v21의 값에다가 또 무슨
이상한 연산(이번이 3번째 단계)을 한 후 v21의 값을 출력하는 것을 알 수 있다. 그리고 이 값이 flag이다.
[그림 6] game clear역할을 하는 sub_4013C5() 함수 내부
이제 거의 다왔다. 실제 [그림 6]의 함수를 분석해보면 나머지는 다 콘솔 정리하는 부분들이고 sub_401337()함수가
중요하다. 이 함수는 아래 [그림 7]과 같은 코드를 가진다.
[그림 7] sub_401337()함수 분석
자 이제 위 [그림 7]에서 나온 코드를 분석하면 뭔가 이상하다. 지금까지 flag배열이라고 알고 있던 v21이 flag가
아니고 문제를 푼지 오래되서 까먹어서 위에서 잘못 말했다. 그냥 어떤 값이다. 그래도 문제는 없는데 이 함수를
분석하면 qword_604180메모리에 mmap()함수로 excute가능한 메모리를 할당하고 102번의 for문을 돌면서
우리가 지금까지 열심히 구했던 v21의 각 원소와 byte_6040E0메모리에 들어있던 각 원소끼리 한 바이트씩
xor연산을 하여 새로 할당받은 qword_604180메모리에 넣는다. 그리고 8번째 줄에서 실행을 한다.
이제 분석은 다 끝났다. 실제로 위 v21에 어떤 연산을 취한 결과와 byte_6040E0의 값들을 xor하여 실행가능한
메모리영역에 올리고 실행한다. 즉 쉘코드를 실행시키는 것이다. 이제 위에서 설명한 v21을 건드리는 2가지 단계를
그대로 파이썬 코드로 옮기고 byte_6040E0과 xor한 결과 값을 구한 후 쉘코드를 실행시키기만 하면된다.
이 때 나는 쉘코드를 출력하는 파이썬 코드와 그 쉘코드를 실행시키는 c코드를 짜서 실행을 했다.
파이썬 코드를 실행하면 아래 [그림 8]과 같이 바이트 코드가 출력되고, C코드를 실행하면 아래 [그림 9]와 같이
flag가 나온다!!! 아 아니다. 안나온다. C코드는 세그멘테이션 폴트가 뜨는데 오류 수정하기가 귀찮아서 IDA에
메모리에 그대로 바이트 코드를 직접 수동으로 박아주고 실행하였더니 아래 [그림 9]와 같이 flag가 튀어 나왔다.
[Exploit Code] - snake_exploit.py
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 | previous_score = 0 score = 0 stage = 1 tail_length = 4 v21 = [0 for i in xrange(102)] def convert2byte(data): return (data & 0xff) ## Stage 1 while(tail_length <= 304): score += stage if(10*stage + previous_score - 7 <= score): if(stage & 1): v21[stage-1] += stage ^ 7 v21[stage-1+32] = v21[stage-1+32] + score - previous_score + (-3)*stage v21[stage-1+64] += stage + 12 v21[stage-1] = convert2byte(v21[stage-1]) v21[stage-1+32] = convert2byte(v21[stage-1+32]) v21[stage-1+64] = convert2byte(v21[stage-1+64]) else: v21[stage-1] += (stage ^ 2) + 2 v21[stage-1+32] += (2 * stage ^ 7) + 2 v21[stage-1+64] = v21[stage-1+64] + score - previous_score + (-3)*stage + 15 v21[stage-1] = convert2byte(v21[stage-1]) v21[stage-1+32] = convert2byte(v21[stage-1+32]) v21[stage-1+64] = convert2byte(v21[stage-1+64]) stage += 1 previous_score = score tail_length += 1 ## Stage 2 if(stage & 1): v21[stage-1] = v21[stage-1] + ((stage - score + 2 * previous_score) ^ 0xE) - 77 v8 = v21[stage+31] v21[stage+31] = v21[stage+31] + (-20)*(score ^ 2 * stage) v21[stage+63] += ((stage - 31304) >> 2) v21[stage-1] = convert2byte(v21[stage-1]) v21[stage+31] = convert2byte(v21[stage+31]) v21[stage+63] = convert2byte(v21[stage+63]) else: v21[stage-1] += stage+56 v8 = (score - previous_score) v21[stage+31] = v21[stage+31] + score - previous_score - 123 v21[stage+63] = v21[stage+63] + 4 * (score - previous_score) - 9 v21[stage-1] = convert2byte(v21[stage-1]) v21[stage+31] = convert2byte(v21[stage+31]) v21[stage+63] = convert2byte(v21[stage+63]) ## Stage 3 byte_6040E0 = [0x53, 0x4A, 0x8D, 0xED, 0x4A, 0xBE, 0x6E, 0x67, 0x51, 0x63, 0x78, 0x3E, 0x57, 0x0E, 0x41, 0xAD, 0x73, 0x66, 0x33, 0x6B, 0x4D, 0x72, 0x62, 0x75, 0x56, 0xA0, 0x43, 0x57, 0x73, 0x70, 0x7D, 0x07, 0x6F, 0x63, 0x47, 0xAF, 0x75, 0x52, 0x43, 0x6A, 0x60, 0x61, 0x25, 0x44, 0x12, 0xA5, 0x36, 0x45, 0x1E, 0x43, 0xE0, 0x1F, 0xCC, 0x61, 0xE9, 0x86, 0xFC, 0x70, 0xFE, 0x14, 0x90, 0x55, 0xB8, 0x06, 0x5D, 0x56, 0x5E, 0x66, 0x41, 0x61, 0x45, 0x10, 0xAF, 0x14, 0x17, 0x63, 0x19, 0x39, 0x92, 0x99, 0xA2, 0x8C, 0x1F, 0x9B, 0x21, 0x11, 0x22, 0xB7, 0x25, 0xC5, 0x28, 0xD6, 0x91, 0xDD, 0x2B, 0x77, 0x00, 0x48, 0x31, 0xFF, 0x0F, 0x05]#, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] qword_604180 = [0 for i in xrange(102)] for i in xrange(0,102): qword_604180[i] = v21[i] ^ byte_6040E0[i] for i in xrange(0, 102): print "%.2x"%qword_604180[i], | cs |
[Exploit Code] - snake_exploit.py
1 2 3 4 5 6 7 8 9 10 11 12 | #include <stdio.h> #include <string.h> char code[] = "\x55\x48\x89\xe5\x48\xb8\x6e\x6b\x5f\x69\x74\x2e\x5d\x00\x49\xb9\x65\x74\x27\x73\x5f\x64\x72\x69\x48\xba\x5f\x77\x69\x6e\x65\x5f\x6f\x66\x48\xbe\x6b\x5f\x69\x73\x5f\x74\x68\x65\x49\xb8\x5f\x6c\x69\x66\x65\x2e\x5f\x4c\x48\xbf\x53\x45\x43\x55\x5b\x68\x61\x63\x50\x41\x51\x41\x50\x52\x56\x57\xba\x41\x00\x00\x00\x48\x89\xe6\xbf\x01\x00\x00\x00\xb8\x01\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x48\x31\xff\x0f\x05"; int main() { printf("len:%d bytes\n", strlen(code)); (*(void(*)()) code)(); return 0; } | cs |
[Get Flag~~!!!!]
[그림 9] flag 확인
끝~!!!
'CTF writeup' 카테고리의 다른 글
[Samsung CTF_2017] dfa(Defense) (0) | 2017.07.23 |
---|---|
[Samsung CTF_2017] Readflag(Attack) (0) | 2017.07.23 |
[Secuinside CTF_2017] TrippleRotate(reversing) (0) | 2017.07.22 |
[Secuinside CTF_2017] babyheap(pwn) (0) | 2017.07.20 |
[Codegate CTF_2017] EasyCrack 101(reversing) (0) | 2017.02.19 |