PIC (Position-Independent Code)
1. PIC ?
리눅스에서 ELF 는 실행파일(Executable)과 공유 오브젝트(Shared Object, SO) 로 2가지가 있다.
- 실행파일 : 일반적인 실행파일
- 공유 오브젝트 : 라이브러리 파일
공유 오브젝트는 재배치(Relocation)가 가능하도록 설계되어 있다.
재배치는 메모리의 어느 주소에 적재되어도 코드의 의미가 훼손되지 않는 것을 의미한다.
재배치가 가능한 코드를 Position-Independent Code (PIC) 라고 한다.
2. 정적 라이브러리 (Static Link Library)
1. 정적 라이브러리
정적 라이브러리(Static Library) 는 프로그램 컴파일 시 Linking 단계에서
라이브러리가 제공하는 코드를 복사하여 실행파일에 넣는 방식의 라이브러리이다.
오브젝트 파일을 ar 명령을 이용해 모아둔 아카이브 파일(.a) 이다.
정적 라이브러리를 만들고 라이브러리 함수를 어떻게 다른 파일에서 호출하는지 확인해보자
2. 정적 라이브러리 생성
// Name : add.c
#include <stdio.h>
void add( int a, int b ){
int sum = 0;
sum = a + b;
printf( "[add] : %d\n", sum );
}
2개의 숫자를 입력받고 더한 뒤 출력하는 함수이다.
$ gcc -c add.c
위의 명령어로 add.c 파일의 오브젝트 파일을 생성할 수 있다
- -c : 컴파일 하지만 링크 하지 않음
$ ar rv libadd.a add.o
위에서 만든 오브젝트 파일을 libadd.a 라는 정적 라이브러리 파일(.a) 로 만들어주자
3. 라이브러리 함수 호출 프로그램 생성
// Name : test.c
#include <stdio.h>
int main(){
printf( "[test] start!\n" );
add( 10, 10 );
printf( "[test] finish!\n" );
return 0;
}
위에서 만든 라이브러리 함수 (add) 를 호출하는 프로그램이다.
$ gcc test.c -o test libadd.a
정적 라이브러리를 포함하여 컴파일한다.
프로그램을 실행하면
라이브러리 함수 (add) 가 실행된 것을 볼 수 있다.
정적 라이브러리를 이용하여 실행 파일을 생성하면 실행 파일 내에 라이브러리 함수의 코드가 포함된다.
→ 실행 환경이 바뀌어도 잘 동작한다.
라이브러리 함수를 여러 프로그램에서 사용하면 각각의 실행 파일 마다 똑같은 코드가 포함된다.
→ 똑같은 코드가 반복되므로 메모리나 디스크 공간이 낭비된다.
라이브러리를 업데이트 하려면 프로그램을 다시 컴파일 해야한다.
이러한 단점을 해결한 것이 공유 라이브러리이다.
3. 공유 라이브러리 (Dynamic Link Library)
1. 공유 라이브러리
정적 라이브러리와 다르게 컴파일 시에 Linking 되는 것이 아닌
프로그램이 동작할 때(Runtime) 프로그램에 연결된다.
여러 오브젝트 파일을 하나의 오브젝트 파일(.so)로 만들어 이를 공유해서 사용할 수 있도록 한 것이다.
실행 시 공유 라이브러리를 참조하는 방식으로 동작한다.
공유 라이브러리를 만들고 라이브러리 함수를 어떻게 가져오는지 확인해보자
2. 공유 라이브러리 생성
// Name : add.c
#include <stdio.h>
void add( int a, int b ){
int sum = 0;
sum = a + b;
printf( "[add] : %d\n", sum );
}
위의 정적 라이브러리의 코드와 같다.
$ gcc -c -fPIC add.c
-fPIC 옵션을 이용하여 독립적인 코드로 컴파일한다
- -fPIC : 위치 독립 코드 생성 (메모리의 다른 위치에 로드되기 때문에 공유 라이브러리에 필수)
$ gcc -shared -o libadd.so add.o
공유 라이브러리 .so 파일을 생성한다.
- -shared : 오브젝트 파일을 공유 라이브러리에 연결
생성한 공유 라이브러리를 사용하기 위해서는 경로를 ld.so.conf.d 에 등록해주어야 한다.
$ sudo vi /etc/ld.so.conf.d/libadd.conf
/etc/ld.so.conf.d/ 디렉터리에 .conf 확장자를 가진 파일을 생성해주면 된다.
파일 이름은 무엇이든 할 수 있지만 .conf 확장자로 끝나야 한다.
생성한 파일에 공유 라이브러리(.so) 파일이 있는 디렉터리의 경로를 작성한다.
.conf 파일을 생성하고 라이브러리 경로를 추가한 뒤에는 동적 링커 캐시를 업데이트 해줘야 한다.
$ sudo ldconfig
위의 명령어로 동적 링커 캐시를 업데이트 할 수 있다.
$ ldconfig -p | egrep "libadd"
ldconfig -p 명령어로 공유 라이브러리가 잘 등록 되었는지 확인할 수 있다.
3. 라이브러리 함수 호출 프로그램 생성
// Name : add.c
#include <stdio.h>
int main(){
printf( "[test] start!\n" );
add( 10, 10 );
printf( "[test] finish!\n" );
return 0;
}
정적 라이브러리의 테스트 코드와 동일하다.
$ gcc test.c -o test -ladd -L.
공유 라이브러리를 링크하여 컴파일한다.
- -l : lib 접두사 없이 라이브러리 이름을 가정한다. (libadd.so 이기 때문에 -ladd )
- -L : 라이브러리가 존재하는 디렉터리 지정 ( '.' 은 현재 디렉터리를 나타낸다)
ldd 명령을 사용하면 test 프로그램에서 libadd.so 공유 라이브러리를 참조하는 것을 알 수 있다.
프로그램을 실행하면
라이브러리 함수가 잘 실행된 것을 확인할 수 있다.
공유 라이브러리를 링크한 파일을 실행하면 동적 링커 로더(ld.so)가
공유 라이브러리와 실행 바이너리가 같은 프로세스 공간을 사용하도록 한다.
→ 실제 라이브러리 코드는 실행 파일에 포함되지 않고 공유 라이브러리에만 존재한다.
실행 파일을 배포할 때 공유 라이브러리를 함께 배포해야 한다.
4. PIC 알아보기
1. PIC (Position Independent Code)
공유 라이브러리를 컴파일 할 때 -fPIC 옵션을 사용했다.
-fPIC 옵션은 위치 독립 코드로 생성하는 옵션이다.
PIC 는 Position-Independent Code 로 메모리의 어느 공간에든 위치할 수 있고 수정 없이 실행되는
위치 독립 코드이다.
PIC 로 컴파일 된 코드를 사용하는 각 프로세스들은 이 코드를 서로 다른 주소에서 실행할 수 있다.
// Name : pic.c
#include <stdio.h>
void main() {
puts("hello");
puts("hello");
puts("hello");
}
puts 함수를 3번 호출하는 코드이다.
이제 PIC 로 컴파일 했을 때와 하지 않았을 때의 특징을 비교해보자
2. PIC 적용
$ gcc -shared -fPIC -o pic.so pic.c -m32
PIC 가 적용된 공유 라이브러리를 생성한다.
-fPIC 옵션을 적용하면 PIC 가 적용된 공유 라이브러리를 생성할 수 있다.
readelf 의 -d 옵션으로 확인해보자
- -d : dynamic 영역을 구체적으로 표시한다.
PIC 가 적용된 파일은 TEXTREL 엔트리 없는 것을 볼 수 있다.
TEXTREL 은 코드의 .text 섹션에서 재배치가 필요한 경우를 나타낸다.
그 다음 RELCOUNT 를 확인해보자
RELCOUNT 는 공유 라이브러리의 재배치 항목 수를 나타내는 태그이다.
PIC 는 재배치가 필요없지만 RELCOUNT 가 3 인 것을 볼 수 있다.
이는 gcc 가 기본적으로 사용하는 시작 파일에 포함된 코드 때문이다.
-nostartfiles 옵션을 넣고 컴파일 하게 되면 RELCOUNT 가 0이 되어 사라지게 된다.
$ gcc -shared -fPIC -o pic.so pic.c -m32 -nostartfiles
RELCOUNT 가 없어진 것을 확인할 수 있다.
PIC는 재배치가 필요하지 않다는 것을 알 수 있다.
3. PIC 적용되지 않음
$ gcc -shared -fno-PIC -o nopic.so pic.c -m32
PIC 가 적용되지 않은 공유 라이브러리를 생성한다.
-fno-PIC 옵션을 적용하면 PIC 를 적용하지 않은 공유 라이브러리를 생성할 수 있다.
readelf 로 확인해보자
TEXTREL 엔트리가 존재하므로 .text 영역의 재배치가 필요한 것을 알 수 있다.
RELCOUNT 는 6으로 실행 시 6개의 주소가 재배치 되어야 한다.
지금은 puts 함수를 3번 호출하는 간단한 프로그램이지만 프로그램의 크기가 커지게 되면
재배치 수가 늘어나 재배치에 걸리는 시간이 길어지게 된다.
또한 실행 시 재배치가 필요한 부분의 코드를 재작성 하기 위해 text 영역 내에서 재작성 하는 과정을 거치다가
copy on write 가 발생하여 다른 프로세스와 text 영역을 공유할 수 없게 된다.
→ 공유 라이브러리의 장점이 사라지게 된다.
copy on write?
PIC 가 적용되지 않은 라이브러리는 절대 주소를 사용한다.
→ 함수 호출 & 데이터 엑세스를 위해 특정 메모리 주소에 의존한다.
copy on write 에 대해 알아보기 전에 재배치에 대해 알아보자
공유 라이브러리가 프로세스에 로드되면 동적 링커가
라이브러리가 로드되는 실제 메모리 위치와 일치하도록 라이브러리의 코드를 수정한다.
이러한 과정을 재배치라고 한다.
공유 라이브러리가 여러 프로세스에 로드되면 처음엔 수정 사항이 없기 때문에
모든 프로세스가 .text 영역에 대해 동일한 라이브러리를 공유한다.
→ 메모리를 효율적으로 사용하게 된다.
재배치는 일반적으로 text(코드) 영역에서 이뤄진다.
.text 영역은 읽기 전용이기 때문에 재배치가 필요한 경우 운영체제가 일시적으로 쓰기 가능하게 만든다.
이 때 copy on write 가 발생한다.
공유 메모리 페이지를 수정하는 대신 운영체제는
재배치를 수행하는 프로세스를 위해 라이브러리의 복사본을 만든다.
원본 라이브러리는 변경되지 않고 계속 다른 프로세스와 공유된다.
→ 각 프로세스들이 자체 복사본을 가질 수 있기 때문에 메모리 사용량이 증가한다.
결과적으로 copy on write 가 발생할 때 마다 프로세스는 동일한 라이브러리를 공유하지 않게 된다.
공유 라이브러리를 쓰는 이유가 없어진다.
공유 라이브러리를 PIC 로 생성하지 않으면
- 재배치에 시간이 소요된다.
- 다른 프로세스와 라이브러리를 공유할 수 없다.
위와 같은 이유로 공유 라이브러리는 PIC 로 컴파일 된다.
5. PIC & non-PIC 비교하기
1. 함수 호출 방식
위에서 컴파일한 pic.so 와 nopic.so 를 비교해보자
pic.so 의 어셈블리를 확인하자
Dump of assembler code for function main:
0x00001020 <+0>: lea ecx,[esp+0x4]
0x00001024 <+4>: and esp,0xfffffff0
0x00001027 <+7>: push DWORD PTR [ecx-0x4]
0x0000102a <+10>: push ebp
0x0000102b <+11>: mov ebp,esp
0x0000102d <+13>: push ebx
0x0000102e <+14>: push ecx
0x0000102f <+15>: call 0x107b <__x86.get_pc_thunk.bx>
0x00001034 <+20>: add ebx,0x2fcc
0x0000103a <+26>: sub esp,0xc
0x0000103d <+29>: lea eax,[ebx-0x2000]
0x00001043 <+35>: push eax
0x00001044 <+36>: call 0x1010 <puts@plt>
0x00001049 <+41>: add esp,0x10
0x0000104c <+44>: sub esp,0xc
0x0000104f <+47>: lea eax,[ebx-0x2000]
0x00001055 <+53>: push eax
0x00001056 <+54>: call 0x1010 <puts@plt>
0x0000105b <+59>: add esp,0x10
0x0000105e <+62>: sub esp,0xc
0x00001061 <+65>: lea eax,[ebx-0x2000]
0x00001067 <+71>: push eax
0x00001068 <+72>: call 0x1010 <puts@plt>
0x0000106d <+77>: add esp,0x10
0x00001070 <+80>: nop
0x00001071 <+81>: lea esp,[ebp-0x8]
0x00001074 <+84>: pop ecx
0x00001075 <+85>: pop ebx
0x00001076 <+86>: pop ebp
0x00001077 <+87>: lea esp,[ecx-0x4]
0x0000107a <+90>: ret
End of assembler dump.
puts 함수 호출 시 puts@plt 를 call 한다.
plt, got 를 이용하여 puts 함수를 호출한다.
nopic.so 의 어셈블리를 확인해보자
Dump of assembler code for function main:
0x0000114d <+0>: lea ecx,[esp+0x4]
0x00001151 <+4>: and esp,0xfffffff0
0x00001154 <+7>: push DWORD PTR [ecx-0x4]
0x00001157 <+10>: push ebp
0x00001158 <+11>: mov ebp,esp
0x0000115a <+13>: push ecx
0x0000115b <+14>: sub esp,0x4
0x0000115e <+17>: sub esp,0xc
0x00001161 <+20>: push 0x2000
0x00001166 <+25>: call 0x1167 <main+26>
0x0000116b <+30>: add esp,0x10
0x0000116e <+33>: sub esp,0xc
0x00001171 <+36>: push 0x2000
0x00001176 <+41>: call 0x1177 <main+42>
0x0000117b <+46>: add esp,0x10
0x0000117e <+49>: sub esp,0xc
0x00001181 <+52>: push 0x2000
0x00001186 <+57>: call 0x1187 <main+58>
0x0000118b <+62>: add esp,0x10
0x0000118e <+65>: nop
0x0000118f <+66>: mov ecx,DWORD PTR [ebp-0x4]
0x00001192 <+69>: leave
0x00001193 <+70>: lea esp,[ecx-0x4]
0x00001196 <+73>: ret
End of assembler dump.
puts 함수 호출 시 0x1187 을 call 한다.
0x1187 이 어떤 영역인지 확인해보자
gdb 에서 info file 명령을 치면 확인할 수 있다.
pwndbg> info file
Symbols from "/home/gunp4ng/pwnable/pic/nopic.so".
Local exec file:
`/home/gunp4ng/pwnable/pic/nopic.so', file type elf32-i386.
warning: Cannot find section for the entry point of /home/gunp4ng/pwnable/pic/nopic.so.
Entry point: 0x0
0x00000154 - 0x00000178 is .note.gnu.build-id
0x00000178 - 0x00000198 is .gnu.hash
0x00000198 - 0x00000208 is .dynsym
0x00000208 - 0x00000287 is .dynstr
0x00000288 - 0x00000296 is .gnu.version
0x00000298 - 0x000002c8 is .gnu.version_r
0x000002c8 - 0x00000330 is .rel.dyn
0x00001000 - 0x00001024 is .init
0x00001030 - 0x00001040 is .plt
0x00001040 - 0x00001048 is .plt.got
0x00001050 - 0x00001197 is .text
0x00001198 - 0x000011b0 is .fini
0x00002000 - 0x00002006 is .rodata
0x00002008 - 0x0000202c is .eh_frame_hdr
0x0000202c - 0x000020ac is .eh_frame
0x00003f10 - 0x00003f14 is .init_array
0x00003f14 - 0x00003f18 is .fini_array
0x00003f18 - 0x00003ff0 is .dynamic
0x00003ff0 - 0x00004000 is .got
0x00004000 - 0x0000400c is .got.plt
0x0000400c - 0x00004010 is .data
0x00004010 - 0x00004014 is .bss
0x1187 은 .text 영역인 것을 알 수 있다.
6. Reference
https://blackperl-security.gitlab.io/blog/2016/05/03/2016-05-03-linux-03/
'Hacking > Pwnable' 카테고리의 다른 글
PIE (0) | 2024.10.13 |
---|---|
가젯 찾는법 (ROPgadget, ropsearch, rp-lin) (0) | 2024.09.18 |
ROP (x64) (0) | 2024.08.25 |
ROP (x86) (0) | 2024.08.16 |
RTL Chaning (x64) (0) | 2024.07.27 |