[Reversing]

‍허진·2023년 3월 21일
0

hacking

목록 보기
4/7

#Assignment 0

main 함수의 disassemble 결과이다. 약간 길고 복잡하지만, memset으로 배열을 초기화하고 계속 문자열을 출력하며 check_key 함수를 호출하고 strcmp 함수로 문자열끼리 같은지 비교하는 등의 과정이 수행된다는 것을 파악할 수 있다. 하나씩 파헤쳐보자.

앞부분은 생략하고, memset 부분이다. rdi, esi, edx에 순서대로 값을 저장해서 특정 배열에 0을 0x64(100)byte만큼 초기화했다.

'Insert available key(hex value): '를 출력했다. 16진수로 키를 입력해야 하는 것 같다.


scanf 함수를 실행한다! %x로 16진수를 받는 것도 보인다. 그런데 밑의 cmp 명령어에 0x11223344가 있고 je로 eax와 같은지 비교하는 것이 보인다. 아마 0x11223344가 16진수 키 아닐까??

일단 키로 예상되는 값을 입력하고 넘어갔다.


이번에는 'Insert available key(decimal value)'를 출력한다. 10진수 키를 입력받는 것 같다!


아쉽게도 이번엔 키를 cmp로 직접 비교하지 않고 check_key 함수를 호출하고 있다. 지금은 키를 모르기에 아무 값이나 입력하고 si 명령어로 check_key함수 내부를 살펴보았다.


check_key 함수를 게속 실행하였더니 edx에 0x12341234를 저장하고 eax에는 0x22222222를 저장하여 마지막에 eax에 eax와 edx를 더한 값을 저장하였다! 그리고 이 값을 이용하여 cmp로 비교하는 것으로 보아, 최종적으로 eax에 저장된 0x12341234 + 0x22222222 = 0x34563456의 십진수 값(878,064,726)이 두번째 키인 것을 예측할 수 있었다.


일단 지금은 아무 숫자나 입력했기에 Wrong!을 출력하고 check_key와 main 함수가 차례로 종료된다.

프로그램을 다시 실행하고 0x11223344와 878,064,726을 차례로 입력하였다.


이번에는 Insert flag : 로 flag를 입력하라고 하고 있다.

scanf로 %100s, 즉 최대 100바이트의 문자열을 입력받고 있다. 그런데 아직 flag를 모르기에 whatisflag를 입력해보았다.


strcmp의 인자로 전달되는 flag를 찾을 수 있었다! flag는 'flag{Cykor2023_seminar_started!}'였다.


성공!

#Assignment 1

main 함수의 disassemble 결과는 다음과 같다.

대충 봤을 때, 뭔가 문자열을 출력하고 입력을 받은 후에 원하는 flag와 동일하다면 문자열을 출력해주고 끝내며, 다르다면 그냥 종료시키는 프로그램인 것 같다.

하나씩 살펴보도록 하자.

앞부분은 생략하고, 해당 부분부터 보자. rbp-0xc 주소에 0x12342345를 넣고 있다.


실제로 해당 명령어를 실행했을 때 0x7fffffffde60 ◂— 0x12342345ffffdf60에서 0x12342345가 저장된 것을 확인할 수 있었다.


그 다음에 "Enter appropriate number(hex)"를 출력한다.

scanf 함수를 부르는 부분까지 넘어왔다. 역시나 순서대로 rsi에 rax(0x12342345가 저장된 주소)를 대입, rdi에 포맷 지정자인 %x를 대입하고 scanf 함수를 부른다.

아마도 0x12342345가 flag 아닐까? 입력해보았다.


입력하였다.


rbp-0xc에 저장된 값(0x12342345)과 비교하고,


jne이기에 not equal이면 main+97로 jump하게 된다.


하지만 나는 flag와 동일한 값을 입력했기에 그대로 진행하여 "Well done!"을 출력했다!


성공!


#Assignment 2


메인 함수는 다음과 같다. 대충 봤을 때 memset으로 배열을 초기화하고 어떠한 문자열을 출력, scanf로 문자열을 받고 이를 어떠한 flag와 비교하는 듯하다.

자세히 분석해보자.


앞부분은 건너뛰었다. memset 함수를 호출하는데, 인자로는 0x7fffffffde00주소에 있는 값(배열?)과 0, 0x64를 rdi, rsi, rdx에 순서대로 저장하고 전달하고 있다. 배열을 0으로 100만큼 초기화하는 것이라고 이해하면 될 것 같다.


그 다음 "Enter flag : "를 출력한다.


rdi와 rsi를 설정해주고 scanf를 호출한다. %100s로 보아하니 최대 길이를 100byte만큼 받는 것 같다.


현재는 flag를 몰라서 whatisflag를 입력했다. 입력한 것이 rsp에 저장된 것을 확인할 수 있다.


계속 진행하다보니 strcmp 함수를 호출하는 부분이 있다. 두 문자열이 같은지 비교하는데, 전자는 내가 입력했던 'whatisflag'이고, 후자는 'flag{Welcome_Newbies^^}'라는 문자열이었다. flag를 찾았다!


일단 이 상태도 계속 진행하다보면 jne에서 문자열이 flag와 동일하지 않기에 mian+127로 넘어가게 된다.

flag를 알았으니 다시 진행해보자!

이번엔 flag를 정확하게 입력했다.


flag와 동일하였기에 'Well done!'을 얻을 수 있었다.


성공!

#Assignment 3




main 함수를 disassemble해보았다. 굉장히 길다.. 그런데 자세히 살펴보면 eax에 1바이트씩 대입하고, 이를 비교하여 분기를 결정하는 것이 계속 반복된다. 왠지 main+414가 flag와 달랐을 때 실행을 종료시키는 명령어가 있을 것 같다.
따라서 매번 비교할 때마다 비교하는 대상을 종합하면 flag가 되지 않을까? 라는 생각을 해본다.

자세히 분석해보도록 하자.

앞부분을 쑥 넘어가다보니 'Enter flag'를 출력하는 부분이 있다.


이전 문제들과 마찬가지로 rdi, rsi를 설정해주고 %100s로 100byte크기만큼 입력을 받는다는 것을 확인할 수 있었다.

지금은 flag를 모르기에 WhatisflagWhatisflag를 입력하였다.

다음 명령어는 movzx이다. 이는 기존 mov와는 다르게 상위 비트들을 0으로 채워주면서 대입하는 것이다. Byte는 1바이트만 채우기에 상위 3바이트를 0으로 채우면서 부호 없는 수를 대입할 때 많이 쓰인다고 한다.

그다음 al 레지스터와 0x66을 비교하는데, 당연히 flag와 다르기에 je에서 main+86으로 넘어가지 못하고 계속 진행하고, 결국 main+414로 넘어가고 프로그램이 종료된다. 그러므로 main함수를 disassemble한 것에서 비교값들을 미리 종합하여 flag로 입력해보자.

66 6c 61 67 7b 59 6f 75 5f 61 72 65 5f 43 79 4b
6f 72 21 7d

비교하는 16진수 수를 종합한 것은 다음과 같았다. 이를 ASCII로 변환하여 보았다.

flag를 찾았다!

flag를 입력하고 계속 진행하였다.

je를 계속 만족하여 진행하다가 'Well done!'을 출력하였다!


성공!

#Assignment 4


main 함수를 disassemble 해봤다. memeset으로 배열을 초기화하는 함수도 보이고, scanf로 입력 받는 부분도 보인다. flag를 입력하면 well done을 출력하는 부분으로 이어지겠지만, 잘못 입력한다면 그대로 종료될 것 같다.

한줄씩 실행해보았다.


memset에 배열을 넣고 0으로 100만큼 초기화한다.


Enter flag: 를 출력해준다.


scanf로 최대 100byte 크기의 문자열을 입력받는다. 일단은 flag를 모르기에 whatisflag?를 입력하였다.

cmp로 eax의 값과 0x20을 비교한다. eax에는 0이 저장되어 있으므로 jbe에서 조건을 만족하여 main+99로 분기한다.

그리고 eax에 rbp + rax - 0x70에서 1바이트를 넣는 것을 확인할 수 있다. (상위 3바이트는 0으로 채운다.) 따라서 rax에 0x77이 저장된다. 사실 이 값은 우리가 입력했던 문자열이 저장된 것이다! 우리가 입력한 문자열의 첫 문자를 저장한 것이다.


그리고 ecx에 eax의 값에 1을 더한 값을 저장하고, eax 값은 다시 0으로 만든다. 즉, ecx에 우리가 입력한 문자열의 첫 문자에 1을 더한 문자를 저장한 것이다.


rdx에 flag가 저장되었다! 그런데 flag의 형태가 조금 이상하다. -> flag = gmbh|sfwfstf`fohjoffsjoh`jt`gvo"~

그리고선 eax에 rax + rdx의 1byte를 저장한다. 이는 flag의 첫 문자를 저장한 것이다. 그 다음에 eax와 ecx를, 즉 flag와 우리가 입력한 문자열의 첫 문자끼리 비교하는 연산을 수행한다.

하지만 둘이 같지 않았기에 'Wrong!'을 출력하고 종료하게 된다.

여기서 의심이 드는 점이 있다. flag가 이상하다는 점과, 우리가 입력한 문자에 1을 더해서 저장한다는 점이다. 마침 flag의 첫 문자인 g는 'flag'의 'f'에 1을 더한 문자이다!

python 코드로 아까 찾았던 flag의 각 문자에서 1을 뺀 문자를 얻어보았다.

출력 결과는 다음과 같았다.


익숙하게 보던 flag 형식을 얻어냈다! 결국 우리가 처음 찾았던 flag는 기본 형식에 각각 1을 더해서 저장해놓았던 것임을 알 수 있었다.


성공!

#Assignment 5

main 함수를 disassemble 해보았다. 처음보는 strtok 함수가 있다! 검색 결과 strtok 함수는 첫번째 인자로 문자열을 받고, 두번째 인자로 구분자를 받아서, 문자열을 구분자마다 나누어서 반환해준다고 한다.

따라서 예상해보건대 어떠한 문자열을 구분자로 계속 나누고, strcmp 함수로 특정 문자열과 비교하는 형식이 진행되지 않을까 싶다.

자세히 분석해보자.


먼저 처음에 rsi에 저장된 파일의 경로를 스택에 저장하였다!


그리고 함수를 계속 진행하였다. 스택에 저장했던 파일의 경로를 rax에 그리고 최종적으로 rdi에 저장했고, rsi에는 rip + 0xe1f에 저장된 값인 '/'를 저장하였다. 이들은 strtok의 인자에 전달되는 값들로, rdi에 저장된 파일 경로 문자열이 첫 번째 인자로, '/'이 구분자로써 두 번째 인자로 전달되었다.

함수 실행의 결과로 구분자 /에서 끊긴 home이 eax에 저장되었다.

그리고 함수는 main+103으로 분기하고, cmp에서 eax에서 저장된 값이 null값과 같지 않으므로 jne에서 다시 main+74로 분기하였다.

해당 과정이 계속해서 반복되었다. home에서 시작하여 jinheo, Desktop, Jin, reversing, assignment5까지 파일 경로에 맞게 진행하였고, 마지막으로 strtok 함수가 null 값을 반환하였다. 이 null 값으로 인해 jne에서 분기가 이루어지지 않았다!

분기가 이루어지지 않아서 진행한 곳에는 strcmp 함수가 있었다. 그래서 strcmp 함수가 비교하는 두 인자를 보았는데 하나는 파일의 마지막 경로(파일 이름)이었던 'assignment5'이고, 하나는 이 문제의 flag라고 할 수 있는 'FunnyReversing'이었다!

두 문자열이 같지 않으므로 함수는 실행을 종료하였다. 그렇다면 마지막에 je를 만족하기 위해 파일의 이름을 'FunnyReversing'으로 바꾸고 파일을 실행시켜 보자.

'Well done!'을 얻을 수 있었다! 파일의 이름이 마지막으로 저장되어 strcmp에서 비교되고, je를 만족하여 'well done!'을 출력한 것이다.

성공!

#Assignment 6

main 함수를 disasseamble 해보았다.

생각보다 간단해 보이는데?? 내부의 함수로 보이는 check 함수도 disassemble 해보았다.


check 함수가 어마어마했다.. 일단 대충 봤을 때 time함수와 localtime 함수가 나타나고, 많은 양의 연산을 이어가다가 check 함수를 다시 써서 뭔가를 얻어내는 것 같았다.

일단 하나씩 진행해가면서 알아보자.

먼저 'Enter student number :'을 출력하면서 어떠한 입력을 받으려고 한다.


이렇게 scanf에서 입력을 받고, 입력한 값은 check 함수의 인자로 전달한다.

check 함수의 내부로 들어왔다! 먼저 time 함수를 호출하는 부분과 localtime을 호출하는 부분이 보인다.


두 함수가 반환값으로 어떠한 값을 보내주었다. 검색해보니 time 값이 반환한 값을 localtime 함수에 사용하여 localtime 함수는 struct tm 형의 구조체를 반환한다고 한다. struct tm은 다음과 같은 필드로 이루어져 있다.

struct tm {
  int tm_sec;   // 초 (0-60)
  int tm_min;   // 분 (0-59)
  int tm_hour;  // 시 (0-23)
  int tm_mday;  // 월별 일 (1-31)
  int tm_mon;   // 월 (0-11)
  int tm_year;  // 연도 - 1900년을 기준으로 한 값
  int tm_wday;  // 요일 (0-6, 0은 일요일)
  int tm_yday;  // 연중 날짜 (0-365, 1월 1일부터)
  int tm_isdst; // 서머타임(Daylight Saving Time)의 여부
};

이 구조체가 어떻게 쓰일지는 모르겠지만, 일단 알아두도록 하자! ㅎㅎ

그 다음에는 무지막지한 계산을 하게 된다. 곱하고, 쉬프트 연산하고, 더하고, 빼고,,,, 하다 보면 계산의 결과값을 출력해준다!


그리고 그 출력값과 <_tmbuf+4>의 4바이트 값을 비교하게 된다. <_tmbuf+4>은 tm 구조체의 두 번째 멤버를 뜻한다! (모두 int형이므로 각각 4바이트씩 차지해서) 결국, flag는 프로그램을 실행한 분(minute)임을 파악했다!

이제 앞의 무지막지한 계산을 정리해보자.. 패드에 직접 써보면서 일일이 정리해보았다!


명령어를 한줄 한줄 정리하다 보니 내가 입력했던 값이 계산되는 과정에서 <_tmbuf+24>, 요일(0~6)과 <_tmbuf+12>, 날짜까지 변수에 포함되는 것을 확인할 수 있었다.

그리고 이를 파이썬 코드로 정리해보았다.

코드를 실행하고 1부터 차례대로 입력해보면서 결과값을 확인하였는데, 신기한 점을 발견했다!

1~10까지 입력하면서 확인한 것인데, 모든 결과값이 60을 넘지 않는다는 것이다!! 결국 그 길었던 계산은 flag(프로그램 실행 시 몇 분인지)와 맞추기 위해 0~59 사이의 값이 나오도록 하는 것이었다!!

계산 과정을 살펴보니, 곱셈과 쉬프트 연산, 뺄셈으로 구성된 연산이 사실은 %(mod) 연산을 위한 것이었다! 0x3e8이 10진수로 1000이고, 0x3c가 10진수로 60이었기에, 이들을 마지막에 곱하고 빼줌으로써 % 연산을 구현했던 것이다. 그래서 마지막 결과도 % 60 연산이 적용되어 0~59사이의 값만 도출됐던 것임을 알게 되었다!


현재 시각(프로그램 실행 시 X시 27분이었음)과 같은 값을 출력하는 5를 입력하고 'Well done!'을 얻어냈다!

#Assignment 7


main 함수를 disassemble 해봤다. fopen, fread, fclose, sprinf 등의 함수들이 있는 것을 보니 파일을 열고 입출력하는 함수들이 있는 것 같다. 자세히 분석해보자.


먼저 fopen 함수에서 'flag.txt'라는 파일을 열고 있다. 그리고 반환값이 null값과 같은지 비교하고 같을 시에는 main+132로 분기하고 있다. 반환값이 null값과 같다는 것은 flag.txt라는 파일이 없어서 파일 열기를 실패했다는 뜻이다!

main+132로 분기하면 함수를 종료하기 때문에, touch flag.txt로 flag.txt를 만들고 다시 분석해보았다.


fread 함수를 통해 파일 안의 내용을 읽고 있다. 인자로는 파일과 사이즈, 길이를 전달하고 있다. 읽은 문자열은 rsi에 저장한다!


fclose로 파일을 닫고 있다.


sprintf로 문자열을 작성하고 있다! 이게 어디에 쓰일지는 모르겠는데,,


그리고 system 함수에 파일 내의 문자열을 전달하고 있다. system 함수는 전달받은 인자를 그대로 실행하는 함수이다.(리눅스 명령어 실행하듯이) 이 상황에서는 파일에 "echo 'Well done!'\nUUUU"가 있기에 이를 실행할 것이다.


system 함수를 실행하니 다음을 출력하고 함수가 종료되었다.

검색해보니 해당 문제를 해결하기 위해서는 system 함수의 자식 분기를 만들지 않기 위해 디버깅 전에 'set follow-fork-mode parent'를 입력하면 된다고 한다.

입력하고 다시 실행해보았다.


이번엔 system 함수도 실행하였고, 다음에 fopen 함수를 다시 실행하였다. fopen 함수의 인자로는 방금과 같이 'flag.txt'가 전달되었다.

그 다음 명령어로는 fopen 함수의 반환값과 null값을 비교하여, 같지 않다면 main+255로 분기하도록 되어있었다. 그런데 main+255로 분기하면 함수가 그냥 종료된다. 즉, fopen 함수의 반환값을 null로 만들어야 분기하지 않고 'Well done!'을 출력할 수 있는 것이다!

fopen 함수의 반환값을 null값으로 만들기 위해서는 fopen 함수가 열고자 하는 파일의 이름을 가진 파일이 존재하지 않으면 된다. 그렇다고 프로그램을 실행할 때에도 flag.txt가 존재하지 않으면 프로그램이 그냥 종료된다. 그렇다면 방법은 무엇일까??

이때 사용할 수 있는 것이 바로 system 함수이다. system 함수는 전달받은 명령어를 수행하는데, 아까 flag.txt에 적힌 문자열을 전달받아 수행하는 것을 보았으니, flag.txt에 flag.txt를 삭제하는 명령어를 적어놓으면, 처음의 fopen 함수는 정상적으로 작동하지만 system 함수 이후의 fopen 함수는 flag.txt가 없어서 null값을 반환하고, 결국 jne에서 조건을 만족하지 못하여서 'Well done!'을 출력할 수 있을 것이다!

따라서 flag.txt에 "rm flag.txt"를 적어놓고 assignment7을 수행하였더니 system 함수 이후의 fopen 함수가 null값을 반환하였고, 성공적으로 "Well done!"을 출력했다!

성공!

#Assignment 8

main 함수를 disassemble 해보자.

함수가 되게 짧다. 그렇지만 짧다고 무시하면 안된다. 일단 vuln_func 함수가 보인다. 해당 함수도 disassemble 해보자.

vuln_func 함수 역시 길지 않다. 함수 내부를 보니 0x61626364와 eax에 저장된 값을 비교하는 것 같다. 아마 0x61626364가 flag 아닐까?? 라는 생각을 가지고 자세히 분석해보자.


rdi에 0x5a2fcc0011223344를 인자로 하여 vuln_func 함수를 호출하고 있다. vuln_func 함수 내부를 살펴보자.


rbp - 0x28 부분에 rdi에 저장되었던 인자값을 저장했다.


그리고 scanf로 입력을 받는데 그냥 %s이다. 이건 입력 바이트에 제한이 없다는 것이고, buffer overflow가 가능한 상황이다. 실제로 아래의 cmp에서 eax와 0x61626364를 비교하고 있는데, 이 eax는 rbp - 0x28에서 참조하고 있다.

즉, scanf의 입력값으로 rbp - 0x28에 저장된 0x11223344값을 0x61626364로 덮는다면 flag와 일치시킬 수 있을 것이다!

scanf의 입력값으로 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaadcba'을 입력하여 rbp - 0x28을 dcba로 덮었다! (little endian 방식으로 저장되기에 dcba는 0x61626364로 저장된다.)

비교값이 동일하여 'Well done!'을 출력하였다.

성공!

profile
매일 공부하기 목표 👨‍💻 

0개의 댓글