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.solibc와 같은 필요한 .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가 뜨면서 에러가 발생하는 경우가 있는데, 이는 원하는 라이브러리가 로딩은 됐지만 바이너리 내부에 링킹되어있는데 libcld가 서로 맞지않기 때문에 발생한다.

이에 따라 patchelf 명령어를 통해 libcld 모두를 맞춰줄 필요가 있다.

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 변경을 위해 두번 적용해야함을 알았을 때는 참 힘들었다…

그래도 많이 배울 수 있었으니까~