patchelf 사용법 및 libc/ld 개념
System Hacking - patchelf 사용법 및 libc/ld 개념
1. libc
libc
란 C 표준 Library를 가리키는 말로, C언어로 작성된 프로그램이 동작하는데 필요한 기본 함수들을 제공한다.
대표적으로 GNU C Library인 glibc가 있고, 우리가 아는 printf
, scanf
, malloc
등 대다수의 주요 함수들이 포함되어있다고 보면된다.
일반적으로 아래 위치에 존재한다.
/lib/x86_64-linux-gnu/libc.so.6
2. ld.so (interpreter)
ld.so
는 동적 링커 (Dynamic Linker/Loader)를 가리키는 말로, 실행 시 프로그램이 필요로하는 .so (공유 객체, Shared Object)
파일들을 메모리에 로드하고 연결하는 역할을 한다.
실행 파일이 실행되면 커널은 ELF 헤더에 지정된 ld.so
를 호출하고,
ld.so
는 libc
와 같은 필요한 .so
파일들을 찾아서 메모리에 매핑하고 심볼을 연결한다.
그 뒤에 main()
함수로 점프하여 프로그램을 시작하게 된다.
일반적으로 아래 위치에 존재한다.
/lib64/ld-linux-x86-64.so.2
3. patchelf
3.1. patchelf란
patchelf
는 ELF 바이너리의 Program Header Table과 일부 Section Header 정보를 직접 편집하는 명령어이다.
이 명령어를 통해
-
interpreter
(ld.so
경로)를 변경 - rpath (runpath)를 설정
- 의존
.so
파일 변경 등을 수행할 수 있다.
3.2. CTF에서 patchelf의 필요성
CTF 문제에서 libc 파일이 제공될 때가 있다. 이 경우 LD_PRELOAD
를 통해 바이너리를 실행시키면 제공된 라이브러리가 로드되도록 할 수 있다.
LD_PREALOD=./libc.so.6 ./binary
이렇게 했을 때 segmentation fault가 뜨면서 에러가 발생하는 경우가 있는데, 이는 원하는 라이브러리가 로딩은 됐지만 바이너리 내부에 링킹되어있는데 libc
와 ld
가 서로 맞지않기 때문에 발생한다.
이에 따라 patchelf
명령어를 통해 libc
와 ld
모두를 맞춰줄 필요가 있다.
3.3. 주요 기능
3.3.1. 의존 라이브러리 이름 변경
사용할 라이브러리를 변경/교체한다.
patchelf --replace-needed libold.so libnew.so ./binary
3.3.2. RPATH 설정
라이브러리 (libc
)의 탐색 경로를 변경한다.
patchelf --set-rpath './lib' ./binary
3.3.3. 인터프리터 변경
ELF 헤더에 정의된 ld.so
의 경로를 변경한다.
patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 ./binary
3.4. ld는 어디서 구하나?
libc
만 제공되고 ld
파일은 거의 제공되지않는 것 같다. 이럴 때는 제공된 libc
의 버전이 뭔지를 먼저 확인해야한다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ strings libc |grep GNU
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et al.
Compiled by GNU CC version 5.4.0 20160609.
GNU Libidn by Simon Josefsson
이렇게 strings
명렁어를 통해 확인할 수 있다. 이 libc
파일의 버전은 Ubuntu GLIBC 2.23-0ubuntu11
이다.
자 그러면, 이 libc
파일을 링킹해주는 ld
파일을 찾아보자. 그냥 구글링을 했다…
여기서 deb
파일을 찾을 수 있는데 이걸 다운받아서 아래와 같이 압축을 풀어준다.
dpkg-deb -x libc6_2.23-0ubuntu11_amd64.deb [new_lib_dir_name]
그리고 압축이 풀린 디렉터리를 내려가보면 ld
파일 뿐만 아니라 제공된 libc
파일도 함께 확인이 가능하다.
4. 실제 예시
Dreamhack에서 제공된 oneshot
바이너리를 이용하여 변경해보자.
이 바이너리를 그대로 실행하는 것 자체는 문제는 없지만, CTF 문제 풀이에 사용하게 되면 각종 심볼의 위치와 함수 주소들이 달라지기 때문에 문제가 생긴다.
예를 들어 oneshot
문제를 풀기위해서 one_gadget
을 사용했었는데, 제공된 libc
를 이용해서 one_gadget
의 주소를 확인해보면
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
이렇게 보인다.
그런데 ldd oneshot
을 통해 확인한 libc
를 기준으로 확인해보면 아래와 같다.
0x583ec posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rax == NULL || {"sh", rax, rip+0x17301e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0x583f3 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ)
constraints:
address rsp+0x68 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, rip+0x17301e, r12, ...} is a valid argv
rbx == NULL || (u16)[rbx] == NULL
0xef4ce execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
rbx == NULL || {"/bin/sh", rbx, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp
0xef52b execve("/bin/sh", rbp-0x50, [rbp-0x78])
constraints:
address rbp-0x50 is writable
rax == NULL || {"/bin/sh", rax, NULL} is a valid argv
[[rbp-0x78]] == NULL || [rbp-0x78] == NULL || [rbp-0x78] is a valid envp
아예 들어가있는 내용과 주소 등 모든게 달라진 것을 볼 수 있다.
다른 주소를 사용하면 기껏 작성한 exploit이 (로컬에서는) 전혀 작동하지 않을 것이기 때문에, 바이너리에 적용된 라이브러리와 동적 링커를 patchelf
를 통해 변경해주면 로컬에서도 실험을 수행할 수 있다.
4.1. patchelf 적용 순서
patchelf
를 적용할 때는 아래 순서로 적용한다.
- patchelf –replace-needed
- patchelf –set-rpath
- patchelf –set-interpreter
이 순서로 적용하면서 전체적인 흐름을 볼 것이다.
4.2. 라이브러리 의존성 확인
먼저 제공받은 oneshot
의 라이브러리 의존성을 확인해보자.
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ ldd oneshot
linux-vdso.so.1 (0x00007ffcc0d41000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000760510000000)
/lib64/ld-linux-x86-64.so.2 (0x00007605109a8000)
사용 중인 libc
는 현재 내 OS의 libc.so.6
을 사용하고 있고, ld
도 마찬가지이다.
readelf -d
명령어를 통해서 ELF 동적 섹션에 있는 심볼 정보를 보면 첫째 줄에 (NEEDED)
가 Shared library: [libc.so.6]
임을 볼 수 있다.
readelf -l
명령어를 통해서 ELF 헤더를 보면 INTERP
헤더와 바로 아래에 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
를 볼 수 있고, LOAD
섹션이 이어지는 것을 볼 수 있다.
4.3. 라이브러리 교체
그러면 먼저 라이브러리를 교체한 후 결과를 보자.
일단 변화가 없는 것을 볼 수 있다. 이는 변경한 libc
파일의 이름을 똑같이 했기 때문인데 만약 libc
파일의 이름을 다른 것으로 해서 하면 달라진 결과를 볼 수 있다.
4.4. RPATH 설정
이 상태에서 바이너리의 라이브러리 의존성을 확인해보면 아래와 같이 보인다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ ldd oneshot
linux-vdso.so.1 (0x00007ffd21dc2000)
libc => not found
libc
가 있지만, 찾지를 못하고 있다. 이는 RPATH
가 제대로 설정이 되어있지 않기 때문으로, libc
파일이 존재하는 현재 디렉터리로 RPATH
를 설정해주자.
변경한 후에 결과를 보면 (NEEDED)
섹션 위에 RUNPATH
섹션이 추가되었고, Library runpath: [.]
로 설정된 것을 볼 수 있다.
또한 라이브러리 의존성을 확인해보면 libc
파일을 이번에는 제대로 찾고 있음을 볼 수 있다.
4.5. interpreter 변경
이제 interpreter
를 변경해줄 차례이다.
변경하지 않고 바로 실행시키면 아래처럼 segmentation fault를 볼 수 있다..
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ ./oneshot
Segmentation fault (core dumped)
그런데 처음 interpreter
를 변경하면 제대로 실행되지않고, 라이브러리 의존성도 확인이 안된다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ patchelf --set-interpreter ld-linux-x86-64.so.2 oneshot
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ ldd oneshot
ldd: exited with unknown exit code (139)
이것 때문에 한참 헤맸는데 결과적으로 readelf -l
명령어를 통해 문제를 알 수 있었다.
INTERP
섹션이 가장 아래 순서로 내려왔고, [Requesting program interpreter: ld-linux-x86-64.so.2]
까지 볼 수 있지만, 그 밑에 보여야할 LOAD
섹션이 보이지 않는다. 이 때문에 프로그램을 실행시키는 OS 입장에서는 해석이 안되니까 실행이 안되는 것이다.
어떻게 해결하지 싶었지만 다시 한번 interpreter
변경 명령어를 수행해보면 결과가 제대로 나오는 것을 볼 수 있다.
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ patchelf --set-interpreter ld-linux-x86-64.so.2 oneshot
ubuntu@instance-20250406-1126:~/dreamhack/level1/oneshot$ ldd oneshot
linux-vdso.so.1 (0x00007ffddf3c8000)
libc => ./libc (0x000074dfff200000)
ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x000074e0005ad000)
이렇게 되는 이유는 사실 정확히 모르겠다. GPT에게 물어보면서 확인한 바로는, patchelf
가 ELF 구조를 재조정하면서 기존에 존재하던 요소들이 임시로 제거되거나 위치가 바뀌면서 발생하는 문제일 수 있다고 한다.
Dreamhack에서 제공한 hook
바이너리에 처음 patchelf
를 수행했었는데, 이때는 별 문제가 없었다.
심지어 RPATH
도 설정하지않아도 해결이 됐어서 이렇게 쉽게 되는구나~ 했었다.
그런데 oneshot
바이너리에도 똑같이 적용하려하니 너무 많은 문제와 에러를 볼 수 있었고, 이를 해결하려고 하다보니 많은 공부가 된 것 같다.
마지막 interpreter
변경을 위해 두번 적용해야함을 알았을 때는 참 힘들었다…
그래도 많이 배울 수 있었으니까~