[pwnable.xyz] xor(50 pts) :: Write-Up

두비니

·

2020. 8. 12. 04:44

 

What can you access and what are you going to write?

 

 

 

 

 

똥?

 

음 아마도 xor을 해주는게 아닐까 싶네요.

문제 정보에 대해서 확인해봅시다.

 

여기서 딱히 볼거는 FULL RELRO때문에 GOT overwrite가 불가능하다는것말고는 딱히 없네요. PIE가 걸려있다는 것도 봐줘야 하는데 음... 이따가 다시 보도록 합시다.

 

 

일단 아이다로 까봅시다.

 

전체적으로 add랑 비슷한 흐름이네요. v4와 v5를 xor한 결과를 result[v6]에 넣어주네요.

이때 v4, v5, v6이 모두 우리가 설정해주는 값이기 때문에 우리가 원하는 곳으로 접근이 가능하네요. 단, v6이 9보다 작아야 하기 때문에 result의 영역보다 주소값이 작은 영역만 접근할 수 있겠네요.

 

 

 

 

또한 flag를 불러오는 win()함수가 있네요. 위의 접근방법을 통해 win()함수를 실행시킵시다.

그럼 main함수의 구조를 파악해서 어떤 방식으로 공격을 해야 할 지 봅시다.

 

 

다음은 main함수의 어셈블리어입니다. 마지막 부분만 가져왔습니다.

마지막을 보면 leave, ret로 인해서 종료되는 것이 아닌 다른 함수를 call하고 종료되는 것을 확인할 수 있습니다. 아이다로 보면 exit()함수가 불려지는 걸 확인할 수 있네요.(원래는 call 0x800 옆에 어떤 함수인지 뜨는데 이건 왜 안뜨는지 모르겠네요,,) 뭐 GOT overwrite인가 싶었는데, FULL RELRO여서 vmmap으로 이용할 수 있는 영역을 찾아보았습니다.

 

vmmap으로 사용할 수 있는 영역을 찾아보았습니다. vmmap명령어는  Virtual Memory MAP의 줄임말로, 프로그램이 실행된 뒤 실제로 매핑 된 메모리 영역을 보여주어야 합니다. 앞에서도 말했지만, vmmap은 프로그램 실행 후 확인하여야 합니다. 혹시나 모르는 분들을 위해서 캡쳐 첨부합니다.

vmmap은 peda에서 지원하는 기능이며, 일반 gdb는 이걸 지원하지 않는 것으로 알고있습니다. 아무튼 vmmap을 보면, 맨 위의 코드 영역이 rwxp(read, write, execute permission)인걸 확인할 수 있습니다. 즉 code영역을 마음대로 수정할 수 있다는 점이죠. 그럼 이걸 이용해서 마지막에 call exit하는 부분에서 call win으로 우회하도록 하겠습니다.

 

 

+) 아까전에 main의 어셈영역을 보면 코드영역이 0x0a34부터 시작하던데 저렇게 거대한 0x0000555555554000 영역이 어떻게 code영역입니까?

음 일단 두 상황의 차이를 봅시다. 위에서 확인한 main함수의 어셈블리어는 프로그램을 실행시키기 전이고, vmmap으로 확인한 코드영역은 프로그램을 실행시킨 이후입니다. 음 쉽게 설명하자면 일종의 offset이라고 이해하면 좋을 것 같습니다. 막상 실행되었을 때 예를들어 "0x000부터 0x100까지 내가 쓸거야"라고 절대적인 주소를 정해놓는다면 다른 프로그램이 쓰고 있을 수도 있기때문에 프로그램이 실행하면서 필요한 상대적인 메모리 주소들만 기록해놓습니다. 그러고 난 뒤에 실제로 프로그램이 실행이 되면 code 시작주소 + offset 에 실제로 실행이 되겠죠. 다음과 같이 확인할 수 있네요.

 

 

 

++) 참고로 왜 권한이 주어지는지 궁금해서 찾아봤는데 _do_global_ctors_aux()라는 함수에서 영역에 대해서 rwx권한을 부여하네요. 굳이 왜 그런지는....모르겠습니다. 그냥 설정인가봅니다.

int _do_global_ctors_aux()
{
  _DWORD *addr; // [rsp+8h] [rbp-8h]
  
  for ( addr = (_DWORD *)((unsigned __int64)_do_global_ctors_aux & 0xFFFFFFFFFFFFF000LL); *addr != 0x464C457F; addr += 2 ) ;
  return mprotect(addr, 0x1000uLL, 7);
}

 

 

 

아무튼 여기까지 요약

1. result[v6] = v4 ^ v5 때문에 OOB(out of bound)기법으로 result보다 낮은 주소 접근 가능

2. vmmap으로 확인해보니 code영역 수정 가능

3. 2번을 이용해서 call exit부분을 call win으로 바꿀거임

 

 

그럼 바꿉시다

 

 

우선 result변수는 bss영역에 있는 전역변수이고, 주소는 0x202200이네요.

 

win과 main함수의 주소는 각각 0xA21, 0xA34네요. 물론 peda에서 p main/win으로 구해도 상관없습니다.

 

main함수의 마지막 call exit을 call win으로 바꾸고 싶은거니깐

 

v4 ^ v5 = call win의 주소

v6 = [call exit 주소 - result변수 주소] / 8

 

8인 이유는 64비트니깐. 그리고 배열의 인덱스니까 (설마 이걸 모르는건 아니겠죠...?)

일단 구하기 쉬운 v6부터 구해봅시다.

 

v6 = [ 0xb15 - 0x202200 ] / 8 = -262,877.375

로 하려했는데 8로 나누어 떨어지지 않네요ㅎㅎ... 다른곳 쓰겠습니다

 

 

v6 = [ 0xac8 - 0x202200 ] / 8 = -262,887

 

 

이제 call win의 주소를 찾으면 되는데, 두 가지 방법이 있습니다. 첫 번째 방법에서 원리를 이해해야지 두 번째 방법도 이해할 수 있기 때문에 일단 첫 번째 방법도 봅시다.

 

1. 직접 구하기

 

일단 Intel x86 architecture 기준으로, call 명령어는 5 byte로 이루어져 있으며, opcode(명령어)와 operand(명령어의 피연산자)의 값은 다음과 같습니다.

 

 

CALL : E8 XX XX XX XX

 

 

음 처음보는 사람은 저게 뭐지 싶을거같아서 설명을 조금 하면, 어셈블리어보다 한단계 더 낮은 기계어로 바꾼 것입니다. 리눅스에서는 이걸 objdump 명령어로 확인할 수 있습니다. 왼쪽 부분이 hex값, 오른쪽 부분이 어셈블리어입니다.

 

 

아무튼 저 위에 XX부분에는 주소, operand가 들어가게 됩니다. 단 여기서 주소는 현 위치 기준 상대주소입니다. 즉 지금 문제 상황에 대입해서 설명하면, call exit이 불려지는 부분에서의 다음 주소를 기준으로 win함수까지의 offset이 XX XX XX XX에 들어갈 값입니다. (물론 리틀엔디안기준입니다.)

 

 

그럼 들어갈 값 : 0xa21 - 0xacd = 0xffffff54, 즉 call win은

 

CALL WIN : E8 54 FF FF FF

 

가 되겠네요. 그럼 hex값으로 표시했을 때 call win의 주소는 0xffffff54e8일것이고, 이 값은 10진수로 1099511583976 입니다.

 

그럼 v4에는 1, v5에는 1099511583977이 들어가면 되겠죠?

 

 

instruction 주소에 대해서 더 알고싶은 사람 참고 : https://umbum.dev/102

 

jmp, call instruction 주소 계산

Intel x86 architecture에서 JMP, CALL 명령어는 5Byte로 opcode와 operand는 다음과 같다. ```c JMP  : E9 XX XX XX XX CALL : E8 XX XX XX XX ``` 이 때 operand는 절대주소 값이 아니라, 현 위치 기준 상대주..

umbum.dev

 

그래서 결론적으로, call win을 구하기 위해서 이 난리를 친 이유는 main함수 안에서 win을 부른 적이 없기 때문입니다. 따라서 call win을 만들었다는 표현이 더 맞는데, 이걸 이 운영체제에 맞는 방법으로 계산해서 직접 만드는 바람에 많이 복잡해졌네요.

이걸 어렵게 표현하자면 "x86-64에서 PIE가 걸려있는 경우 사용하는 RIP relative addressing 기법을 이용해 call win의 주소를 구했습니다"라고 표현합니다. ㅔ... 설명하니까 많이 길어졌네요.

 

결론적으로 v4 : 1 v5 : 1099511583977 v6 : -262887 겠죠?

 

 

계산과정이 복잡한데 pwntools쓰면 많이 간단해지니까 pwntools쓰는 방법도 봅시다.

 

2. pwntools 쓰기

 

위에서 설명한 내용 그대로 파이썬 코드로 바꾸면 다음과 같습니다. exit_addr은 0xac8로 설정해놓았습니다. 

 

 

단 여기서 다른 점은 code영역을 수정할 수 있는 권한이 있기 때문에 그냥 어셈코드 자체를 수정한 뒤에 그부분의 주소를 불러왔습니다. 확실히 툴이 편하쥬?

 

 

둘 중 어떤 방법을 택하든, 잘 나오는걸 확인할 수 있습니다.

 

첫 번째 방법

 

두 번째 방법

 

 

끝! 막상 풀이 적으니 생각보다 복잡하네요ㅎㅎ..