Excute
먼저 다운로드 한 파일을 실행시키면 다음과 같은 형태의 프로그램이 실행 된다.
Key-generate와 load flag 그리고 print flag를 선택해 줄 수 있으나 당연히 2번이나 3번 옵션을 실행하면 flag를 노출시키지 않는다. 2번의 경우 can’t open flag 를 출력하고 프로그램이 종료되며 3번 옵션의 경우 WARNING: NOT IMPLEMENTED를 출력하고 Wanna take a survey instead?를 출력하는데 아무 값을 입력하지 않아도 Segmentaion fault가 나온다.
Analyze
먼저 문제 파일에 어떤 보호기법이 걸려있는지 조회하기 위해 gdb에 올려 checksec명령을 진행하여 확인하였다.
그리고 Hex-ray에 올려 main 함수를 확인하여 프로그램의 전반적인 흐름을 파악한다.
main
- Line 9 : generate_key 함수 실행
- Line 19~21 : v3가 2인 경우 load_flag 함수 실행
- Line 25~29 : v3가 1인경우 key_len을 받고 generate_key함수 실행
- Line 31~34 : v3가 3인 경우 print_flag함수 실행
generate_key
- Line 11 : memset함수를 사용해 s배열을 0x48만큼 0으로 초기화
- Line 12~23 : open 함수를 이용해 /dev/urandom을 열어 s배열에 난수를 a1만큼 저장해준 후 배열의 전체를 돌면서 0이 들어있으면 그부분을 채우고 strcpy를 통해 s배열 값을 key변수에 복사한다.
Load_flag
- Line 6 open함수를 이용해 flag를 연다.
- Line 12 read함수를 통해 0x40만큼 flag변수(전역변수)에 flag파일로부터 읽어 들인 값을 저장한다
- Line 13~14 for문으로 0에서 0x3f번 반복하며 flag[i] = flag[i] ^ key[i] 를 통해 flag배열에 결과적으로 암호화된 값이 저장된다.
Print_flag 함수
- Line 6 do_comment변수를 result변수에 대입해준다.
- Line 7 do_comment의 주소가 0이고 입력 값이 121(‘y’)일 경우 do_comment의 주소를 f_do_comment(함수의 경우 함수이름 자체가 함수 호출 주조이다.)로 바꾸어주고 result변수에 바뀐 do_comment 값을 넣어준다.
f_do_comment 함수
- Read 함수로 buf변수에 0x21만큼 입력을 받는다.
여기까지 핵심이라고 생각되는 함수들을 전반적으로 살펴 보았으나 print_flag함수에서 기대와 다르게 flag값을 출력시켜주지 않는다. 그러다 IDA에서 real_print_flag함수를 확인 할 수 있었다.
real_print_flag
실질적으로 flag를 출력해주는 함수임을 확인 할 수 있다.
Solve
전반적으로 프로그램의 구성을 살펴보았다. 해결방법을 살펴보던 중 real_print_flag함수와 f_do_comment함수가 매우 가까운 위치에 존재한다는 것을 확인할 수 있었다.
두 함수 모두 마지막 한 바이트를 제외하면 모두 같은 값을 가지는 것을 알 수 있다. 그런데 아까 print_flag함수에서 특정 조건하에서 do_comment변수의 값을 f_do_comment함수의 주소로 새롭게 대입시켜주는 것을 확인 할 수 있는데 만약 여기서 대입시키는 값을 마지막 1 바이트의 값이 0x00이 될 수 있도록 조작 해주면 real_print_flag함수를 호출 할 수 있을 것이다. 따라서 현재 목표는 정확히 result변수에 대입되는 값에서 하위 1바이트를 00으로 바꿀 수 있는 방법을 찾아야한다.
가장 의심스러운 부분으로 generate_key 함수에 포함된 strcpy함수를 생각해 볼 수 있다.main함수에서 generate_key함수의 인자로 63을 주는 것을 확인할 수 있는데 실제 generate_key함수 내부에서는 입력값을 0x40(64)까지 제한하는 것을 확인 할 수 있다.
이러한 이유 때문에 generate_key함수를 자세히 들여다 보다 strcpy함수를 확인 할 수있었다. strcpy함수는 마지막에 null바이트를 붙여 주기 때문에 0x40만큼 더미 없이 완전히 꽉 채워준 형태로 전달하게 되면 결과적으로 0x41만큼이 복사되며 해당 공격을 off-by-one공격이라고 한다.
Bss에 위와 같이 변수들이 선언 되어있다. 따라서 위에서 말한 것처럼 원래 f_do_comment의 주소가 들어있는 do_comment 변수의 마지막 하위 1바이트를 00으로 만들 수 있을 것이다. (전역변수는 낮은 주소에서 큰 주소로 자란다) 여기까지의 추론으로 real_print_flag 함수를 호출 할 수 있게 되었다.
하지만 여기서 문제가 발생하는데 real_print_flag는 이미 xor연산을 통해 암호화된 flag값을 출력해주기 때문에 off-by-one을 이용해 성공적으로 real_print_flag함수를 호출 하였더라도 문제는 해결되지 않는다.
여기까지의 해결방법을 우선 정리하면
Sol)
1. Option 3에서 y를 입력해 f_do_comment의 주소를 do_comment에 대입해준다
2. Option 1를 선택해 0x40(64)를 입력해 key배열을 꽉 채워 off-by-one 공격을 트리거 한다.
3. Option 2를 선택해 flag를 암호화된 형태로 로드해준다.
4. Option 3에서 n(y만 아니면 상관없다)을 입력해 real_print_flag함수를 호출 시킨다.
이제 암호화 되어있는 flag를 복호화 시킬 방법을 찾아야한다. 먼저 암호화되는 방식은
flag[i] = flag[i] ^ key[i]이다. 만약 앞에서 말한 sol 1~2를 진행해서 do_comment에 real_print_flag의 주소를 성공적으로 넣어주었다면 key의 길이를 0부터 차근차근 올려가면서 strcpy함수가 호출될 때 key의 마지막 바이트에 null(0x00)이 들어가는 것을 이용하면 xor연산의 특징을 이용해 암호화가 애초에 진행되지 않은 상태의 flag를 얻어 낼 수 있다. (xor연산에서 00을 연산하면 자기자신이 나온다.) 키의 길이를 0부터 63까지 입력해주며 64회 수행하면 복호화 된 flag를 얻을 수 있을 것이다.
결과적으로 문제해결을 위한 솔루션은 다음과 같다.
Sol)
1. Option 3에서 y를 입력해 f_do_comment의 주소를 do_comment에 대입해준다
2. Option 1을 선택해 0x40(64)를 입력해 key배열을 꽉 채워 off-by-one 공격을 트리거 한다.
3. Option 1을 선택하고 key배열을 재설정한다.
4. Option2를 선택하고 flag를 로드한다.
5. Option3를 선택해 변조된 주소인 real_print_flag함수를 출한다
6. 3,4,5를 반복적으로 실행해준다.
exploit코드는 아래와 같다.
Flag를 확인할 수 있다. 다만 어떤 문제인지 확인 할 수 없으나 중간중간에 잘못 출력 되는 문자들이 발생한다. 작성한 exploit을 여러 번 실행시켜 완전한 flag를 얻을 수 있었다. 또한 flag의 첫번쨰 값은 알 수 없기 때문에 플래그 형식상 F를 예상 할 수 있어 스크립트를 쓸 때 F로 설정해 주었다.
'Wargmae > Pwnable.xyz' 카테고리의 다른 글
[Pwnable.xyz – Punch it] (0) | 2021.01.17 |
---|---|
[Pwnable.xyz - PvP] (0) | 2021.01.13 |
[Pwnable.xyz - J-U-M-P] (4) | 2021.01.07 |
[Pwnable.xyz - Welcome] (1) | 2021.01.02 |