본문 바로가기

아이티 :D/Network

Pcap 파싱하기

1. Pcap 파일이란 무엇인가?


-Packet capture의 약자로 네트워크 트래픽을 캡쳐하기 위한 API로 구성되어 있습니다.

 윈도우 시스템에서는 Winpcap이며, 리눅스 시스템에서는 libpcap입니다.

 네트워크 트래픽을 분석하기에 아주 용이한 라이브러리의 모음이라고 볼 수 있습니다.


-Pcap file format

[글로벌헤더] + [패킷헤더 + 패킷데이터] + [패킷헤더 + 패킷데이터] ......


[글로벌헤더]

*magic_number: pcap 시그니쳐이며, "0xa1b2c3d4"의 고정값을 가집니다. (32bit)

*version_major, version_minor: pcap파일의 버전을 나타냅니다.(각 16bit)

*thiszone: GMT와 localT의 차이를 나타낸 필드. (32bit)

*sigfig: 0으로 값이 고정되어 있습니다. (32bit)

*snaplen: 캡쳐된 패킷의 길이를 나타냅니다. 보통은 65535값을 가집니다. (32bit)

*network: 데이터 링크 타입을 나타냅니다 OSI7layer에서 2계층을 명시합니다. (32bit)


[패킷헤더]

*ts_sec: 패킷이 언제 캡쳐되었는지를 나타냅니다. (32bit)

*ts_usec: ts_sec을 microseconds로 나타낸 값입니다. (32bit)

*incl_len: pcap에 저장된 패킷데이터의 길이를 나타냅니다. 이 값은 아래 설명 할 orig_len과 

 글로벌헤더 내의 snaplen보다 결코 클 수 없습니다.

*orig_len: 네트워크에서 캡쳐되었을때 패킷 데이터의 길이입니다. 만약 이 값과 incl_len의 값이 다르다면

 글로벌헤더 내의 snaplen의 값으로 패킷 사이즈가 결정됩니다.

2. Pcap 파일을 알기위한 전초전


HxD를 이용하여 pcap속으로 들어가서 하나하나 살펴보도록 하겠습니다.

pcap에서 첫번째 패킷데이터는 어디서부터 시작될까요?

정답은 글로벌헤더+패킷헤더 만큼의 사이즈 이후부터 패킷데이터가 시작합니다.

위 그림을 보시면 빨간 테두리로 둘러싸인 값들이 [글로벌헤더(24byte) + 패킷헤더(16byte)] 입니다.

이 값 이후에 " 00 1C 42 00 00 18 ......" 시작되는 값들이 실제 패킷데이터 부분입니다.

아래 그림에서 와이어샤크로 확인해보도록 하겠습니다.


[글로벌헤더(24byte) + 패킷헤더(16byte)] 이후에 나오는 값들과 위 그림에서 와이어샤크로 확인한 값과

동일한 것을 확인할 수 있습니다.

즉 pcap file은 처음에 [글로벌헤더]가 나온이후 [패킷헤더 + 패킷데이터] + [패킷헤더 + 패킷데이터]의

과정을 반복하면서 마지막 패킷까지 이루어져 있습니다.


지금부터는 패킷데이터들을 살펴볼텐데 대표적으로 [Ethernet frame], [IP frame], [TCP frame]

을 살펴보도록 하겠습니다.


-Ethernet frame: 데이터링크 계층에서 사용되는 대표적인 프로토콜


*preamble: 호스트들과의 비트 동기화를 위해 존재합니다. 물리계층에서 추가되어졌습니다.

*SFD: start of frame delimiter의 약자로 프레임의 시작을 알리는 필드입니다.

        다음 필드로 목적지주소 (DA)가 이어진다는 걸 알려줍니다. 

*Destination Address: 목적지 주소 입니다.

*Source Address: 출발지 주소 입니다.

*EtherType: 상위계층의 프로토콜 정보를 알려줍니다.

*Data: 데이터부분입니다.

*FCS: frame check sequence의 약자로 전송시에 수신측의 에러검출을 위해 존재합니다.

// preamble, SFD, FCS는 패킷 캡쳐 범위 밖입니다.//

// DA , SA, EtherType은 Ethernet header부분에 해당됩니다//





-IP frame: IP계층에서 사용되는 대표적인 프로토콜


*Version: 버전을 나타내는 필드로 현재는 4버전을 사용하고 있다.

*Header Length: 헤더의 길이를 나타내는 필드로 20 ~ 60byte까지 가변적인 값을 가집니다.

워드 단위로 헤더 길이를 표시합니다.

*Type Of Service: 서비스의 우선순위를 나타내는 필드이지만 IPv4에선 사용되지 않습니다.

*Total Packet Length: IP패킷 전체의 길이를 바이트 단위로 나타냅니다.

*Identifier: fragment 되었을때 어떤 원본 패킷에 일부인지를 확일 할 수 있는 식별자입니다.

*Flags: 3비트의 크기를 가지는데 1번째 비트는 0으로 고정된 값을 가지며

    2번째 비트: 0과 1로 구분되며 0은 fragment 가능, 1은 fragment 불가능

    3번째 비트: 0과 1로 구분되며 0은 마지막 조각, 1은 뒤에 조각이 더 있음

*fragment offset: 분할된 조각의 순서를 알려주는 필드이다.

분할된 조각의 시작점 위치에서 8을 나눈 값을 가지게 되는데 예를 들어

1500바이트의 패킷이  512바이트의 패킷 3개로 fragment되었다고 가정할때

1번째 패킷은 0바이트 부터 시작하기에 fragment offset의 값이 0이되고

2번째 패킷은 512부터 시작하기에 512/8하면 fragment offset의 값이 64가 된다.


*Time to live: 간단하게 말하면 IP패킷의 수명으로 볼 수 있는데 , 1 ~ 255까지의 값을 가지며,

  라우터를 지날 때 마다 1의 값이 감소한다.

*Protocol ID: 상위계층의 프로토콜 타입을 지정해주는 필드입니다.

1: ICMP

2: IGMP

6: TCP

17: UDP

*Header Checksum: 헤더에 대한 오류검출을 하는 필드입니다. 목적지에서 이 필드를 확인하여 결과가

   틀리다면 패킷을 버립니다.

*Source IP Address: 출발지 IP주소입니다.

*Destination IP Address: 목적지 IP주소입니다.

*IP Header Options: IP헤더의 필수적인 부분은 아닙니다. 그냥 옵션이 필요하다면 정의할 수 있습니다.

// 별다른 옵션이 추가가 안된다면 기본적으로 20byte로 구성됩니다.//


-Tcp frame: 전송계층에서 사용되는 대표적인 프로토콜 중 하 나

IP가 목적지까지 패킷을 전송 후 TCP는 패킷의 누락

또는 순서의 정렬등을 책임집니다.

때문에 Tcp는 신뢰성있는 프로토콜이라고 불리며, 연결지향적입니다.


각 필드들을 설명하기 전에 MTU와 MSS에 대해서 간단히 설명하겠습니다.

//MTU: Maximum Transmission Unit의 약자로 한번의 프레임에 전송할 수 있는

       최대크기의 데이터 또는 패킷이며, 보통은 1500의 크기를 가집니다.

MSS: Maximum Segment size의 약자로 TCP가 한번에 전송 할 수 있는 최대크기의 데이터를 말합니다.

  이 값은 MTU값에 의해 좌우되는데 MSS = MTU - IPheader + Tcpheader 의 공식을 가집니다.//

각 필드들을 설명하겠습니다.


*Sourse Port: 출발지 포트주소입니다.

*Destination Port: 목적지 포트주소입니다.

*Sequence Number: 예로 1500byte 패킷을 전송할 때 MSS의 크기가 100이라고 하면 15개의

   세그먼트로 쪼개져서 전송이 됩니다. 이때 Sequence Number가 각각의 세그먼트의

   첫번째 바이트에 해당하는 값을 번호로 붙입니다. 1번째 패킷은 0~99 까지니깐

   "0", 2번째 패킷은 100~199니깐 "100" 3번째는 "200"

*Acknowledgement Number: 전송 받은 패킷에 대한 응답인데 위 예를 그대로 이어 가겠습니다.

    1번째 패킷을 전송 받은 수신자는 0 ~ 99 까지의 데이터를 받았기때문에

    다음은 100 ~ 199까지의 데이터가 필요로 하게 됩니다.

    그래서 100이라는 Ack Number를 응답패킷으로 보내게 됩니다.

    즉 받은 패킷의 양 + 1의 값을 더하여 Ack number가 구성되는거죠

    Sequence 와 Ack Number가 이해가 되셨나요??..되셨으면 좋겠어요

*HLEN: TCP세그먼트의 길이를 나타냅니다.

*Reserved: 예약필드인데 나중에 사용하기 위해 예약되어 있으며 0으로 세팅되어 있습니다.

*Codebits: 6개의 비트로 이루어져 있고 세그먼트의 용도와 내용을 결정하는 필드입니다.

URG: 긴급 데이터라는 뜻이며, 이 비트값이 채워지면 순서에 상관없이 먼저 송신됩니다.

ACK: Ack number의 유무를 알리는 비트이며, 0은 Ack num이 없다는 뜻이며 1은 있다는 뜻

  입니다. SYN패킷 이후 세그먼트들은 전부 이 비트값이 1로 채워집니다.

PSH: 이 비트값이 세팅되면 수신측은 받는 즉시 Application단으로 송신해야합니다.

RST: 연결장비에 문제가 생겼으므로 초기화를 진행해야 한다는 뜻이며, 이 값이 채워지면

  연결이 종료됩니다.

SYN: TCP연결을 할 때 양 호스트간 순서번호를 동기화 하는 목적으로 사용됩니다.

FIN: 송신측이 데이터 송신을 마쳤다는 뜻이며 연결 종료를 요청한다는 뜻입니다.

*Window Size: 흐름제어를 위해 사용되는 필드로 상대편에게 자신의 여유 버퍼크기를 지속적으로

   알려주는 역할을 합니다.

*CheckSum: 전송중에 Tcp segment 데이터는 훼손이나 변조가 될 수가 있는데 이를 확인하기 위한

     필드입니다.

*Urgent Pointer: Codebits의 URG비트와 같이 사용되며, 긴급데이터의 마지막 바이트 순서번호입니다.

*Option & Padding: Option 필드는 말그대로 Tcp 헤더에 옵션을 추가하는거고 Padding은 

   이 Option필드가 32의 배수가 되지 않으면 0을 추가하여 배수가 되도록 정렬하는

   역할을 합니다.

// 별다른 옵션이 추가가 안된다면 기본적으로 20byte로 구성됩니다. //

3. Pcap parser 만들기!


제가 만들 파일은 TCP에서 80port(http) 통신할때 전송하는 파일을 제가 원하는 폴더에 

드랍시키는것입니다. 과정중에 각 패킷별로 Layer정보도 print 하도록 하겠습니다.

그럼 하나하나 파싱을 시작해볼게요.


1) Layer2 information

Lay2는 Ethernet frame속의 Ethernet header 필드를 사이즈에 맞게 파싱합니다.

목적지주소(6byte) + 출발지주소(6byte) + 상위계층 프로토콜타입 (2byte) 입니다.

위에서도 설명했듯이 패킷데이터는 [글로벌헤더 + 패킷헤더] 다음에 존재합니다.

1번째 패킷은 [글로벌헤더 + 패킷헤더] + [해당 패킷 데이터의 길이] 만큼의 값을 가지겠네요.

2번째 패킷은 [글로벌헤더 + 패킷헤더] + [1번째 패킷 데이터의 길이] 만큼의 값 다음에 존재하게 됩니다.


//배열을 선언하여 각 패킷의 순서, 길이, 데이터를 구하는 코드입니다.

idx라고 표현한 값이 파일 사이즈보다 작을때까지 (즉 데이터사이즈가 파일사이즈보다 커지기 전까지)

pkg_list()는 푸틴이 진행되면서 1번째 인자로 인덱스를, 2번째 인자로 길이를, 3번째 인자로 데이터를 

저장하게 됩니다. 

그리고 idx에는 idx + PKG_H_SIZE(패킷 헤더사이즈) + pkg_length(패킷 데이터길이)가더해지면서 각 패킷의 순서를 매겨가게 됩니다.

목적지주소: data[0:6]

출발지주소: data[6:12]

상위계층 프로토콜 타입: data[12:24]



1번째 사진은 와이어샤크로 확인한 1번째 패킷의 이더넷 부분입니다.

목적지주소와 출발지주소 그리고 프로토콜 타입의 값들과 아래 사진에서 보시는

제가 파싱한 값과 일치하는걸 확인 할 수 있습니다.


2) Layer 3 information

Lay3는 IP frame속의 IP header 필드를 사이즈에 맞게 파싱합니다.

IP정보는 [글로벌헤더 + 패킷헤더] + [Ehternet Size] 다음에 위치하게 됩니다.

여기서 Ethernet header는 위에서도 언급했듯이 패킷 데이터 = Ethernet frame속의 Ethernet header필드입니다.

data[0:14]까지가 Ethernet 정보를 담고 있기때문에 data[14:34]까지가 IP 정보를 가지고 있습니다.

하지만 저희는 Total packet length, protocol type, source ip, destination ip 필드만  프린트하도록 하겠습니다.

Total Packet Length: data[ETHER_SIZE+2:ETHER_SIZE+4]

source IP: data[ETHER_SIZE+12:ETHER_SIZE+16]

destination IP: data[ETHER_SIZE+16:ETHER_SIZE+20]

protocol IP: data[ETHER_SIZE+9:ETHER_SIZE+10]



Total Packet Length = 0x005f = int(95)

source IP = 0x0a.d3.37.03 = int(10.211.55.3)

destination IP = 0x6d.65.48.1b = int(109.101.72.27)

protocol ID = 0x11  = int(17)

값들이 정확하게 파싱되었습니다.

참고로 위 IP들은 쉬프트연산을 이용하여 오른쪽으로 이동시켜줘야합니다.

왜냐면 0a.d3.37.03 으로 끊어서 형변환을 해서 프린트 하기 위함이죠

아니면 0x0ad33703이 한번에 변환되서 엄청난 값이 프린트될거에요.


3) Layer 4 information

Lay4는 TCP정보를 파싱합니다.

TCP정보는 [글로벌헤더 + 패킷헤더] + [Ehternet Size] + [IP Size] 다음에 위치하게 됩니다.

source port, destination port, sequence number, code bits(flag information)필드를 프린트 하겠습니다.



source port: data[ETHER_SIZE+IP_SIZE:ETHER_SIZE+IP_SIZE+2]
dst port: data[ETHER_SIZE+IP_SIZE+2:ETHER_SIZE+IP_SIZE+4]
sequence number: 시퀀스 넘버는 와이어샤크상에서 특별한 값을 가집니다.
 보안상의 문제로 인하여 랜덤한 값 4바이트가 바이너리에 저장되게 되는데요
 어차피 그래도 특정한 값은 들어가니깐 그 값을 기준으로 넥스트 시퀀스 넘버와
 에크넘버를 구하면 됩니다.
 넥스트 시퀀스 넘버를 구하는 공식은 시퀀스넘버 + segment length 값입니다.
 와이어샤크로 확인해보겠습니다.

 



첫번째 사진부터 살펴보시면 Tcp segment length가 "604"이고 Sequence Num이 "1"입니다.

그리고 Next Sequence Num은 "605"라고 되어있습니다. 바이너리 값을 한번 확인해주세요

시퀀스 넘버인 00 00 00 01이 아닌 c5 9a 86 9e 라는 랜덤한 값이 들어가 있습니다.

그럼 두번째 사진을 살펴보실까요?

2번째로 단편화된 패킷입니다. 시퀀스넘버가 "605"부터 시작되는걸 확인할 수 있습니다.

그리고 바이너리 값도 아까 첫번째 사진에서 본 값에서 헥스값으로 "604"만큼의 값이 증가한 걸

알 수 있습니다.

그럼 Ack Num은 어떻게 될까요?

1번째로 단편화된 패킷에서 시퀀스 넘버는 "1", 당연히 Ack num은 "0"이겠죠?

Next Sequence값이 "605"이기 때문에 1번째 패킷을 받은 수신자측은 "605"의 Ack num을 

응답 패킷에 담아 보내게 됩니다.


"605"의 Ack num을 응답패킷으로 날리는걸 확인 할 수 있습니다.

바이너리상에 Ack num은 2번째 사진에서 Sequence Num인 "605"와 동일하겠죠?

동일하네요.

아까 위 Tcp frame에서 확인 한 내용입니다. 하지만 실제로 이 부분이 제일 까다롭습니다.

단편화된 세그먼크들을 순서에 맞게 정렬하여 하나의 파일로 만드는 작업이 제일 시간도

많이 걸리고 고생했습니다..ㅠㅠ 제가 완전 초보라서 진짜 어떻게 해야 할지 막막했습니다.

Sequence 와 Ack number는 이렇게 넘길게요

Code bits(Flag information): data[ETHER_SIZE+IP_SIZE+13:ETHER_SIZE+IP_SIZE+14]

    flags는 FIN = 0x01,SYN = 0x02,RST = 0x04,PSH = 0x08

    ACK = 0x10,URG = 0x20

    이렇게 변수를 선언한 후 바이너리상에 저장된 flag의 합을 얻어와서

         위 변수 값들이 속해있으면 print 되도록 구성했습니다.


TCP정보를 포함하고 있는 패킷을 확인해보겠습니다.

flag값이 SYN 하나밖이네요. 아마 처음 시작하는 패킷인가 봅니다.

그럼 Code bits 값이 0x02가 바이너리상에 저장되어 있겠네요 와이어샤크로 확인해보겠습니다.

와이어샤크에서 확인 한 결과 정확하게 SYN값이 저장되어 있네요.


이렇게해서 Layer4까지 파싱해서 print하는건 완료되었습니다.

이젠 TCP 80port http상에서 전송중인 파일을 로컬로 저장시키는 작업을 진행하겠습니다.


4. pcap상에서 전송되는 파일 로컬로 저장시키기!

이번 작업에서는 아래 2가지를 중점으로 진행하겠습니다.

- 단편화된 세그먼트들 순서 정렬하여 하나의 파일로 합치기

- 각 파일의 시그니쳐를 저장하고 그 시그니쳐를 기반으로 파일의 종류를 결정하기

http1.1에서는 커넥션이 끊어지지않고 연결되어있는데 그렇다면 본문내용이 얼마만큼인지, 

어디가 끝인지를 어떻게 확인할까요? 2가지 방식이 있는데 1번째는 Contents-Length를 보여주는 방식입니다.

1) 단편화된 세그먼트 정렬하여 하나의 파일로 합치기

*단편화된 세그먼트들이 최종적으로 하나로 합쳐졌을때 Contents-Length 값은 단편화된 세그먼트들을

모두 합친값.

*헤더값을 제외하고 데이터만 뽑기 위해서는 "\r\n\r\n" 값 이후의 값들을 Dump합니다.

http헤더값을 제외한 후 진짜 데이터시작부분 (파일 시그니쳐 부분)부터 Dump해야 

데이터만 추출되기 때문입니다. http헤더 마지막에 0d0a = "\r\n"이 나오고 파일시그니쳐 앞에

0d0a = "\r\n"이 한번더 나오는데 즉 2번연속 0d0a0d0a = "\r\n\r\n"값 이후로 파일시그니쳐가

바로 시작됩니다. 이 포인트를 기준으로 잡습니다.

*len(dump)의 값을 변수로 잡아줍니다.

*Next Sequence Number를 구하는 공식은 아래와 같습니다.

현재 패킷의 Sequence Number + Total Packet Length - [글로벌헤더 + 패킷헤더]입니다.

즉 현재 패킷에서 Next Sequence Number를 구할 수가 있습니다.

와이어샤크상에선 표시되지만 정작 바이너리상에선 Next Seqence Number는 저장되어있지 않습니다.

*패킷 카운트만큼 반복 루틴을 이용하여 현재 시퀀스 넘버와 넥스트시퀀스 넘버가 같다면 

현재 dump file에 해당 찾은 패킷의 dump를 더하고 루틴을 계쏙해서 반복해나갑니다.

언제까지 반복할까요?

Contents-Length와 len(dump)의 값이 같아질떄까지 반복합니다.

저 두 값이 같아졌다는 말은 단편화된 세그먼트들이 최종적으로 합쳐졌다는 의미입니다.

여기서 break문을 걸고 파일을 로컬로 저장하면 완료입니다.


위 사진은  단편화된 세그먼트들이 최종적으로 합쳐졌을때의 Contents-Length값입니다.

확인해보도록 하겠습니다.



위 Content-Length값과 와이어샤크로 확인한 9992번째 패킷의 Contents-length값이 "2059829"로 

동일한 것을 확인할 수 있습니다.

넥스트 시퀀스 넘버를 가진 패킷을 찾으면 "\r\n\r\n"이후로 dump를 뜨고 기존의 dump에 더해주는 과정을 계속 반복합니다 

Content length와 len(dump)값이 같아 질 때까지.

같아지면 로컬에 파일을 저장합니다.

위 사진은 실제로 http헤더의 마지막에 "\r\n\r\n"이 존재하는 모습입니다 이후로 데이터만 존재하게 됩니다.

2)file Signature

* http://forensic-proof.com/archives/300 에서 파일시그니쳐들을 확인하실 수 있습니다.


예로 위의 시그니쳐들을 전역 변수로 선언하였습니다.

시그니쳐를 찾는 함수입니다.

"\r\n\r\n"을 분기점으로 이후에 바로 파일 시그니쳐가 나오기때문에 위와같이 코드를 작성하였습니다.

예로 png mp4 gif mp3 파일만 등록해놨습니다.


이렇게 프로그램을 구성하고 최종적으로 pcap속의 파일들을 추출 해봤습니다.


패킷속에 gif png파일들은 다 추출되네요..

시그니쳐들을 더 추가하시면 그리고 패킷속에 해당 파일들이 존재한다면 다 추출이 될거같습니다.

파일의 이름도 파일 종류별로 폴더에 정리하는것도 해주면 좋겠다는 생각이 듭니다.


여기까지 pcap파일의 구성과 각각의 내용을 파싱해보고 또 전송된 파일들을 로컬로 저장시키는것 까지

해보았습니다.

많이 부족하고 두서도없고 정리도 잘안되어있지만 열심히 했으니 혹시라도 정말

이 포스팅을 보고 참고를 하는 분들이 계시다면 얼마나 감사할까요

너무 허접해서 소스 전부를 못올리는점 양해부탁드리고

이렇게 포스팅을 마치도록 하겠습니다.

감사합니다..^^