1.OpenVPN을 이용한 VPN 환경 구축

OpenVPN은 오픈 소스 프로그램으로 가상 사설망을 구축할 수 있는 소프트웨어다. 이와 비슷한 프로토콜로 PPTP와 L2TP/IPsec이 있다. 최근의 추세는 IPsec 혹은 OpenVPN 중 하나로 구축하는 것 같다. IPSec은 하드웨어 장비를 통해서 구현하는 경우가 많은데, 음.. 글쎄 돈이 남아돌지 않는다면 굳이 IPSec 기반으로 구현할 필요가 있을까 라는 생각을 해본다.

OpenVPN은 SSL기반의 VPN으로 openssl라이브러리를 사용한다.


 

1.jpg

 

위의 그림은 OpenVPN 서버와 클라이언트에 tun 디바이스가 만들어지고, 이 디바이스를 이용해서 10.8.0.0 주소영역을 가지는 사설망이 만들어 진것을 보여준다.


 

TUN방식은 다음과 같은 장점을 가진다.

  • 네트워크 디바이스를 생성함으로써, 네트워크 구조가 명확하고 비교적 안정적으로 작동한다는 장점을 가진다.

  • 고정 IP를 할당할 수 있어서 위치에 관계 없이 안정적으로 사설망을 유지할 수 있도록 한다.

  • 사설망을 위한 DHCP, 네임서버를 구축할 수 있다.

1.1 기본 원리

VPN은 물리적으로 떨어져 있는 두개의 네트워크를 가상의 네트워크로 묶는 개념이다. 두개 이상의 네트워크를 하나의 네트워크처럼 보이게 하려면 어떻게 해야 할까 ? Tunnel(터널)을 뚫으면 된다.


 

아래와 같이 간단하게 설명할 수 있다.


 

2.jpg

 

  • 10.10.0.0/16 네트워크와 10.50.0.0/16 네트워크가 internet(public network)를 사이에 두고 서로 떨어져 있다. 이들 네트워크는 사설 네트워크이기 때문에 트래픽을 전달할 수 없다.

  • 하지만 사설 네트워크에는 internet으로 트래픽을 보내기 위한 gateway가 있을 것이고, 이들은 public ip를 가지고 있을 것이다.


 

두 개의 사설 네트워크가 gateway를 통해서 연결이 되므로, 원칙적으로 사설 네트워크 끼리 통신이 가능하다.. 편지를 보내는 것처럼, 패킷을 한번 더 싸서 보내면 된다.

 

1.2 VPN 구성

아래는 VPN 를 구축한 사례다. eth0 네트워크 디바이스를 사용하는 222.x.189.97 이라는 공인IP 로 등록된 vpn 서버 1대가 있고, 내부방으로 구성된 2대의 서버가 추가적으로 있는 상태다. eth1 네트워크 디바이스를 이용해서 내부네트워크와 통신을 하고있다.

외부 클라이언트 가 내부네트워크 서버에 ssh 및 ftp 등 접근시도를 할려면 vpn 서버를 통하여 접근시도하는 방법밖에는 없는 상태라고 가정하고 구축을 해보도록 하겠다.

3.jpg

 

1.3 OpenVPN 서버 설치

1.3.1 CentOS

CentOS 6.3을 기준으로 한다. centos는 EPEL레포지토리를 등록해야 한다.

wget http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
rpm -Uvh epel-release-6-8.noarch.rpm
yum install openvpn -y
 


 

키 관리를 도와주는 easy-rsa를 따로 설치해야 한다.

yum install easy-rsa -y
 


 

easy-rsa 파일들을 /etc 밑에 복사한다.

mkdir /etc/openvpn/easy-rsa

cp -rf /usr/share/easy-rsa/2.0/* /etc/openvpn/easy-rsa
 


 

OpenVPN은 Open SSL 라이브러리를 사용한다. 이를 위해서 openssl 설정파일을 만들어야 하는데, 미리 만들어져 있는 예제 설정파일을 복사해서 사용하면 된다. openssl 버전에 맞는 파일을 사용하자. "최대한 편하고 쉽게!!!" easy-rsa를 설치한 이유다.

cp /etc/openvpn/easy-rsa/openssl-1.0.0.cnf /etc/openvpn/easy-rsa/openssl.cnf
 


 

easy-rsa 디렉토리 밑에 있는 vars 파일을 편집한다. 다른 건 수정할 필요 없다. "KEY_"로 시작하는 변수들만 수정하자.

# vi /etc/openvpn/easy-rsa/vars
 

대략 아래와 같은 환경변수들을 수정하면 된다.

export KEY_COUNTRY="US"
export KEY_PROVINCE="NY"
export KEY_CITY="New York"
export KEY_ORG="Organization Name"
export KEY_EMAIL="administrator@example.com"
export KEY_CN=droplet.example.com
export KEY_NAME=server
export KEY_OU=server
 


 

var를 적용하고, key를 빌드하기 시작한다. key 를 생성하게되면 현재 디렉토리에서 keys  라는 디렉토리가 생성된다.

cd /etc/openvpn/easy-rsa

chmod +rwx *
source ./vars
./clean-all
./build-ca
 

OpenVPN 서버에 필요한 키를 생성하기 위한 CA를 만든다.

./build-key-server server
 

[OpenVPN 클라이언트 사용자 키 추가]

##### 사용자별로 키 값을 생성해야된다. 접속하는 사용자가 10명이면 10개의 사용자 키 생성 해야됨.

VPN 을 사용할 클라이언트의 사용자 키를 만들어 줍니다. 이것 역시 서버키 생성과 마찬가지로 마지막 물어보는 2가지 항목에서 y 를 눌러 동의 하고 넘어 갑니다.

./build-key dslee


 

이제 최종적으로 pem 파일을 만들어 줍니다.

./build-dh

 


 

1.4 OpenVPN 서버 설정

이제 남은 건 설정파일이다. OpenVPN을 소개하는 책이 따로 출판되었을 정도로 OpenVPN은 많은 기능을 제공한다. 여기에서는 TUN 디바이스를 이용해서 step 3 VPN 환경 구축을 할 것이다.


 

vi /etc/openvpn/server.conf


 

아래내용 추가

###################################################

port 1194

proto tcp

dev tun

ca /etc/openvpn/easy-rsa/2.0/keys/ca.crt

cert /etc/openvpn/easy-rsa/2.0/keys/server.crt

key /etc/openvpn/easy-rsa/2.0/keys/server.key # This file should be kept secret

dh /etc/openvpn/keys/dh2048.pem

server 10.8.0.0 255.255.255.0

ifconfig-pool-persist ipp.txt

push "redirect-gateway def1 bypass-dhcp"

push "dhcp-option DNS 168.126.63.1"

push "dhcp-option DNS 168.126.63.2"

client-to-client

keepalive 10 120

comp-lzo

plugin /usr/lib64/openvpn/plugin/lib/openvpn-auth-pam.so login

user nobody

group nobody

persist-key

persist-tun

status openvpn-status.log

log /var/log/openvpn.log

verb 9

mute 10

crl-verify /etc/openvpn/keys/crl.pem

##########################################################


 

crl-verify 파일 생성

vi /etc/openvpn/keys/crl.pem


 

-----BEGIN X509 CRL-----

MIIB6TCB0jANBgkqhkiG9w0BAQQFADCBojELMAkGA1UEBhMCVVMxCzAJBgNVBAgT

AkNBMRUwEwYDVQQHEwxTYW5GcmFuY2lzY28xFTATBgNVBAoTDEZvcnQtRnVuc3Rv

bjERMA8GA1UECxMIY2hhbmdlbWUxETAPBgNVBAMTCGNoYW5nZW1lMREwDwYDVQQp

EwhjaGFuZ2VtZTEfMB0GCSqGSIb3DQEJARYQbWFpbEBob3N0LmRvbWFpbhcNMTQx

MjE4MTMwMzQ1WhcNMTUwMTE3MTMwMzQ1WjANBgkqhkiG9w0BAQQFAAOCAQEADMuN

+z/F7fDQU/fgvHoOh1Mn/TQPf8CN/ITfSCOrNqQORCxqSMp2rf3YhRhOnClvzYTC

wjDr04lwWWhkbKzhRBH10zTP92EC9qk9Y379f9NLilhRmv6pxbItLZQmJxR9p9kB

0SynSaiHUu6X9mLbZ3Z3OYtmPIJI51KFmtpwM1OH++8qs/j9ZA+g9tqzU30Sn6LI

wpZc5jRJ7oBDwVtOe+rfDxfMe16pJW5bchjNKPneM5jXFyMDIIWJYVdXJpfS4i6m

r+iXENUmQqTJ4zUNqtbuFLWWGp5T7JFvSuxYgWW5/ULwXzL6Z0hYhN0VbfaxFghm

7cQmfXwnc4tuTTMefg==

-----END X509 CRL-----


 

추가 하도록 한다.


 

[OpenVPN 서버 실행]


 

OpenVPN 서버를 서비스에 등록 후 실행 합니다.


 

chkconfig --add openvpn

chkconfig --level 23456 openvpn


 

/etc/init.d/openvpn start

openvpn을 시작 중: [ OK ]

netstat -ant | grep 1194


 

tcp 0 0 0.0.0.0:1194 0.0.0.0:* LISTEN


 

[iptables 방화벽 설정]

#################################

*nat

:PREROUTING ACCEPT [21:1472]

:POSTROUTING ACCEPT [0:0]

:OUTPUT ACCEPT [0:0]

-A POSTROUTING -d 192.168.95.0/24 -o eth1 -j MASQUERADE

-A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

COMMIT

#################################

추가


 

##########################################

#-A FORWARD -j REJECT --reject-with icmp-host-prohibited

#########################################

이부분은 주석처리 및 제거


 

[OpenVPN 클라이언트 설정]


 

리눅스 서버에서 생성한 사용자 키 ca.crt, dslee.crt, dslee.key 3가지 파일을 안전하게 윈도우로 다운로드 하여 윈도우용 OpenVPN 이 설치된 경로인 C:\Program Files\OpenVPN\config 에 넣어 줍니다.

클라이언트 설정파일을 C:\Program Files\OpenVPN\sample-config 에 가서 client.ovpn 파일을 config 폴더로 복사 해옵니다.


 

client.ovpn 파일을 열어서 아래 내용은 추가하도록 한다.


 

##################################

client

remote 222.x.189.97 1194

proto tcp

dev tun

ca   ca.crt

cert dslee.crt

key  dslee.key

ns-cert-type server

comp-lzo

auth-user-pass

persist-key

persist-tun

nobind

resolv-retry infinite

verb 3

mute 10  

################################


 

클라이언트에서 openvpc-gui 를 실행한후에 connect 를 실행하면, 사용자 id & pw 입력하라는 창이 나타나게 되는데, vpn 서버의 리눅스 사용자 계정 정보를 입력하면 접근할수 있다.



 

마치며..

 

OpenVPN 를 이용하여 VPN 환경을 구축하였다. 요즘은 스위치 및 공유기 등에서 VPN 이 지원되기 때문에 구지 이렇게 까지 서버단에서 구성하고 할 필요는 없을수 있지만, 리눅스 운영체제에서도 충분히 효율적으로 운영관리할수있다는 것을 경험하기 위해서 이번에 살펴보는 시간을 가졌다.  무엇보다 비용을 지불하지 않고, 오픈소스를 이용하여 구축하였고, 큰 규모의 사내인프라에서는 문제가 될수있겠지만, 중&소규모에서는 무난하게 사용할수있지 않을까 생각 된다.

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

Brute Forcing 공격에 대비하기 



0.개요
우리가 서비의 로그를 확인할 경우
pam_unix(sshd:auth): authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=xxx.xxx.xxx.xxx
pam_succeed_if(sshd:auth): error retrieving information about user developer
Invalid user developer from xxx.xxx.xxx.xxx
input_userauth_request: invalid user developer
pam_unix(sshd:auth): check pass; user unknown

이러한 형태의 로그를 자주 접했을 것이다 그리고
이러한 로그는 한번에 많게는 수십개씩 바로 붙어 확인이 될것이다
이러한 형태의 로그가 바로 패스워드 및 아이피를 무작위대입하여
공격을 하는 형태이다.

이러한 공격의 위험성은 패스워드 정책과 규칙을 재대로 관리 하는 곳이라면
패스워드가 풀릴 큰위험성은 없지만. 예를 들어 1111, password, 회사명 등
누구나 유추가능하고 쉬운 패스워드를 쓸경우 쉽게 풀려버리기도 한다.

그리고 순간적으로 너무 많은 접속을 하게되면 소켓 자원의 부족으로
정상적인 접근이 어렵게 되기도 한다

이러한 위험성에서 벗어날 수 있는 방법중 한기지를 소개하고자 한다


1. 소개 어플리케이션

소개할 어플리케이션은  fail2ban 이라는 프로그램이며
이는  모니터링할 어플리케이션의 로그파일과 iptables를 이용하여
접속 시도 확인과 차단을 한다
차단기준은 접속 실패 횟수와 횟수에 도달한 시간이 차단의 기준이다
예를 들어 5분안에 5회 이상 접속 실패일  경우 접속을 차단한다

2. 사이트 및 다운로드 
fail2ban의 공식 사이트  http://www.fail2ban.org

다운로드 페이지에서
http://www.fail2ban.org/wiki/index.php/Downloads

사용하고 있는 종류위 배포판의 설치 파일을 다운받는다
참고로 필자는 CentOS6을 사요하고 있다
Red Hat/CentOS     RPMs are available through EPEL. 항목의
링크를 따라가면 epel 저장소를 추가할 수있는 설치파일을 다운 받아 설치한다

cd /usr/local/src/
wget https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarch.rpm
rpm -ivh epel-release-latest-6.noarch.rpm

여기까지 진행하면 fail2ban의 설치 준비가 완료 된 것이다

3. 설치하기 

yum으로 설치하면된다
yum install fail2ban


4. 설정하기 

/etc/fail2ban/jail.conf를 열어 다음 부분을 확인한다
필자는 sshd  모듈만 사용하도록 하였지만 직접 열어보고
필요한 부분은 enable 시켜 사용하면 된다

[DEFAULT]

# 차단하지 않을 화이트 아이피를 등록하는 곳이다
ignoreip = 127.0.0.1/8 xxx.xxx.xxx.xxx/32

# 차단되었을 경우 차단하고 있을 시간을을 설정한다
bantime  = 600

# 로그를 탐색할 시간을 설정한다
findtime  = 600

이상 실패시 차단을 할 횟수를 설정한다
maxretry = 5


아래와 같은 많은 차단 모듈 설정이 있지만 필자는 ssh 접속에 대해서만
fail2ban을 적용할 것이다  적용하기 위해 enabled = true를 추가 한다
[sshd]
enabled = true
port    = ssh
logpath = %(sshd_log)s

정장하고 fail2ban을 재시작 한다
chkconfig fail2ban on
service fail2ban restart

확인방법

iptables -nL -v
...
Chain fail2ban-SSH (1 references)
 pkts bytes target     prot opt in     out     source               destination         
 3570  402K RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0     

다음과 같은 항목이 추가 되었는지 확인하다

그리고 로그에서
 fail2ban.server[6751]: INFO Stopping all jails
 fail2ban.jail[6751]: INFO Jail 'sshd' stopped
 fail2ban.jail[6751]: INFO Jail 'ssh-iptables' stopped
 fail2ban.server[6751]: INFO Exiting Fail2ban
 fail2ban.server[6953]: INFO Changed logging target to SYSLOG (/dev/log) for Fail2ban v0.9.3
 fail2ban.database[6953]: INFO Connected to fail2ban persistent database '/var/lib/fail2ban/fail2ban.sqlite3'
 fail2ban.jail[6953]: INFO Creating new jail 'sshd'
 fail2ban.jail[6953]: INFO Jail 'sshd' uses pyinotify
 fail2ban.filter[6953]: INFO Set jail log file encoding to UTF-8
 fail2ban.jail[6953]: INFO Initiated 'pyinotify' backend
 fail2ban.filter[6953]: INFO Added logfile = /var/log/secure
 fail2ban.filter[6953]: INFO Set maxRetry = 5
 fail2ban.filter[6953]: INFO Set jail log file encoding to UTF-8
 fail2ban.actions[6953]: INFO Set banTime = 600
 fail2ban.filter[6953]: INFO Set findtime = 600
 fail2ban.filter[6953]: INFO Set maxlines = 10
 fail2ban.server[6953]: INFO Jail sshd is not a JournalFilter instance
 fail2ban.jail[6953]: INFO Jail 'sshd' started

이러한 로그를 확인 하면 정상적으로 설치 및 설정이 된것이다


차단 및 해제를 는 로그에서 ban은 차단  unban은 해제를 나타낸다
 fail2ban.filter[8536]: INFO [sshd] Found 158.69.207.180
 fail2ban.filter[8536]: INFO [sshd] Found 158.69.207.180
 fail2ban.filter[8536]: INFO [sshd] Found 158.69.207.180
 fail2ban.filter[8536]: INFO [sshd] Found 158.69.207.180
 fail2ban.filter[8536]: INFO [sshd] Found 158.69.207.180
 fail2ban.actions[8536]: NOTICE [sshd] Ban 158.69.207.180
 fail2ban.filter[8536]: INFO [sshd] Found 108.0.11.43
 fail2ban.actions[8536]: NOTICE [sshd] Unban 158.69.207.180


5. 트러블슈팅
fail2ban.filter[32750]: WARNING Determined IP using DNS Lookup: 185a25b149c226.greendata.pl = ['185.25.149.226']
위와 같은 로그가 쉴세없이 남는다면

/etc/fail2ban/jail.conf에서  usedns=no로 설정을 하면 해결된다


아래와 같은 로그가 남고 아이피테이블에 체인생성이 되지않으면
fail2ban.action[32750]: ERROR iptables -w -N f2b-SSH#012iptables -w -A f2b-SSH -j RETURN#012iptables -w -I INPUT -p tcp --dport ssh -j f2b-SSH -- stdout: ''
fail2ban.action[32750]: ERROR iptables -w -N f2b-SSH#012iptables -w -A f2b-SSH -j RETURN#012iptables -w -I INPUT -p tcp --dport ssh -j f2b-SSH -- stderr: "iptables v1.4.7: option `-w' requires an argument\nTry `iptables -h' or 'iptables --help' for more information.\niptables v1.4.7: option `-w' requires an argument\nTry `iptables -h' or 'iptables --help' for more information.\niptables v1.4.7: option `-w' requires an argument\nTry `iptables -h' or 'iptables --help' for more information.\n" fail2ban.action[32750]: ERROR iptables -w -N f2b-SSH#012iptables -w -A f2b-SSH -j RETURN#012iptables -w -I INPUT -p tcp --dport ssh -j f2b-SSH -- returned 2
fail2ban.actions[32750]: ERROR Failed to start jail 'ssh-iptables' action 'iptables': Error starting action

 /etc/fail2ban/action.d/iptables-common.conf 파일에서 lockingopt = -w 설정을 lockingopt = 바꿔주면 된다 
 

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

[SAR(System Activity Reporter)를 이용한 시스템 모니터]

 

[시스템 환경]

Cpu : Intel(R) Core(TM)2 Quad CPU    Q6600  @ 2.40GHz

Ram : 1G

Hdd : 10G

아키텍쳐: 64bit

KVM 가상화 환경.


 

1. 서론

 

sar명령어는 지금 현재의 시스템 상황보다는 이전의 로그를 바탕으로 어떤 시기에 어떤 일들이 있었는가를 측정하여 시스템의 장기적인 부하량을 예측하고 대처할 수 있도록 해 주는 프로그램이다.   쉽게 말해서 시스템의 모니터링 툴 중에 하나 이다.

 

2. 본론

 

그러면 sar 의 주요 기능을 한번 살펴보도록 하겠다.

 

sar에서 확인할 수 있는 사항

- I/O 전송량 - 페이징- 프로세스 생성 숫자

- 블락 디바이스 활동

- 인터럽트 - 네트워크 통계

- run 큐 및 시스템 부하 평균

- 메모리와 스왑 공간 활용 통계

- 메모리 통계

- CPU 이용도

- 특정 프로세스에 대한 CPU 이용도

- inode, 파일, 기타 커널 테이블에 대한 상태

- 시스템 스위칭 활동(context switch)

- 스와핑 통계 - 특정 프로세스 통계

- 특정 프로세스의 자식 프로세스 통계

- TTY 디바이스 활동

 

생각보다 많은 것들을 확인 할수 있는 좋은 툴이다.


 

[설치]

 

그럼 sar 설치는 어떻게 하면 될까?

 

간단히 yum install sysstate 명령을 이용해서  설치하면 간편하게 설치가 완료!!

아니면 아래 사이트에서도 다운로드 받아 설치가 가능하다.

다운로드(http://sebastien.godard.pagesperso-orange.fr/download.html) 받아 설치.


 

사용옵션 정리

- sar명령어에 옵션을 주지 않고 수행했을 경우 -u옵션이 default로 적용된다.

- sar -A: 모든 관련정보를 출력한다.

- Shell> sar

%user : 사용자 레벨(application level) 에서 실행중일때의 CPU 사용률 (%)

%nice : 사용자 레벨(appliaction level) 에서 nice 가중치를 준 CPU 사용률(%)

%system : 시스템레벨(kernel) 에서 실행중일때의 CPU 사용률(%)

%iowait : system이 I/O요청을 처리하지 못한 상태에서의 CPU의 idle 상태인 시간의 비율(%)

%steal : virtual processer에 의한 작업이 진행되는 동안 virtual CPU에 의해 뜻하지 않는 대기시간이 생기는 시간의 비율(%)

%idle : CPU가 쉬고있는 시간의 %


 

[명령어 살펴보기]

 

sar 5 2

 

1.jpg

 

 

5초 간격으로 2회 수행.

 

2.jpg

 

sar -b

 

버퍼의 activity를 점검하여 I/O와 transfer의 통계를 출력한다.


 

[위에 표시된 숫자의 의미는 아래를 참조.]

tps: 물리적 디스크에서 발생한 초당 전송량이며, 여기서의 전송은 물리적 디스크에 요청한 I/O이다.

rtps: 물리적 디스크로부터 발생된 초당 읽기의 총 요청 횟수

bread/s: 드라이브 안의 블럭에서 초당 읽은 데이터의 총합.

bwrth/s: 드라이브 안의 블록에서 초당 쓰여진 데이터의 총합


 

sar -B

 

페이징 통계를 출력.

 

3.jpg

 

[위에 표시된 숫자의 의미는 아래를 참조.]

pgpgin/s: 디스크로부터 초당 paged in된 page의 총 수

papgout/s: 디스크에 초당 paged out 된 page의 총 수


 

sar -w

 

새롭게 만들어져 활동하고 있는 프로세스를 출력한다.

 

4.jpg

 

sar -d

 

* DEV: network device의 결과로부터의 통계

 IFACE: Network Interface 이름

 rxpck/s: 초당 받은 패킷수

 txpck/s: 초당 전송한 패킷수

 rxbyt/s: 초당 받은 bytes

 txbyt/s: 초당 전송한 bytes

 rxcmp/s: 압축된 패킷을 초당 받은 수

 txcmp/s: 압축된 패킷을 초당 전송한 수

 rxmcst/s: 초당 받은 다중 패킷 수

 

5.jpg

 

sar -r

 

가용메모리 점검 및 메모리 공간의 통계를 출력한다.


 

kbmemfree : 사용가능한 총 메모리의 양(kbytes)

kbmemused : 사용중인 총 메모리의 양(kbytes), 커널에서 사용중인 메모리는 제외

%memused : 사용된 메모리의 %

kbbuffers : 커널에서 buffer 메모리로 총 사용된 메모리의 양 (kbytes)

kbcached : 커널에서 cache data 로 사용된 총 메모리의 양(kbytes)

kbcommit : 현재 작업을 위해 필요한 메모리의 총량(kbytes),메모리 부족이 발생하지 않기 위한 RAM/swap 사용량의 추정치  

%commit : 현재 작업을 위해 필요한 메모리 총량의 %, kernel은 보통 메모리를 overcommits하므로 일반적으로 100%를 넘을 것이다.



 

6.jpg

 

 

sar -R

 

메모리 통계.

 

frmpg/s : 시스템에서 초당 자유로워진 memory pages 의 양 페이지의 크기는 시스템 아키텍쳐에따라 달라지며 보통 4K / 8K 이다.

bufpg/s : 시스템에서 초당 buffer 에 추가적으로 더해진 memory pages 의 양  

campg/s : 시스템에서 초당 system에 의해 cache된 memory pages 의 양


 

7.jpg

 

 

[그외의 명령어 정리]

 

sar -S → 스왑 점검 및 공간의 통계를 출력한다.

sar -v → 커널테이블 & 파일에서 inode의 상태를 출력한다.

sar -w → 작업 생성과 시스템 switching활동 현황 출력

sar -W → swapping의 통계 출력


 

[sar 설정하기]

 

8.jpg

 

 

- sa1은 매 10분마다 시스템 모니터링한 결과를 /var/log/sa/saXX파일에 바이너리 형태로 기록한다. XX는 기록하는 날짜이다.

- sa2 -A는 23시 53분에 바이너리 파일을 읽어서 사람들이 볼 수 있도록 보고서를 만든다. /var/log/sa/sarXX 형태로 기록된다. sa1에서 5 3이라고 해놓으면 매 10분마다 실행을 하면서 5초동안 3번을 기록한다는 뜻이다.

- sar를 이용하여 각 결과 값을 위에 설명한 옵션을 이용하여 확인하면 된다.

 

 

3. 마무리

 

지금까지 sar 에 대해서 살펴보았다. 시스템 관리를 하다보면, 부하가 언제부터 진행이 되었고, 현재 시스템에서 어떠한 곳에 부하가 많이 발생하는지 체크할수 있는 프로그램으로 좋을것 같다.

사용법도 어렵지 않고, 가볍게 사용할수 있으며,  각 프로세스의 사용률을 % 단위로 알수 있어서 좋은 점수를 주고 싶다.

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,


스크린샷 프로그램 shutter

민트나 우분투를 사용중 스크린샷 을 사용하려면 alt+printscreen (현재 화면스크린샷)이나

prisntcreen(전체 화면 스크린샷)키를 사용하였다.

이렇게 사용하면 자기가 원하는 부분만 스크린샷을 할수없어 따로편집해야하는 번거러움이 있어서

찾아본결과 shutter 프로그램이 나왓다.

  

 

 1.jpg

 

 

 

소프트웨어 관리자에서 shutter 검색한후 설치~

  2.jpg

 

 

프로그램 실행화면

 

 

선택 영역을 클릭하며 선택한 영역만 스크린샷을 찍을수 있다

 

 

 3.jpg

 

 

 

 

창모드를 클릭하고 웹브라우저를 클릭하면 현제 웹브라우저 화면이 캡쳐된다.

 

 

 4.jpg

 

 

 

데스크탑을 클릭화면 전체화면을 스크린샷을 찍는다.

 

 

 


 

 

저장할때 형식을 지정할수도 있다.

 5.jpg

 

 

스크린샷이 필요한 문서작업할대 일일히 스크린샷을 찍고 편집해서 잘라내고 저장하고 번거로운작업을 이프로그램하나로 쉽게 해결했다.

사용하기편리해서 문서작업을 할때 애용할듯하다.


블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,


 NTFS 파일시스템

 

 

 

 

NTFS란?

 

 NTFS는 윈도 NT 계열 운영체제의 파일 시스템으로 윈도 2000, 윈도 XP, 윈도 서버 2003, 윈도 서버 2008, 윈도 비스타, 윈도 7, 윈도 서버 2008 R2 등에도 포함되어 있다. NTFS의 NT는 윈도 NT와 비슷하게 새로운 기술이라는 뜻의 New Technology의 준말이다. MS-DOS와 이전 버전의 윈도에서 쓰였던 마이크로소프트의 이전 FAT 파일 시스템을 대체하였다. NTFS는 FAT와 HPFS(고성능 파일 시스템)을 거쳐 몇 가지 개선이 있다. 이를테면, 메타데이터의 지원, 고급 데이터 구조의 사용으로 인한 성능 개선, 신뢰성, 추가 확장 기능을 더한 디스크 공간 활용을 들 수 있다. (출처: 위키백과)

 

 

NTFS 살펴보기

 

 

 

 

 

(63번 BR부분)

 

 

 

 

 

 

부가적인 내용

 

포렌식 현장도착후 할일:  메모리 덤프, 사진촬영

 

- Login Bomb 때문에 정상종료를 하면 안된다.

: 전원플러그 강제로 뽑아야 한다. (아직까지는..)

 

-  포렌식 툴은 2개이상 띄워서 비교해야 한다.

 

- 하디디스크는 '쓰기방지툴'을 꼭 사용해야 한다. (EX: 플로피디스크 열기/닫기 )

: '권장' 이 아니라 '필수' 이다.

 

 

- slack space(슬랙스페이스) : 파일의 크기가 데이터 단위 크기의 배수가 되지 않을 때,

저장 매체에서 파일이 저장되고 남은 잉여 공간을 말합니다.

 

램슬랙: 파일 끝을 표시하기 위해 섹터 마지막까지 0으로 채우는 공간

 

일반슬랙: 저장 되었던 파일의 내용 정보와 시간정보를 제공하는 공간

slack space는 File Table 에서 인식을 못하므로 일반적인 Browsing 으로는 해당 

파일을 열람 할수 없음 디스크에서 Slack Space 크기와 위치를 검사하고 디스크의

물리적 주소를 파악, 해당 클러스터의 정보를 조사해야함

 

Slack space 종류: 램슬랙, 파일슬랙, 파일시스템 슬랙

슬랙 스페이스로 인해 데이터나 파일 시스템의 끝부분을 알 수 있기 때문에 삭제된

파일을 복구 할때 유용하게 사용됨 

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

## MFT(Master File Table)

# MFT란
- MFT Entry들의 모음
- MFT의 시작위치는 BR에서 확인한다.
- MFT의 크기는 볼륨에 있는 파일과 디렉토리의 개수에 비례한다.
  예)볼륨에 파일과 디렉토리의 개수가 60000개일 때 MFT Entry개수는 대략 60000개 이상 차지한다.
  60000 * 1KB = 60000 KB = 약 60 MB

# MFT Entry
- 파일 또는 디렉토리에 대한 정보를 담고 있다.파일명,크기,시간,파일내용,위치,권한,할당량 ...
- 기본적으로 파일이나 디렉토리는 하나의 Entry를 사용하며 기록할 내용이 많은 경우 Entry를  
   여러 개 사용할 수도 있다.
- 기본 크기는 1KB이다.(BR 또는 MFT Entry Header에서 확인가능)
- MFT Entry는 섹터 2개를 차지한다.
- MFT Entry Header와 여러 개의 속성들(Attributes)로 구성된다.
- MFT Entry 마지막 속성의 뒷부분에 End Marker(0xFFFFFFFF)로 끝난다.


## MFT Entry Header
- 크기가 42 bytes

# Signature
- MFT Entry임을 나타내는 표시이다.
- 위치: 0~3번 바이트
- 크기: 4 Bytes
- 정상 Entry에는 FILE 이라는 문자열이 적혀있다.
- 문제가 발견된 Entry에는 BAAD라고 적혀있다.


# Offset of Fixup Array
- MFT Entry 시작 위치로부터  Fixup 배열까지의 거리
- 위치: 4~5번 바이트
- 크기: 2 Bytes
- 보통 0x48이라고 적혀있다.

(참고) Fixup Array
- 데이터가 저장된 섹터의 내용의 손상여부를 확인하기 위한 기법
- 섹터의 마지막 2바이트를 특정 Signature로 바꿔놓고 원래의 2바이트 값은 Fixup배열에
   기록해 둔다.
- Signature값은 Fixup 배열의 맨앞에 기록한다.
- 섹터를 읽을 때 Signature값이 틀리면 손상된 섹터로 간주한다.
- 위치: 보통 MFT Entry Header가 끝나고 바로 이어진다. Offset to Fixup Array항목에서 확인
- 크기: Count of Fixup Values 항목에서 확인
- 이 기법이 사용되는 항목은 MFT Entry,Index Record, RCRD Record, RSTR Record 등이다.
- 파일 내용이 들어가는 클러스터의 섹터에는 적용하지 않는다.

# Count of Fixup Values
- Fixup 배열(Array)의 크기(항목의 개수)
- MFT Entry는 기본적으로 3이다.
   (MFT Entry가 1KB이므로 섹터 2개를 사용 + Signature가 1개 항목 사용)
- 위치: 6~7번 바이트
- 크기: 2 바이트

# $LogFile Sequence Number(LSN)
- 설명은 뒤에서 다시 다룬다.
- 내용은 다양한 값을 가질 수 있다.
- 위치: 8~15번 바이트
- 크기: 8 바이트

# Sequence Value
- MFT Entry가 할당 또는 해제될 때 값이 증가한다.
- MFT Entry를 재할당하면 이값이 바뀌므로 내용이 바뀌었다는 것을 추측할 수 있다.
- 위치: 16~17번 바이트
- 크기: 2 바이트(16 bit)

(참고) File Reference Address (64 bit)
Seqence Value(16 bit) + MFT Entry Address(48 bit)

# Hard Link Count
- 하드 링크의 개수
- 보통 1 이다. Base MFT Entry에서만 사용된다.
- Hard Link 개념을 윈도우에서도 사용할 수 있다. 하지만 실제 도구가 없어서 사용할
  수 없다. 그러나 MS에서 제공되는 도구가 있다.
- 위치: 18~19
- 크기: 2 Bytes

# Offset to First Attribute
- MFT Entry의 첫 번째 속성(Attribute)의 위치(Offset)
- 보통 56(D)=0x38 이지만 얼마든지 다른 값이 올 수 있다. 확인 필수
- 위치: 20~21
- 크기: 2 Bytes

# Flags
- MFT Entry의 상태정보
- 내용이 0x1 이면 MFT Entry가 사용 중임을 나타낸다.
- 내용이 0x2 이면 MFT Entry가 디렉토리에 사용되고 있음을 나타낸다.
- 위치: 22~23번 바이트
- 크기: 2 바이트

# Used Size of MFT Entry
- MFT Entry 전체크기(보통 1KB)중에서 사용중인 공간의 크기를 나타낸다. 정확하지는
   않고 8의 배수로 기록된다.
- 내용: 다양한 값을 가질 수 있음.
- 위치: 24~27번 바이트
- 크기: 4 바이트

# Allocated Size of MFT Entry
- MFT Entry 전체크기를 나타낸다.
- 보통 1024 가 적혀있다. (MFT Entry는 1KB로 고정되어 있음)
- 위치: 28~31번 바이트
- 크기: 4 바이트

# File Reference to Base MFT Entry
- Non-Base MFT Entry의 Base MFT Entry의 위치를 나타낸다.
- File Reference Address로 기록되어 있다.
- Non-Base MFT Entry 에서만 기록된다.
- Base MFT Entry에서는 언제나 0 이다.
- 위치: 32~39번 바이트
- 크기: 8 바이트

(참고) Base MFT Entry & Non base MFT Entry

# Next Attribute ID
- 현재 MFT Entry에 새로운 속성이 기록될 때 사용할 속성ID(고유한 값, 속성 TYPE ID와 다름)
- 위치: 40~41번 바이트
- 크기: 2 바이트

## Attribute(속성)

# 구성
- 속성 헤더(Header) + 속성 내용(Content)

# 속성 헤더 구조 (Resident)
- Resident 형태의 속성은 내용이 MFT Entry내에 있다
- $FILE_NAME (파일 이름 속성)

# 속성 헤더 구조 (Non-Resident)
- Non-resident 형태의 속성은 내용이 다른 클러스터에 들어있고 속성에는 위치만 기록됨.

# 속성 종류(Attribure Type)
$STANDARD_INFORMATION                
$ATTRIBUTE_LIST
$FILE_NAME
$VOLUME_VERSION
$OBJECT_ID
$SECURITY_DESCRIPTOR
$VOLUME_NAME
$VOLUME_INFORMATION
$DATA
$INDEX_ROOT
$INDEX_ALLOCATION
$BITMAP
$SYMBOLIC_LINK
$REPARSE_POINT
$EA_INFORMATION
$EA
$LOGGED_UTILITY_STREAM


# $STANDARD_INFORMATION
- 파일과 디렉토리에 대한 시간정보/소유자/보안설정/파일종류/할당량 등의 정보를 담는다.
- 모든 파일과 디렉토리에 대한 Base MFT Entry에는 반드시 존재하는 속성
- 속성 타입 번호 : 0x16                 
- 저장 형태: 반드시 Resident 형태만 가능함
- 크기: 보통 72(Byte), 다를 수 있음, 확인 필요

- 속성 구조 요약

항목 이름        설명        위치        크기          
Created Time        처음으로 파일이 생성된 시간 정보        0~7        8          
Modified Time        마지막으로 파일 내용이 수정된 시간        8~15        8          
MFT Modified Time        MFT Entry 내용이 마지막으로 수정된 시간        16~23        8          
Accessed Time        마지막으로 파일 내용을 접근한 시간        24~31        8          
Flags        파일의 특성        32~35        4          
Maximum number of version                36~39        4          
Version Number                40~43        4          
Class ID                44~47        4          
Owner ID        파일의 소유자의 ID        48~51        4          
Security ID                52~55        4          
Quota Charged                56~63        8          
Update Sequence Number                64~71        8        


# $FILE_NAME
- 파일이나 디렉토리의 이름과 이름에 관련된 추가 정보가 저장되는 속성이다.
- 이 속성은 반드시 Base MFT Entry에 있어야 읽을 수 있다.
- 속성 타입 번호 : 0x48
- 저장 형태: 반드시 Resident 형태만 가능함
- $FILE_NAME의 Attribute Content 구조

위치        크기        설명          
0~7        8        부모 디렉토리의 File Reference Address          
8~15        8        Created Time          
16~23        8        Modified Time          
24~31        8        MFT Modified Time          
32~39        8        Accessed Time          
40~47        8        파일의 할당 크기          
48~55        8        파일의 실제 크기          
56~59        4        Flag          
60~63        4        Reparse Value          
64~64        1        이름의 길이          
65~65        1        이름의 형식          
66~??        동적        이름        
- 이 속성은 Index Entry에도 존재하며 Index Entry에 있는 $FILE_NAME속성을 우선적으로 갱신한다.
- Flag 항목은 $STANDARD_INFORMATION 속성의 flag와 동일한 구조이다.

# $DATA
- 실제 파일이나 디렉토리의 내용이 저장되는 공간이다.
- 속성 타입 번호 : 0x128
- 저장 형태: 700 Byte를 기점으로 작으면 Resident 형태로 저장되고 크면 Non-Resident 형태로 저장된다.

# $DATA속성의 ADS 항목
- Alternate Data Stream
- 파일이나 디렉토리에 추가되는 $DATA 속성

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

리버스 엔지니어링이란?

리버스(reverse)라는 말은 반대, 역(逆)의 뜻을 가지고 있는데, 리버스 엔지니어링을 역공학이라고 쓰기도 합니다.

리버스 엔지니어링은 목표가 되는 프로그램이나 프로토콜을 분석하여 똑같은 동작을 만들어 내는 것을 말합니다.

 

올리디버거.png

 

리버스 엔지니어링의 종류

통상적으로 컴파일된 바이너리(EXE, DLL, SYS 등)를 디스어셈블러라는 도구를 이용하여 어셈블리 코드를 출력한 후

그것을 C언어 소스형태로 다시 옮겨 적고 적당한 수정을 통해 리버스하고 있는 파일과 동일한 동작을 하는 프로그램을 만드는 것이 있습니다.

 

모든 어셈블리 코드를 소스 형태로 옮기지 않고 그냥 동작 방식만을 알아낸다거나 일정 부분만 수정하는 것들도 리버스 엔지니어링이라고 할 수 있습니다. 예를 들면 바이러스를 분석하는 일은 모든 코드를 알아낼 필요가 없기 때문에 동작 방식만 알아내면 됩니다.

그리고 크랙처럼 일정 부분만 수정하여 사용제한을 푸는 것 등도 이에 해당됩니다.

 

Rce1.jpg

 

실행파일을 디스어셈블 하지 않고도 그 실행파일이 만들어내는 데이터 파일이나 패킷등을 분석하여 똑같이 재구현하는 것도 리버스 엔지니어링입니다.

예를 들면 오래전 PC 게임에서 많이 하던일인데, HEX 에디터 등으로 세이브 파일을 분석하여 에디트를 만들거나 게임자체를 조작하는 것이 있고,

당나귀와 호환되는 이뮬 같은 프로그램은 당나귀 프로토콜의 패킷을 분석하여 동일한 동작을 하도록 만들어낸 것입니다.

리버스 엔지니어링에서 가장 많이 사용되는 방식은 첫번째로 이야기 했던 바이너리를 디스어셈블 하여 코드를 얻어내는 것입니다.

이것을 하기 위해서는 먼저 인텔 어셈블리를 배워야 하고, 물론 C언어도 알아야 됩니다.

 

리버싱.jpg

 

 

그런데 여기서 컴파일된 바이너리가 VC++나 gcc등으로 컴파일한 것이 대부분이지만 비주얼 베이직으로

컴파일 한 것도 있고 델파이(파스칼)로 컴파일 한 것도 있을 것입니다. 이 바이너리들은 모두 CPU에서 직접 실행되는 것들이기 때문에

디스어셈블 해보면 모두 똑같은 방식으로 되어 있습니다. 그래서 VC++, VB, 델파이(파스칼)등으로 컴파일 된 것도

디스어셈블 한 뒤 C 소스 코드로 옮길 수 있습니다. 물론 리버스 하는 사람이 VB나 파스칼로 옮겨 적을 수도 있을 것입니다.

하지만 C언어로 하는 것이 가장 간편합니다.

 

 

대부분 리버스해서 얻어내는 것들은 프로그램의 로직(알고리즘)이기 때문에 어느 언어로 표현하든 결과는 똑같기 때문입니다.

자바나 닷넷으로 컴파일 된 바이너리는 CPU에서 직접 실행되지 않고 자바 가상머신이나 닷넷 프레임워크를 통해서 실행됩니다. 그래서 자바로 컴파일된 바이너리를 열어보면 자바 바이트코드 문법으로 되어 있고 닷넷으로 컴파일 된 것은 MSIL이라는 문법으로 되어 있습니다. 이런 것을은 인텔 어셈블리와 문법이나 명령어가 다르므로 따로 자바 바이트코드나 MSIL을 공부해서 리버스 하면 되겠습니다.

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

[리버스 엔지니어링 스터디]

[이 글은 저자인 이승원씨가 작성한 '리버싱 핵심원리: 악성 코드 분석가의 리버싱 이야기'를 참고하여 쓰여졌습니다.]

 

코드 케이브를 삽입 후 프로그램을 패치한다!

인라인 코드 패치(Inline Code Patch)

 


 

오늘은 인라인 코드 패치(Inline Code Patch)에 대해서 공부해보도록 하겠습니다. 인라인 코드 패치란 원하는 위치에 있는 코드를 직접 패치하기 어려울 때 코드 케이브(Code Cave)라고 하는 패치 코드를 삽입한 후 실행하여 프로그램을 패치시키는 기법이라 합니다. 주로 패킹 혹은 암호화된 프로그램은 EP(Entry Point)에서 OEP(Original Entry Point) 코드를 복호화 시킨 뒤, 복호화 된 OEP 부분으로 이동합니다. 만약, 우리가 패치하려는 코드가 암호화된 OEP 영역에 존재한다면 직접적인 패치가 곤란할 것입니다. 복호화 과정을 거치면서 직접 패치한 코드를 전혀 다른 코드로 복호화 하기 때문입니다.

240DDE46536F7AB51B2461

여기서 앞서 배우게될 코드 케이브란 것을 설치하게 된다면 어떨까요? EP 코드에 있는 복호화 과정을 거친 뒤 JMP 명령을 수정하여 코드 케이브로 이동하게 만듭니다. 그 후에, OEP는 이미 복호화 과정을 거친 상태이므로 코드 케이브 내의 패치 코드를 통해 간접적인 수정이 가능합니다. 코드 케이브의 패치 코드가 끝나면 복호화된 OEP 코드 부분으로 다시 이동하게 됩니다. 위 그림처럼, 코드 케이브는 사용되지 않는 메모리 영역을 통해 임의의 코드를 삽입할 수 있는 곳을 말합니다. 간단하게, 코드 케이브란 함수를 호출하여 간접적인 수정을 하는 것이라고 이해를 하셔도 됩니다. 오히려, 그러는 편이 이해가 더 쉬울지도 모릅니다. 위 그림과 같이 코드 케이브를 설치하면, 프로그램이 실행될 때마다 프로세스 메모리의 코드를 패치하여 그때그때 처리하기 때문에 이 기법을 인라인 코드 패치라고 부릅니다. 이 글에서는 위에서 설명한 패치 기법을 통해 이중 암호화된 프로그램을 패치할 것입니다. 우선 아래의 파일을 다운로드 받아주세요.

 

 unpackme#1.aC.exe

 

위 파일을 다운로드 받았으면 먼저 실행시켜 보도록 하겠습니다.

 

27100441536F806004768F

 

프로그램을 실행시키니, 잔소리를 패치하라고 알림창이 뜨는 것을 보실 수 있습니다. 그리고 확인을 누르면 아래와 같은 창이 등장합니다.

 

2264B53E536F84C525DCE2

 

위 창을 살펴보니, 텍스트 박스 내에 자신을 언팩(unpack) 해달라는 문자열을 보실 수 있습니다. 우선은 올리 디버거를 통해 이 파일을 살펴보도록 하겠습니다. 먼저 EP 코드 부분을 살펴보도록 합시다.

 

1
2
3
00401000 unpackme.<mo>/$  PUSHAD
00401001                  CALL 004010E9
00401006                  RETN

 

상당히 간단하죠? PUSHAD를 통해 모든 레지스터의 값을 스택에 올립니다. 그리고 4010E9 함수를 호출하고 있습니다. 우리가 원하는건 알림창 내에 있는 잔소리(NAG)를 패치하는 것이기 때문에 원래대로라면 문자열의 위치를 통해 손쉽게 패치할 수 있을것 같으나 위에서 말씀드린대로 모든 문자열이 암호화 되어 찾기도, 변경하기도 힘들기 때문에 그냥 4010E9 함수 내부로 따라가도록 하겠습니다. 

 

1
2
3
004010E9              MOV EAX,004010F5
004010EE              PUSH EAX              ;  kernel32.BaseThreadInitThunk
004010EF              CALL 0040109B

 

위에선 4010F5를 EAX에 저장하고, EAX를 스택에 올린 뒤에 함수 40109B를 호출하고 있습니다. 40109B로 F7(Step Into)를 통해 계속 진행해보도록 하겠습니다.

 

1
2
3
4
5
6
7
8
0040109B              PUSH EAX                         ;  unpackme.004010F5
0040109C              MOV EBX,EAX                      ;  unpackme.004010F5
0040109E              MOV ECX,154
004010A3              XOR BYTE PTR DS:[EBX],44
004010A6              SUB ECX,1
004010A9              INC EBX                          ;  unpackme.004010FA
004010AA              CMP ECX,0
004010AD              JNZ SHORT 004010A3

 

여기서 유심히 보아야 할 부분이 4010A3~4010AD 부분인데, 40109C에서 EBX에다 4010F5를 저장하고, ECX에다 154를 넣는 것을 보실 수 있습니다. 4010A3~4010AD 복호화 루프에서 ECX가 0이 될때까지 1씩 감소시키며, EBX의 값을 1씩 증가시키는 것을 보실 수 있습니다. 여기서 EBX가 1씩 증가되며, 1바이트 정도 읽어온 뒤에 이 값과 44에다 XOR 연산을 취합니다. 즉, XOR 명령으로 복호화가 진행된다는 것을 보실 수 있습니다. ECX가 0이 되는 순간, 루프를 벗어나 4010AF로 계속 진행합니다.

 

1
2
004010AF              PUSH EAX                      ;  unpackme.004010F5
004010B0              CALL 004010BD

 

보시는 바와 같이 EAX의 값인 4010F5를 스택에 올리고, 4010BD 함수를 호출합니다. 4010BD 쪽을 보도록 합시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
004010BD              PUSH EAX                         ; unpackme.004010F5
004010BE              MOV EBX,00401007
004010C3              MOV ECX,7F
004010C8              XOR BYTE PTR DS:[EBX],7
004010CB              SUB ECX,1
004010CE              INC EBX                          ; unpackme.00401249
004010CF              CMP ECX,0
004010D2              JNZ SHORT 004010C8
004010D4              MOV EBX,EAX                      ; unpackme.004010F5
004010D6              MOV ECX,154
004010DB              XOR BYTE PTR DS:[EBX],11
004010DE              SUB ECX,1
004010E1              INC EBX                          ; unpackme.00401249
004010E2              CMP ECX,0
004010E5              JNZ SHORT 004010DB
004010E7              POP EAX                          ; unpackme.004010F5
004010E8              RETN

 

위 어셈블리 코드에서 두개의 복호화 루프를 보실 수 있는데, 하나는 4010C8~4010D2, 또 하나는 4010DB~4010E5 부분을 주목해주세요. 아까 보았던 복호화 루프와 같은 구조입니다. 4010C8 부터 시작하는 복호화 루프는 401007 부터 401085 영역까지 복호화를 진행하고, 4010DB 부터 시작하는 복호화 루프는 4010F5~401248 까지를 복호화 시키게 됩니다. 이중으로 암호화 되어있다는 사실을 알 수 있으며, 4010BD 함수의 호출이 완료되면 4010B5로 돌아가게 됩니다.

 

1
2
004010B5              PUSH EAX                         ;  unpackme.004010F5
004010B6              CALL 00401039

 

다시 스택에 EAX의 값인 4010F5를 올려두는 부분이구요, 함수 401039를 호출합니다. 다시 내부로 진입합시다.

401039 내부를 돌아다니시다 보면 크게 두 파트로 어셈블리 코드를 나눌 수 있습니다. 먼저 401039~40104F 부분을 보도록 하겠습니다.

 

1
2
3
4
5
6
7
8
9
00401039              PUSH EAX                         ;  unpackme.004010F5
0040103A              MOV EBX,EAX                      ;  unpackme.004010F5
0040103C              MOV ECX,154
00401041              MOV EDX,0
00401046              ADD EDX,DWORD PTR DS:[EBX]
00401048              SUB ECX,1
0040104B              INC EBX                          ;  unpackme.00401249
0040104C              CMP ECX,0
0040104F              JNZ SHORT 00401046

 

먼저 이 부분은 EBX가 4010F5로, EDX를 먼저 0으로 값을 덮어씌우고 4010F5~401248에서 순차적으로 4바이트 단위로 값을 읽어온 뒤 ADD(덧셈) 연산을 통해 누적시킵니다. 이어서 401062~401083 부분을 봐봅시다.

 

1
2
3
4
5
6
7
8
9
10
00401062         CMP EDX,31EB8DB0
00401068         JE SHORT 00401083
0040106A         PUSH 30
0040106C         PUSH 00401032  ;  ASCII "Error:"
00401071         PUSH 00401009  ;  ASCII "CrC of this file has been modified !!!"
00401076         PUSH 0
00401078         CALL 00401262
0040107D         PUSH EAX       ;  unpackme.004010F5
0040107E         CALL 00401274
00401083         JMP 0040121E

 

EDX의 값과 31EB8DB0을 서로 비교하고 있습니다. 만약에 값이 서로 같으면 JE로 인해 401083으로 점프하고, 그렇지 않을 경우 40106A로 넘어간 뒤에 에러 알림창을 띄우게 됩니다. 여기서 EDX에 저장된 값은 CRC 체크섬 값이며 이는 오류검증을 위한 부분이라고 생각할 수 있습니다. 만약 코드가 변조되었을 경우 EDX의 값은 31EB8DB0 값과 달라지기 때문에 파일이 변조되었다는 에러 알림창을 보실 수 있습니다. 다시 넘어와서, 401083을 보시면 40121E로 점프하는 것을 보실 수 있습니다. 한번 40121E으로 넘어가보도록 하겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
0040121E      PUSH 0
0040121E      PUSH 0                               ; /pModule = NULL
00401220      CALL <JMP.&kernel32.GetModuleHandleA> ; \GetModuleHandleA
00401225      MOV DWORD PTR DS:[403018],EAX        ;  unpackme.00401280
0040122A      PUSH 0                               ; /lParam = NULL
0040122C      PUSH 004010F5                        ; |DlgProc = unpackme.004010F5
00401231      PUSH 0                               ; |hOwner = NULL
00401233      PUSH 00403024                        ; |pTemplate = "TESTWIN"
00401238      PUSH DWORD PTR DS:[403018]           ; |hInst = NULL
0040123E      CALL <JMP.&user32.DialogBoxParamA>   ; \DialogBoxParamA
00401243      PUSH EAX                             ; /ExitCode = 401280
00401244      CALL <JMP.&kernel32.ExitProcess>     ; \ExitProcess

 

드디어 OEP 코드가 등장했습니다. 40121E 부분부터는 OEP 코드이며, 위 어셈블리 코드만 보았을 경우에는 GetModuleHandleA, DialogBoxParamA, ExitProcess 이렇게 3개의 API 함수가 호출되었음을 알 수 있습니다. 유심히 보셔야 할 부분이 DialogBoxParamA 함수를 호출하는 부분인데, 이 부분은 다이얼로그를 실행시키는 부분입니다. MSDN을 확인해보면 hInstance, lpTemplateName, hWndParent, lpDialogFunc, dwInitParam 이렇게 매개변수 5개를 받는다는 사실을 알 수 있고, 이 중 lpDialogFunc는 다이얼로그 박스 프로시저를 가리키는 포인터, 즉 주소를 의미합니다. 함수 호출시 매개변수는 역순으로 스택에 올라가게 되니, 40122C 주소의 4010F5 값이 lpDialogFunc의 값임을 알 수 있습니다. 바로 4010F5를 확인해보도록 하겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
004010F5      PUSH EBP
004010F6      MOV EBP,ESP
004010F8      ADD ESP,-40
004010FB      CMP DWORD PTR SS:[EBP+C],110
00401102      JNZ 004011D0
00401108      JMP SHORT 00401121
0040110A      ASCII "You must unpack "
0040111A      ASCII "me !!!",0
00401121      JMP SHORT 0040113F
00401123      ASCII "You must patch t"
00401133      ASCII "his NAG !!!",0
0040113F      JMP SHORT 00401165
00401141      ASCII "<<< Ap0x / Patch"
00401151      ASCII " & Unpack Me #1 "
00401161      ASCII ">>>",0

 

여기서 주목해야 할 부분은 40110A~401161이 되겠습니다. 우리가 패치하여야 될 문자열이 저기에 보이네요. 더 아래 부분을 보도록 합시다.

 

1
2
3
4
5
6
7
8
9
004011A5      PUSH 0040110A                     ; /Text = "You must unpack me !!!"
004011AA      PUSH 64                           ; |ControlID = 64 (100.)
004011AC      PUSH DWORD PTR SS:[EBP+8]         ; |hWnd = 7FFD8000
004011AF      CALL <JMP.&user32.SetDlgItemTextA> ; \SetDlgItemTextA
004011B4      PUSH 40                           ; /Style = MB_OK|MB_ICONASTERISK|MB_APPLMODAL
004011B6      PUSH 00401141                     ; |Title = "<<< Ap0x / Patch & Unpack Me #1 >>>"
004011BB      PUSH 00401123                     ; |Text = "You must patch this NAG !!!"
004011C0      PUSH DWORD PTR SS:[EBP+8]         ; |hOwner = 7FFD8000
004011C3      CALL <JMP.&user32.MessageBoxA>    ; \MessageBoxA

 

위 부분에서는 아까 본 문자열을 매개변수로 전달하여 "You must patch this NAG !!!"라는 메시지 박스를 띄우고, 텍스트 박스 내에 "You must unpack me !!!"라고 설정합니다. 이제 패치해야 할 부분을 알아냈고, 흐름도 어느정도 보았으니 이제는 코드 케이브를 내부에 설치하여 문자열을 간접적으로 패치해보도록 하겠습니다. 이 코드 케이브는 파일의 빈 영역이나 마지막 섹션을 확장하여 설치하거나, 새로운 섹션을 추가하여 설치를 할 수 있는데 패치 코드가 얼마 안되므로 파일의 빈 영역을 통해서 코드 케이브를 설치하도록 하겠습니다. PEView 또는 Stud PE 같은 분석 도구를 이용하여 첫번째 섹션 헤더인 .text 섹션 헤더를 살펴보도록 하겠습니다.

 

241D734D53721ECD32DDD0

 

위 사진에서 PointerToRawData를 보니 .text 섹션은 파일에서 400에서 시작하고, SizeOfRawData를 통해 .text 섹션이 파일에서 차지하는 크기가 400정도 된다는 것을 알 수 있습니다. 또한, VirtualSize를 통해서 280 크기만 메모리에 로딩한다는 사실을 알 수 있습니다. (실제로는 SectionAlignment의 배수 단위로 확장되기 때문에 1000이 됩니다.) 한번 헥스 에디터를 통해서 그 공간을 직접 확인해보도록 합시다.

 

273EA73F537226DC131B55

 

위와 보시는 것과 같이 680~800은 사용되지 않는 빈 공간(NULL Padding)이며, 이곳에 코드 케이브를 설치하도록 하겠습니다. 우선은 올리 디버거로 다시 돌아가서, 이 빈 공간에 해당하는 메모리 영역으로 이동해보도록 하겠습니다. 가상 주소(VA)는 ImageBase에서 RVA를 더한 값이므로, ImageBase(400000) + RVA(1000) = VA(401000)이란 결과값을 얻을 수 있으며 VirtualSize까지 고려하게 된다면 401280부터 401400이 빈 공간에 해당하는 영역이라고 볼 수 있겠습니다. 한번 401280으로 가보도록 하겠습니다.

 

1
2
00401280      DB 00
00401281      DB 00

 

확인해보니 401280 부터 빈 영역이 등장하기 시작합니다. 여기서 코드 케이브를 설치하도록 하겠습니다. 401280에는 아래와 같이 패치 코드를 삽입하였습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
00401280      MOV ECX,11
00401285      MOV ESI,004012A8                    ;  ASCII "blog.eairship.kr"
0040128A      MOV EDI,00401123                    ;  ASCII "You must patch this NAG !!!"
0040128F      REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
00401291      MOV ECX,7
00401296      MOV ESI,004012B9                    ;  ASCII "su6net"
0040129B      MOV EDI,0040110A                    ;  ASCII "You must unpack me !!!"
004012A0      REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
004012A2      JMP 0040121E
004012A7      DB 00
004012A8      ASCII "blog.eairship.kr"
004012B8      ASCII 0
004012B9      ASCII "su6net",0

 

위 코드에서 ECX 레지스터에는 NULL 까지 고려한 문자열의 길이가 들어가게 되며, ESI에는 패치에 쓰일 문자열 주소가 들어가고, EDI에는 패치하려는 문자열 주소가 들어갑니다. 그리고 REP 명령을 통해 ECX 레지스터의 값만큼 'MOVS BYTE PTR ES:[EDI], BYTE PTR DS:[ESI]' 명령을 반복합니다. 이때, EDI 값과 ESI 값은 1씩 증가하면서 계속 변경됩니다. 마지막으로 4012A2 부분은 OEP로 점프하는 부분이 있습니다. 우선은 변경된 내용을 저장합시다. 자, 이제 코드 케이브를 설치했으니 OEP로 들어가기 전, 코드 케이브로 점프하여 원하는 문자열로 패치한 뒤 OEP로 넘어가도록 하겠습니다. 직접 디버거를 통해 401083에 있는 어셈블리 코드를 수정해도 되지만, 이 부분은 원래부터 암호화된 영역이였기 때문에 헥스 에디터를 통해서 수정하도록 하겠습니다. 옵셋 483(.text 섹션의 PointerToRawData가 400이므로 파일에서는 400에서 시작하며, 401083에서 ImageBase(400000), .text 섹션의 RVA(1000)을 빼면 83이 나옵니다. 400과 83을 더하면 483) 으로 이동하여 헥스 코드를 살펴보도록 합시다.

 

227761345373850E2C420D

 

401283 부분은 원래 XOR 명령을 통해 암호화 되어있던 영역이므로, 헥스 코드를 수정할때는 XOR 7로 암호화하여 써넣어야 합니다. 

1
00401083      /E9 F8010000   JMP 00401280

위 401083에 해당하는 JMP 명령문의 Instruction은 E9 F8010000 입니다. 그리고 이걸 XOR 7로 암호화 하게 된다면, E9 F8 01 -> EE FF 06이 될 것입니다. 그럼 EE 91 06을 EE FF 06으로 수정하여 저장하도록 합시다. 그런 뒤에 패치된 파일을 실행시키시면 우리가 원하던 결과를 얻을 수 있습니다.

243441445373879E2B3D1D

 

233496445373879E0975BF

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

[리버스 엔지니어링 스터디]

 

함수 호출시 할당되는 메모리 블록

스택 프레임(Stack Frame)

 

 


 

스택에 저장되는 함수의 호출 정보를 스택 프레임(Stack Frame)라고 하며, 이러한 스택 프레임에는 함수로 전달되는 인수와, 함수 실행 모두 마치면 돌아올 복귀 주소와 지역 변수 등의 정보가 들어갑니다. 빠르고 손쉽게 지역 변수 혹은 인수 등에 접근하기 위해 EBP 레지스터를 통하여 스택 프레임을 참조할 수 있습니다. 더욱 파고들기 위해서, 함수 호출 시 스택 프레임이 어떠한 형태로 생성이 되고 소멸은 또 어떻게 되는지 한번 확인해보도록 하겠습니다. 먼저, C언어로 작성된 아래 예제의 코드를 빌드한 후 올리 디버거를 통하여 살펴보도록 합시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
int sum(int a, int b)
{
    int x = a, y = b;
 
    return x + y;
}
 
int main(int argc, char* argv[])
{
    int a = 9, b = 4;
 
    printf("%d\n", sum(a, b));
 
    return 0;
}

 

디버거를 통해 메인 함수 부분을 어셈블리 코드로 본다면 아래와 같습니다. 함수 호출시 스택 프레임이 생성되는 부분과 소멸하는 부분만을 집중적으로 다루도록 하겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0040101C       PUSH EBP
0040101D       MOV EBP,ESP
0040101F       SUB ESP,8
00401022       MOV DWORD PTR SS:[EBP-4],9
00401029       MOV DWORD PTR SS:[EBP-8],4
00401030       MOV EAX,DWORD PTR SS:[EBP-8]       ;  kernel32.76F4338A
00401033       PUSH EAX                           ; /Arg2 = 76F43378
00401034       MOV ECX,DWORD PTR SS:[EBP-4]       ; |
00401037       PUSH ECX                           ; |Arg1 = 00000000
00401038       CALL 00401000                       ; \EXAMPLE.00401000
0040103D       ADD ESP,8
00401040       PUSH EAX                           ;  kernel32.BaseThreadInitThunk
00401041       PUSH 00406030                      ;  ASCII "%d\n"
00401046       CALL 00401054
0040104B       ADD ESP,8
0040104E       XOR EAX,EAX                        ;  kernel32.BaseThreadInitThunk
00401050       MOV ESP,EBP
00401052       POP EBP                            ;  kernel32.76F4338A
00401053       RETN

 

 

위의 어셈블리 코드를 자세히 살펴보시면 메인 함수의 시작과 함께 스택 프레임을 생성하고 있습니다. 여기서 스택 프레임이 어떻게 생겼는지 구조만 살짝 봐보도록 합시다.

 

1
2
3
4
5
6
PUSH EBP
MOV EBP,ESP
...
MOV ESP,EBP
POP EBP
RETN

 

위 구조를 순서대로 살펴보도록 합시다. 먼저, EBP의 값을 스택에 올려 저장합니다. 그리고 ESP의 값을 EBP에 저장합니다. 이렇게 되면 함수 내부에서 ESP가 계속 변해도, 스택 프레임이 소멸되지 않는 이상 EBP는 변경되지 않으므로 안전하게 인수와 지역 변수에 접근할 수 있습니다. 함수를 끝내기 직전에는 ESP를 원래의 값으로 돌려놓고, EBP도 스택에 올려두었던 기존의 EBP 값으로 돌려놓은 뒤에 RETN를 만나 함수를 종료합니다. 다시 메인 함수로 돌아가서 40101F 부분부터 쭉 보도록 합시다.

 

1
2
3
0040101F       SUB ESP,8
00401022       MOV DWORD PTR SS:[EBP-4],9
00401029       MOV DWORD PTR SS:[EBP-8],4

 

위 어셈블리 코드를 천천히 보도록 합시다. 40101F에선 ESP에서 8을 감소시키고 있습니다. 이는 저장될 데이터의 크기만큼 감소시키는 것인데, 코드에서 정수형 변수 두개가 선언되니 총 8바이트가 필요하므로 스택에 저장시키기 위해서 8바이트만큼 공간을 확보하는 것입니다. 그 후, 401022~401029에서는 4바이트(DWORD) 크기에 해당하는 메모리 영역으로 각각 9와 4를 저장하고 있습니다. 여기서 SS:[EBP-4]는 지역 변수 a, SS:[EBP-8]는 지역 변수 b라는 사실을 알 수 있습니다. 이렇게 EBP를 기준으로 하여 오프셋을 더하고 빼는 작업을 통해 손쉽게 지역 변수에 접근할 수 있다는 사실을 알 수 있습니다. (참고로 BYTE는 1바이트, WORD는 2바이트, DWORD는 4바이트, QWORD는 8바이트를 의미합니다.)

 

1
2
3
4
5
00401030       MOV EAX,DWORD PTR SS:[EBP-8]       ;  kernel32.76F4338A
00401033       PUSH EAX                           ; /Arg2 = 76F43378
00401034       MOV ECX,DWORD PTR SS:[EBP-4]       ; |
00401037       PUSH ECX                           ; |Arg1 = 00000000
00401038       CALL 00401000                      ; \EXAMPLE.00401000

 

이번에는 401030에서 4바이트 SS:[EBP-8]의 값을 EAX에 저장하고, 그 저장된 값을 스택에 올립니다. 그다음 똑같이 SS:[EBP-4]의 값을 ECX에 저장하고, 저장된 값을 스택에 올리고 난 뒤에 함수 401000를 호출하게 됩니다. 이렇게 레지스터에 값을 저장하고 스택에 올리는 이유는, 메모리에서 메모리로 데이터가 직접적으로 전달될 수 없기 때문에 그렇습니다. 여기서 401000은 우리가 정의한 함수 sum임을 짐작할 수 있습니다. 이 함수 내부로 진입하게 되면, CPU가 함수 종료 후 돌아오게 될 주소를 스택에 올리게 됩니다. 이때의 스택을 잠시 확인해보도록 하겠습니다.

 

0018FF34   0040103D  
0018FF38   00000009
0018FF3C   00000004

 

함수 sum이 RETN을 만나 끝날때, 스택에 올라간 복귀 주소인 40103D를 보고 돌아올 수 있는 것입니다. 18FF38, 18FF3C는 우리가 방금 스택에 올린 변수 a, b의 값입니다. 다시 돌아와서 함수 sum(401000)의 어셈블리 코드를 보도록 합시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
00401000       PUSH EBP
00401001       MOV EBP,ESP
00401003       SUB ESP,8
00401006       MOV EAX,DWORD PTR SS:[EBP+8]
00401009       MOV DWORD PTR SS:[EBP-4],EAX
0040100C       MOV ECX,DWORD PTR SS:[EBP+C]
0040100F       MOV DWORD PTR SS:[EBP-8],ECX
00401012       MOV EAX,DWORD PTR SS:[EBP-4]
00401015       ADD EAX,DWORD PTR SS:[EBP-8]
00401018       MOV ESP,EBP
0040101A       POP EBP                             ;  EXAMPLE.0040103D
0040101B       RETN

 

위 어셈블리 코드를 간단하게 살펴보면 함수의 시작과 함께 역시 스택 프레임이 생성되며 지역 변수의 공간 확보가 이루어지고 있습니다. 새롭게 스택 프레임이 생성되면서 EBP의 값이 변경되고, 여기서의 SS:[EBP-4], SS:[EBP-8]은 각각 지역 변수 x와 y의 값이 들어가며 SS:[EBP+8], SS:[EBP+C]에는 매개 변수 a와 b의 값이 들어가게 됩니다. 그 후에 SS:[EBP-4]의 값을 EAX에 저장하고, SS:[EBP-8]의 값과 EAX의 값을 더해 EAX에 저장한 뒤에 스택 프레임을 소멸시키기 위해 ESP와 EBP의 값을 기존의 값으로 돌려 놓습니다. 그리고 RETN을 만남과 동시에 스택에 올려뒀던 복귀 주소로 돌아갑니다.

 

1
2
3
4
5
0040103D       ADD ESP,8
...
00401050       MOV ESP,EBP
00401052       POP EBP                            ;  kernel32.76F4338A
00401053       RETN

 

(생략시켜 놓은 부분은 설명했던 부분과 내용이 어느정도 비슷한 부분이므로 생략시킨 것입니다.)

40103D에서는 갑자기 ESP에서 8을 더하는 것을 보실 수가 있는데 이는 sum 함수에게 넘겨준 매개변수 a와 b가 더이상 필요하지 않으므로 8을 더해 스택을 정리하여 주는 것입니다. 여기서 왜 8인가 하면, 매개변수 a, b는 모두 정수형 변수이고 이는 각각 4바이트씩 총 8바이트의 크기를 차지하기 때문입니다. 그 다음, 401050~401053에서는 메인 함수의 스택 프레임을 소멸시키기 위해 ESP와 EBP의 값을 원래 값으로 돌려놓고 RETN를 만나 스택에 올려둔 복귀 주소인 401139로 이동합니다. (401139 부터는 Visual C++ 스텁(Stub, STARTUP) 코드 영역 이므로 더이상 보실 필요가 없습니다.)

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,

[리버스 엔지니어링 스터디]

 

Win32의 기본적인 파일 형식

PE(Portable Executable) 구조

 

 


 

오늘은 PE(Portable Executable)에 대해서 알아보도록 하겠습니다. PE라는 말을 들어보신 적이 있나요? 아마 이 글을 보고 계시는 분들 중에 리버싱 경험이 있으신 분들은 PE란 말을 이미 알고 계실겁니다. 우리가 쓰고 있는 윈도우즈 환경의 실행 파일 포맷을 PE라고 하며, "Portable"의 단어 뜻 그대로 의식성이 있으며 플랫폼에 독립적입니다. PE 파일은 굳이 확장자가 EXE인 파일만 일컫는게 아니며 SCR, SYS, DLL, OCX 등도 포함이 되고, 중간 파일인 OBJ 파일도 PE 파일이라고 간주합니다. PE 구조의 이해는 API 후킹, 압축 실행 파일 등과 같은 고급 리버싱 기법의 기본 바탕이 됩니다. 자, 이제는 PE의 전체적인 구조를 살펴보고 이해하도록 합시다!

 

PE(Portable Executable)의 전체적인 구조 살펴보기

 

우선은 헥스 에디터로 계산기(calc.exe) 파일을 열어보도록 합시다.

 

271C454C520796891039DB

 

위 그림에서 보이는 부분이 calc.exe의 시작 영역이자, PE 헤더(PE Header) 영역이라고 할 수 있습니다. 위의 PE 헤더에는 calc.exe와 같은 실행 파일을 실행하기 위한 여러가지 정보가 기록이 되어 있으며, PE의 내용을 가지고 DLL를 로드하거나 여러가지 리소스를 할당하는 등 상당히 많은 정보가 PE 헤더에 저장되어 있습니다. 중요한 정보가 담긴 만큼, 이 영역의 일부가 누락되거나 손상되는 경우가 있으면 정상적으로 해당 파일을 실행할 수 없습니다. 파일을 백업해두고 HEX 코드 일부를 수정하시고 저장하셔서 실행을 하여 어떠한 결과를 초래하는지 보세요.

 

PE 구조는 차례대로 DOS Header, Stub Code, PE Header(File Header, Optional Header 포함), Section Header로 나뉘며 그 뒤에는 보통 코드를 포함하는 코드(.text) 섹션, 전역 변수 혹은 정적 변수를 포함하고 있는 데이터(.data) 섹션, 문자열이나 아이콘 같은 리소스 데이터를 포함하는 리소스(.rsrc) 섹션으로 나뉘어 등장합니다. 이러한 섹션은 1개 이상 존재하며, 섹션들 사이에 HEX 코드가 00(NULL)으로 나타나는 부분은 정렬 규칙에 의해 크기를 버리고 처리 효율을 높이기 위해 사용하는 영역으로 패딩(padding) 영역이라고 할 수 있습니다.

 

 

 

 

2602A7335207BD6F259CA1

 

참고로 위 그림에서 섹션의 왼쪽에 있는 16진수 값은 해당 섹션의 크기를 말하는 겁니다.

 

27791E3D5207C809096B5F

 

 

위 그림에서는 우선 VirtualSize(가상 메모리에서 해당 섹션이 차지하는 크기), VirtualOffset(가상 메모리 오프셋, VA), RawSize(파일에서 해당 섹션이 차지하는 크기), RawOffset(파일 오프셋) 요 부분은 한번 보도록 합시다. (그림 내에 있는 RVA와 RAW란 녀석은 PE 헤더에 대해 알아볼때 다시 얘기를 할 것입니다.)

 

약간 살펴보시면, 파일이 메모리에 적재되고 나서는 섹션의 크기 혹은 형태 등이 달라졌음을 확인하실 수 있습니다. 또한 파일은 offset, 메모리에는 address라고 되어 있는데, 이는 파일이 오프셋(offset)으로 위치를 표현하고 메모리는 address, VA(Virtual Address, 가상 메모리의 절대 주소)로 위치를 표현한다고 할 수 있습니다. 이제는 PE 헤더에 대해서 조금씩 알아가면서 PE 구조를 이해하도록 하겠습니다.

 

PE 헤더(Portable Executable Header)

 

우리가 알아볼 헤더라는 것은 사실은 여러 가지 필드로 이루어진 하나의 구조체라고 말할 수 있습니다. PE에는 여러가지 헤더가 앞에 자리잡고 있으므로 이는 PE가 여러가지 구조체로 구성이 되어있다고 말할 수 있습니다. 아래에서는 도스 스텁을 제외한 나머지의 헤더를 크게 3개(도스 헤더, NT 헤더, 섹션 헤더)로 나누어 해당하는 헤더의 구조체를 가지고 설명합니다. PE를 구성하는 영역을 도스 헤더부터 섹션 헤더까지 차례대로 살펴보도록 합시다.

 

1. IMAGE_DOS_HEADER

 

가장 처음으로 등장하는 영역은 도스 헤더의 영역입니다. 아래의 코드는 winnt.h 헤더의 일부(나머지 헤더 구조체와 매크로 상수 역시 이 헤더에서 가져옴)로 도스 헤더의 구조체입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define IMAGE_DOS_SIGNATURE                 0x4D5A      // MZ
 
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

위 구조체는 데이터 타입이 WORD인 필드가 총 16개, WORD[]인 필드가 2개, LONG인 필드가 1개로, 해당 구조체의 크기는 총 64 바이트(16진수로는 0x40)이며 필드는 총 19개임을 알 수 있습니다. 위 코드를 보시면 필드가 상당히 많은데, 겁먹을 필요 없이 여기서는 딱 두가지의 필드만 보시면 됩니다. e_magic 필드와 e_lfanew 필드만 보시면 됩니다.

 

272E05485208E5F2352AB8

 

e_magic: e_magic 필드는 PE 파일이 맞는지 아닌지 체크할 때 사용되며, PE 파일 처음부터 2바이트까지 보시면 4D 5A(IMAGE_DOS_SIGNATURE)로 시작하는 부분이 e_magic이 차지하는 공간입니다. 여기서 MZ는 DOS의 설계자인 마크 즈비코프스키(Mark Zbikowski)에서 이름을 따온 것이며, 도스 헤더의 시작을 알리는 코드라 할 수 있습니다. PE 파일의 맨 앞에 위치한 e_magic은 MZ(4D 5A)라는 코드로 고정되어 있으며 가장 앞에서 2바이트를 읽어온 후에 IMAGE_DOS_SIGNATURE와 비교를 하여 서로 다르다면 그것은 PE 파일 구조가 아니라고 할 수 있습니다.

 

e_lfanew: e_lfanew 필드는 IMAGE_NT_HEADER의 시작 오프셋을 가지며, 고정되어 있는 값이 아닌 파일에 따라 가변적인 값을 지닙니다. 즉, PE 헤더(NT 헤더)의 주소는 도스 헤더의 e_lfanew 필드를 참조하여 알아낼 수 있다는 것이 됩니다. 위 그림에서 e_lfanew 필드의 값은 000000D8 입니다. 주의하실 점은 값이 D8000000이 아니라는 겁니다. 이는 리틀 엔디언 표기법때문에 그런데, 여기서 잠깐 리틀 엔디언 표기법에 대해 잠시 알아보도록 합시다.

 

1-1. 리틀 엔디언(Little Endian) 표기법

 

리틀 엔디언(Little Endian) 표기법에 대해 간단히 알아보도록 합시다. 리틀 엔디언은 무엇일까요? 리틀 엔디언 표기법은 Intel 계열 CPU에서 사용하는 표기법으로 낮은 주소부터 시작하여 하위 바이트를 기록하는 것을 말합니다. 만약 0x075BCD15라는 값을 어떠한 저장 공간에 기록하고 싶다면, 하위 바이트부터 시작하여 차례대로 15 CD 5B 07으로 저장되게 됩니다. 바이트의 순서는 이렇지만, 실제 값이 0x15CD5B07이 아니라 0x075BCD15라는 것입니다.

 

23077D385208F23C2FA1C9

 

위 그림에서도 0x11223344라는 값이 메모리 공간에 저장된다고 하면, 11 22 33 44 그대로 저장되는게 아니라, 낮은 주소부터 시작하여 하위 바이트를 기록하니 44 33 22 11과 같은 식으로 기록이 됩니다. 반대로 낮은 주소부터 시작하여 상위 바이트를 기록하는 방법은 빅 엔디언(Big Endian) 표기법이라고 합니다.

 

2. Stub Code

 

스텁 코드(Stub Code)가 무엇인지 잠시 아래 그림을 봐보도록 합시다.

 

233358475208F4EE19113B

 

위의 영역이 바로 도스 스텁 영역입니다. 저 스텁 영역을 자세히 보시면 "This program cannot be run in DOS mode"라는 문자열을 볼 수 있으며, 도스 모드에서 이 파일이 실행되는 것을 막기 위한 것입니다. (16비트 환경에서 실행되는 영역이며, 32비트 환경에선 실행되지 않는 영역입니다) 예전에 쓰던 MS-DS를 구해 가상머신 등으로 설치한 뒤에 윈도우 프로그램을 실행하려 한다면 저 메시지를 볼 수 있습니다. 하위 호환성을 위해서 만든 메시지이라고 생각하시면 됩니다.

 

도스 헤더 구조체에서의 e_lfanew 필드가 고정적인게 아닌 가변적인 것도 도스 스텁 영역의 크기가 가변적이라서 그렇습니다. e_lfanew 필드는 IMAGE_NT_HEADER의 시작 오프셋을 가진다고 했었고, 도스 스텁 영역의 다음이 IMAGE_NT_HEADER 구조체가 위치하니 도스 스텁 영역의 크기가 변하면 e_lfanew 필드의 값도 변합니다. 도스 스텁 영역은 크게 신경 안쓰셔도 되는 영역이라고 생각하시면 됩니다.

 

3. IMAGE_NT_HEADER

 

IMAGE_DOS_HEADER 구조체에 이어 IMAGE_NT_HEADER 구조체를 살펴보도록 하겠습니다.

1
2
3
4
5
6
7
#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00
 
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

 

Signature: _IMAGE_NT_HEADERS의 필드를 살펴보시면 Signature 필드가 가장 처음으로 등장하는데, 데이터 타입이 DWORD니 4바이트를 차지하며, 이 Signature의 값을 가지고 PE 파일 구조인지 아닌지 체크할 수 있습니다. Signature의 값은 IMAGE_NT_SIGNATURE 상수 그대로 PE 00 이라는 값을 지닙니다. 저장되는 순서는 50 45 00 00 이며, 한번 직접 Signature 필드가 어디에 위치하여 있는지 확인해보도록 합시다.

 

2765D13352090CD20419AC

 

위 그림에 표시된 영역이 바로 Signature 필드의 공간입니다. 우리가 생각하는 값이 나왔네요. 한번, 시험삼아 파일을 백업해두고 도스 헤더 영역의 e_magic 필드나 IMAGE_NT_HEADERS의 Signature 필드를 임의의 값으로 수정하여 실행해보시면 엉뚱한 값이 들어감을 확인하여 올바른 PE 파일이 아니라고 에러 처리를 할 것입니다.

 

그리고, Signature 말고도 FileHeader와 OptionalHeader 필드가 있는데 이는 IMAGE_FILE_HEADER, IMAGE_OPTIONAL_HEADER를 설명하면서 같이 알아보도록 하겠습니다.

 

3-1. IMAGE_FILE_HEADER

 

파일 헤더 구조체인 IMAGE_FILE_HEADER에는 PE 파일에 대한 기본적인 내용이 담겨 있습니다. 우선은 아래의 코드를 보도록 합시다.

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_DOS_HEADER만큼은 아니지만 그래도 필드가 좀 많아보이죠? 여기에서는 Machine과 NumberOfSections, TimeDateStamp, SizeOfOptionalHeader, Characteristics 이 5개의 필드에 대해서 알아보도록 하겠습니다. 먼저 Machine을 보도록 합시다.

 

Machine: Machie 필드는 이 파일이 어떤 CPU에서 동작할 수 있는지, 실행될 수 있는 CPU의 타입을 정합니다. 아래는 winnt.h에 정의된 Machine 상수 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

예를 들면, Machine 필드의 값이 014C라면 Intel x86 CPU와 호환이 된다는 것입니다. 한번 calc.exe의 Machine 필드의 값을 확인해보도록 합시다.

 

234512495209173026C9B2

 

 

저기 보시면 4C01로 기록되어 있고, 값은 014C로 IMAGE_FILE_MACHINE_I386 상수와 값이 일치합니다.

 

NumberOfSections: 이 필드는 PE 파일을 구성하는 섹션(Section)의 수로 섹션이 추가되면 이 값이 늘어나고 섹션이 줄어들면 이 값 역시 줄어듭니다. 이 값을 보고 이후에 등장할 섹션의 수를 알아낼 수 있으며 이 필드의 값은 0보다 커야 합니다. 이는 섹션이 한개라도 없는 경우가 존재하지 않는다는 것입니다. 한번 NumberOfSection 필드의 값을 보도록 해봅시다.

 

22370A4E5209188810F4EB

 

 

앞에서 보았듯이 .text, .data, .rsrc, .reloc 섹션이 존재하며, 섹션의 수는 4개입니다. 위 그림에서 NumberOfSections의 값이 0004인 것을 확인하실 수 있으며, 이는 섹션의 수가 4개라는 것으로 실제 섹션의 수와 일치한다는 것을 확인하실 수 있습니다.

 

TimeDateStamp: TimeDateStamp 필드는 PE 파일이 만들어진 시간, 즉 이 파일이 빌드된 날짜가 타임스탬프 형식으로 기록됩니다. 그러나 이는 확실히 신뢰할 수 있는 값이 아니며 변조가 가능하니 대략적인 값으로 생각을 하셔야 합니다. 한번 TimeDateStamp 필드의 값도 확인을 해보도록 합시다.

 

2126C04E52091B5B0B6EED

 

 

TimeDateStamp의 값은 4CE7979D라고 할 수 있고, 이 16진수 값을 10진수 값으로 바꾸면 1290246045 입니다. 이를 다시 표준 시각으로 바꾸면 "Saturday, November 20th 2010, 09:40:45 (GMT)"로, 2010년 11월 20일 9시 40분 45초에 빌드되었다고 대략 예상할 수 있습니다.

 

SizeOfOptionalHeader: SizeOfOptionalHeader 필드에는 옵셔널 헤더(IMAGE_OPTIONAL_HEADER32)의 크기가 담깁니다. IMAGE_OPTIONAL_HEADER의 크기는 정해져 있는것 같지만, 운영체제마다 크기가 가변적이기 때문에 PE 로더가 이 SizeOfOptionalHeader 필드의 값을 확인하고 IMAGE_OPTIONAL_HEADER의 크기를 처리합니다. 이것 역시 한번 직접 값이 어떤지 보도록 합시다.

 

232A034D52091CE02B3C89

 

 

위 그림에서 SizeOfOptionalHeader 필드의 값을 확인하실 수 있으며, 값은 00E0으로 10진수로는 224 바이트 만큼을 자치한다고 할 수 있습니다. 이 필드의 값은 32비트에선 0xE0, 64비트에서는 0xF0의 크기를 지닌다고 합니다.

 

Characteristics: Characteristics 필드는 현재 파일의 형식을 알려주는 역할을 하며, 이 필드의 값을 가지고 실행 가능한 파일인지, DLL 파일인지, 시스템 파일인지, 재배치 여부 등에 대한 정보가 들어있다고 할 수 있습니다. 아래는 winnt.h에 정의된 Characteristics 상수입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

위 상수의 값은 비트 플래그를 사용한 것으로, 2진수 형식으로 증가합니다. 우선은 직접 파일의 Characteristics 필드를 확인하여 보도록 합시다.

 

2226234852091E9C0AA06C

 

 

보시면 Characteristics 필드의 값이 0102라는 것을 알 수 있으며, 이는 0100과 0002를 합한 값과 같습니다. 0x0100은 IMAGE_FILE_32BIT_MACHINE이며, 0x0002는 IMAGE_FILE_EXECUTABLE_IMAGE 인 것을 확인하실 수 있습니다. 이는 즉, 32비트 머신을 필요로 하며, 실행 가능한 파일임을 알 수 있습니다. 파일 헤더의 구조체에 대한 설명은 여기서 마치고, 다음으로는 옵셔널 헤더의 구조체에 대해 알아보도록 하겠습니다.

 

3-2. IMAGE_OPTIONAL_HEADER

 

이번에는 IMAGE_OPTIONAL_HEADER 구조체에 대해서 간단히 알아보도록 하겠습니다. 우선 아래의 코드를 보도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 
typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

IMAGE_OPTIONAL_HEADER 구조체는 PE 구조체 중에서도 가장 크기가 큰 구조체로, 필드의 수가 상당히 많아보이죠? 크기가 큰 만큼 중요한 값도 많이 지니고 있습니다. 총 31개의 필드를 지니고 있고, 이 중에서도 11개의 필드에 대해서 간략히 알아볼 생각입니다. 순서대로 나열하면 Magic, SizeOfCode, ImageBase, AddressOfEntryPoint, BaseOfCode, SectionAlignment, FileAlignment, SizeOfImage, SizeOfHeaders, Subsystem, DataDirectory에 대해 알아볼 것이며, 가장 처음으로 Magic 필드부터 시작하여 차근차근 알아보도록 하겠습니다.

 

Magic: 32비트(IMAGE_OPTIONAL_HEADER32)인 경우에는 값이 10B이며, 64비트(IMAGE_OPTIONAL_HEADER64)인 경우에는 20B라는 값을 가집니다.
 

2426FB3B5209A855478D21

 

 

위 그림에서 Magic 필드의 값이 010B임을 확인하실 수 있으며, 이는 32비트(IMAGE_OPTIONAL_HEADER32) 구조체임을 확인할 수 있습니다.

 

SizeOfCode: 코드 영역의 전체 크기가 이곳에 들어갑니다. 이는 .text 섹션의 크기가 들어간다는 것입니다. SizeOfCode의 값을 확인해보도록 하고, 이어서 .text 섹션의 크기와 SizeOfCode의 값이 일치하는지 비교도 해보도록 합시다.

 

 

2435353A5209ABE817879E

 

 

위 그림에서 SizeOfCode의 값을 확인해보면 00052E00이며, 이는 .text의 크기와 정확히 일치합니다.

 

2539E1355209ACE7219DB4

 

 

RawSize는 파일에서 해당 섹션이 차지하는 크기로 .text 섹션이 52E00만큼 공간을 차지한다고 볼 수 있습니다.

 

ImageBase: PE 파일이 메모리에 로드될 때의 시작 주소를 가리킵니다. 기본적으로 EXE 파일의 경우에는 0x400000 번지가, DLL 파일인 경우에는 0x10000000 번지로 지정되어 있으며 그렇다고 해서 항상 이 번지로 고정되어 있는게 아니라는 점을 주의하셔야 합니다. (이는 링커 옵션을 통해서 시작 주소를 지정할 수 있습니다) DLL의 경우는 기본 ImageBase의 값이 0x10000000 번지로 지정되어 있지만, 다른 DLL이 이 번지를 차지하고 있을 경우에는 다른 곳에 배치되는 재배치가 이루어집니다.

 

이 ImageBase는 RVA의 기준이 되며, 여기서 RVA(Relative Virtual Address, 상대 가상 주소)란 ImageBase를 기준으로 하여 어느만큼 떨어져 있는지를 나타내는 값으로 파일의 오프셋(Offset)과 같은 개념이라고 할 수 있습니다. 다만, RVA는 파일이 아닌 메모리 공간에서의 상대적인 값으로 예를 들면, ImageBase가 0x400000 번지일 경우 .text 섹션의 RVA 값이 0x3000 이라면 실제로 .text 섹션이 로드되는 위치는 0x403000이 되는 것입니다.

 

우선은 calc.exe의 ImageBase의 값을 보도록 하겠습니다.

 

221783355209B3990C6541

 

위 그림에서 ImageBase의 값은 01000000이며, 0x1000000이 시작 주소임을 확인할 수 있습니다.

 

AddressOfEntryPoint: 프로그램이 메모리에서 실행 되는 시작 지점이며, 이는 진입점(Entry Point)를 말하는 것입니다. 위치는 RVA 값으로 저장되어 있으며, WinMain 혹은 DllMain의 번지라고 생각할 수 있습니다. (정확하게는 Start up의 번지라고 할 수 있습니다.) 실제로 다음 실행할 명령이 들어있는 메모리의 번지를 가지는 EIP 레지스터의 값을 파일이 메모리에 로딩되고 나서 ImageBase + AddressOfEntryPoint로 지정합니다. (올리디버거 같은 디버거를 통해서 파일을 실행시키고 나면 디버거가 처음 실행할 위치를 ImageBase + AddressOfEntryPoint로 잡습니다.)

 

251C543A5209B83F090923

 

 

위 그림에서 AddressOfEntryPoint의 값을 보시면 00012D6C 인 것을 확인할 수 있습니다. ImageBase + 12D6C의 값은 진입점의 주소라고 생각할 수 있습니다.

 

BaseOfCode: 코드 영역이 시작되는 상대 주소(RVA)가 담깁니다. BaseOfCode가 RVA니 ImageBase + BaseOfCode의 값은 실제 코드 영역의 주소가 됩니다.

 

253800335209C84E1BD4DA

 

 

위 그림을 보시면 BaseOfCode의 값이 00001000(0x1000)으로, 만약 ImageBase가 0x400000이라면 0x401000이 실제 코드 영역의 주소입니다.

 

SectionAlignment: 메모리에서 섹션의 최소단위를 나타냅니다. 메모리에서 섹션의 크기는 반드시 SectionAlignment의 배수가 되어야 합니다.

 

247E16505209CB8C298D18

 

위 그림에서 SectionAlignment의 값은 00001000(0x1000) 입니다. 이는 메모리 공간에서 섹션의 크기가 0x1000의 배수라고 할 수 있습니다. 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워지며 이 것을 패딩(padding)이라고 합니다.

 

FileAlignment: 파일에서 섹션의 최소단위를 나타냅니다. 파일에서 섹션의 크기는 반드시 FileAlignment의 배수가 되어야 합니다.

 

252A2E505209CB8C051055

 

위 그림에서 FileAlignment의 값은 00000200(0x200) 입니다. 이는 파일에서 섹션의 크기가 0x200의 배수라고 할 수 있습니다. SectionAlignment와 마찬가지로 섹션의 크기에서 구조체 크기를 제외한 빈 공간은 모두 0으로 채워집니다.

 

SizeOfImage: PE 파일이 메모리에 로딩되었을 때의 전체 크기를 담고 있습니다. 이 값은 파일의 크기와 같을 때도 있으며, 다를때도 있으나 다른 경우가 더 많습니다. PE 파일이 메모리에 로딩되고 나서는 SectionAlignment의 영향을 받아 패딩이 따라붙으며, SizeOfImage 역시 SectionAlignment의 영향을 받는다고 할 수 있습니다.

 

243159365209CFFF125E9C

 

위에서 SizeOfImage의 값은 000C0000(0xC0000)이며, 이는 PE 파일이 메모리에 로딩되었을 때의 전체 크기가 0xC0000 라는 말이 됩니다.

 

SizeOfHeaders: 이름 그대로 모든 헤더의 크기를 담고 있습니다. 즉, 도스 헤더, 도스 스텁, PE 헤더, 섹션 헤더의 크기를 모두 더한 값이라고 할 수 있으며 파일의 시작점에서 SizeOfHeaders 만큼 떨어진 Offset에 첫번째 섹션이 존재합니다.

 

277494365209CFFF1D89E0

 

위 그림에서 SizeOfHeaders의 값은 00000400(0x400) 이며, 헤더의 총 크기는 0x400이라고 할 수 있습니다.

 

Subsystem: 이 값을 통해 시스템 드라이버 파일인지, 프로그램이 GUI 혹은 CUI 인지 알아낼 수 있습니다.

1
2
3
#define IMAGE_SUBSYSTEM_NATIVE               1   // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI          2   // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI          3   // Image runs in the Windows character subsystem.

1인 경우에는 시스템 드라이버 파일, 2인 경우에는 GUI 파일, 3인 경우에는 CUI 파일입니다. 이것 외에도 OS2, POSIX, CE 등과 같은 서브시스템이 존재하지만 GUI와 CUI 둘 중 하나인 것이 많아 위의 3개의 상수만 보여드렸습니다.

 

256B69415209E0A20B0620

 

위 그림에서는 Subsystem의 값이 0002(0x2)로 GUI 파일이 되겠습니다. 만약 이 값이 0x3이였다면, CUI 파일이라고 할 수 있습니다.

 

DataDirectory: IMAGE_DATA_DIRECTORY 구조체를 보시면 VirtualAddress와 Size라는 필드가 존재합니다. 우선은 아래의 코드를 먼저 보도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

IMAGE_OPTIONAL_HEADER의 DataDirectory 필드는 익스포트 디렉터리, 임포트 디렉터리, 리소스 디렉터리, 예외 디렉터리, 보안 디렉터리 영역 등에 접근할 수 있는 주소와 크기를 지니고 있는 배열로, IMAGE_DATA_DIRECTORY 구조체의 VirtualAddress를 통해 가상 주소를 알 수 있으며, Size를 통해 크기를 알 수 있습니다. 여기서 중요한 값은 EXPORT, IMPORT, RESOURCE, TLS, IAT인데 우선은 이것들을 잘 기억해두시기 바랍니다. 이부분에 대해서는 추후에 다시 설명하도록 하겠습니다.

 

4. IMAGE_SECTION_HEADER

 

옵셔널 헤더의 다음으로 섹션 헤더에 대해서 알아볼텐데, 섹션 헤더는 섹션 테이블이라고도 하며 IMAGE_SECTION_HEADER 구조체는 섹션에 대한 정보를 관리하는 구조체라고 할 수 있습니다. 이 구조체를 가지고 .text 섹션이나, .data 섹션, .rdata 섹션 등에 대한 정보를 알 수 있다는 것입니다. 우선은 아래의 코드를 먼저 보도록 합시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define IMAGE_SIZEOF_SHORT_NAME              8
 
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
 
#define IMAGE_SIZEOF_SECTION_HEADER          40

위 코드를 보시면 IMAGE_SECTION_HEADER가 보이는데, 필드는 총 11개로 이 중에서 Name, VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData, Characteristics 필드만 간략하게 알아보도록 하겠습니다. 먼저 Name 필드부터 알아보도록 합시다. 아래에서는 .text 섹션의 헤더를 가지고 설명을 하도록 하겠습니다.

 

2723C83E5209F51F14422C

<.text 섹션 헤더가 차지하는 공간>

 

Name: 필드명 그대로 섹션의 이름을 나타냅니다. 저기 상수 IMAGE_SIZEOF_SHORT_NAME의 값인 8, 섹션의 이름은 최대 8바이트까지 가능하다는 겁니다. 그리고 이 필드의 값은 NULL로 비어있을 수 있으며, 다 꽉 채울수도 있습니다. 기본적인 섹션의 이름에 대한 용도는 아래에 표로 정리해 두었습니다.

 

 

섹션명

용도

.text

코드, 실행, 읽기 속성을 지니며 컴파일 후의 결과가 이곳에 저장됩니다. 즉, 이 섹션은 실행되는 코드들이 들어가는 섹션입니다.

.data

초기화, 읽기, 쓰기 속성을 지니며 초기화된 전역 변수를 가집니다.

.rdata

초기화, 읽기 속성을 지니며 문자열 상수나 const로 선언된 변수처럼 읽기만 가능한 읽기 전용 데이터 섹션입니다.

.bss

비초기화, 읽기, 쓰기 속성을 지니며 초기화되지 않은 전역 변수의 섹션입니다.

.edata

초기화, 읽기 속성을 지니며 EAT와 관련된 정보가 들어가 있는 섹션입니다.

.idata

초기화, 읽기, 쓰기 속성을 지니며 IAT와 관련된 정보가 들어가 있는 섹션입니다.

.rsrc

초기화, 읽기 속성을 지니며 리소스가 저장되는 섹션입니다.

 

 

먼저 위 그림에서의 Name 필드를 보도록 합시다. Name 필드는 8바이트를 차지하니, 앞에서부터 8바이트를 그대로 읽으시면 됩니다. 읽었더니 2E 74 65 78 74 (NULL 생략)이며, 2E는 10진수로 64고 이 아스키코드에 해당하는 문자는 '.'이며, 74는 116으로 't', 65는 101으로 'e', 78은 120으로 'x', 74는 116으로 't' 합쳐서 ".text"라고 읽습니다.

 

VirtualSize: PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 메모리에서 섹션이 차지하는 크기를 가집니다. 위 그림에서 VirtualSize와 PhysicalAddress를 필드로 갖는 공용체가 존재하지만, 여기서 PhysicalAddress는 현재 사용되지 않는 필드고 VirtualSize 필드만 사용됩니다. Name 필드 다음부터 4바이트를 읽으면 A1 2C 05 00이 되고, 이는 00052CA1 이라는 값이 됩니다.

 

VirtualAddress: PE 로더를 통해 PE 파일이 메모리에 로드되고 나서의 해당하는 섹션의 RVA 값입니다. 즉, RVA는 이미지 베이스를 기준으로 하는 것이기에 예를 들어서 ImageBase의 값이 0x400000이고, VirtualAddress의 값이 0x1000이라면 로더는 0x401000에 섹션을 올리게 됩니다. 즉 ImageBase + VirtualAddress는 해당 섹션의 실제 주소값이라고 할 수 있습니다. 위 그림에서 VirtualSize 필드 다음부터 4바이트를 읽게 되면 00 10 00 00으로, 이는 00001000(0x1000)의 값이 됩니다.

 

SizeOfRawData: 파일 상에서의 해당 섹션이 차지하는 크기(옵셔널 헤더 구조체의 FileAlignment 값의 배수가 되도록 올림한 값)를 가집니다. 이는 실제로 사용된 크기이며, 패딩을 제외한 크기라고 할 수 있습니다. 위 그림에서 VirtualAddress 필드 다음부터 4바이트를 읽게 되면 00 2E 05 00으로, 이는 00052E00(0x52E00)의 값이 됩니다.

 

PointerToRawData: 파일 상에서의 해당 섹션이 시작하는 위치(파일 오프셋)를 담고 있습니다. 이 값 역시도 옵셔널 헤더 구조체의 FileAlignment 값의 배수가 되어야 하며, 위 그림에서 SizeOfRawData 필드 다음부터 4바이트를 읽게 되면 00 04 00 00으로, 이는 00000400(0x400)이 됩니다.

 

Characteristics: 섹션의 속성 정보를 플래그로 지니며, 여기서는 6가지의 속성만 알아보도록 하겠습니다.

1
2
3
4
5
6
#define IMAGE_SCN_CNT_CODE                   0x00000020  // 코드로 채워진 섹션
#define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  // 데이터가 초기화된 섹션
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  // 데이터가 비초기화된 섹션
#define IMAGE_SCN_MEM_EXECUTE                0x20000000  // 실행 가능한 섹션
#define IMAGE_SCN_MEM_READ                   0x40000000  // 읽기가 가능한 섹션
#define IMAGE_SCN_MEM_WRITE                  0x80000000  // 쓰기가 가능한 섹션

한가지의 속성만 지닐 수 있는게 아닌, 여러가지 속성이 조합된 값을 가지며 만약 이 필드의 값이 0x60000020이라면, 0x40000000(IMAGE_SCN_MEM_READ), 0x20000000(IMAGE_SCN_MEM_EXECUTE), 0x00000020(IMAGE_SCN_CNT_CODE)의 플래그를 지니는 것으로 해당 섹션은 실행 가능한 섹션이며, 읽을 수 있고 코드로 채워진 섹션이라고 할 수 있습니다. 위 그림에서는 영역의 마지막 4바이트를 보시면 20 00 00 60으로, 이는 60000020(0x60000020)이 되는 것이라고 할 수 있습니다.

 

IAT(Import Address Table)

 

IAT(Import Address Table)을 알아보기 전에 몇몇 의문점을 풀어나가보려 합니다. 여기서는 DLL(Dynamic Linked Library, 동적 연결 라이브러리)에 대한 기본적인 지식은 갖추고 있다고 가정하겠습니다. 우리가 쓰고 있는 Win32 응용 프로그램은 과연 함수를 호출할 때 어떤 함수가 어디에 있는지는 어떻게 알고있는 걸까요? 프로그램은 혼자 실행되는 것이 아니라 외부 DLL를 로딩하여 함수를 호출합니다. 예를 들면, Win32 API 함수의 경우는 시스템 DLL에서 가져와 사용을 하게 되는데 함수가 어떤 방식으로 호출되는지 알아보기 위하여 FindWindow API를 사용하는 간단한 프로그램을 디버깅 해보도록 하겠습니다.

 

257A9C47520A11F824185D

 

 

0x401031 번지를 보시면 FindWindow 함수를 직접 호출하는게 아니라,

 

2749544F520A29093BAEF8

 

 

042528C 번지에 있는 74FFFB43이라는 값을 가져와 호출을 하는 간접적인 호출 방식입니다. 왜 이러한 호출 방식을 사용하는 것일까요? 그냥 편하게 CALL 74FFFB43과 같이 직접적인 방식으로 호출을 하면 안되는 걸까요? 한번 간접 호출 방식이 아니라 직접 호출 방식으로 바뀌었다고 가정을 해봅시다. 그렇다면 CALL 74FFFB43과 같은 문장을 만나면 74FFFB43이란 주소가 user32.dll에 있는 FindWindowA의 실제 주소여야 합니다. 하지만 항상 user32.dll를 사용하려고 로딩할때마다 FindWindowA 함수가 74FFFB43를 실제 주소값으로 갖는게 아니라 다른 외부 DLL이 미리 자리를 차지하고 있으면 PE 로더가 다른 빈 공간을 찾아 로딩을 하여 재배치가 이루어지거나, 운영체제의 환경에 따라 user32.dll 내의 FindWindowA의 주소값이 바뀌기도 합니다.

 

그렇기 때문에, 어떠한 환경에서든 FindWindowA 함수의 호출을 보장하기 위해서 컴파일러가 42528C 번지에 미리 공간을 마련하고 파일이 실행된 직후에 PE 로더가 이 공간에 FindWindowA의 실제 주소를 넣어주는 것입니다. 실제로, DLL 내의 함수 주소들을 모아 놓은 테이블을 만들고 코드 섹션에서 만들어 놓은 테이블을 가져다 쓰는 방식으로 관리를 하며 여기서 이 테이블을 IAT라고 합니다. 이 부분에 대해서는 차차 알아가도록 하도록 하고, 우선은 IMAGE_IMPORT_DESCRIPTOR 구조체에 대해 간단히 알아보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
 
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
 
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

위 구조체가 바로 IMAGE_IMPORT_DESCRIPTOR 구조체 입니다. 이 구조체에서는 6개의 필드중OriginalFirstThunk, Name, FirstThunk 필드만 알아보도록 하겠습니다. 하지만, 그전에 IMAGE_IMPORT_DESCRIPTOR가 PE 파일의 어느 곳에 위치하는지 알아보도록 하겠습니다. 옵셔널 헤더 구조체 내의 구조체 배열 DataDirectory의 2번째 요소가 바로 Import Directory 인것 기억하시죠?

 

256EE737520A44E133577B

 

여기서 앞의 4바이트는 VirtualAddress이며, 뒤의 4바이트는 VirtualSize로 VirtualAddress의 값은 00051AFC(RVA)이며, VirtualSize의 값은 00000154입니다. 여기서 RVA의 값을 가지고 RAW를 알 수 있으며 "RAW = RVA - VirtualAddress(메모리 공간에서의 섹션 시작 주소) + PointerToRawData(파일에서의 섹션 시작 위치)"의 식을 통해 RAW의 값을 알아낼 수 있습니다. 51AFC는 .text 섹션에 속해있으며, .text 섹션의 VirtualAddress 값은 1000, PointerToRawData의 값은 400입니다. 이 식을 통해 RAW의 값을 구하면 RAW = 51AFC - 1000 + 400, RAW는 50EFC이 됩니다.

 

2137083C520A4F0D04AB55

 

 

 

위 영역이 모두 IMAGE_IMPORT_DESCRIPTOR 구조체 배열이며 이 영역 처음에서 20바이트 까지는 구조체 배열의 첫번째 요소라 할 수 있습니다. OriginalFirstThunk에 대해 간단히 알아보기 전, IMAGE_THUNK_DATA라는 구조체를 잠시 보고 넘어가도록 하겠습니다.

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

IMAGE_THUNK_DATA의 필드는 총 4개로 모두 공용체의 멤버이며, 공용체의 멤버인 만큼 DWORD가 차지하는 크기인 4바이트의 공간을 4개의 필드가 공유하며, 이 구조체는 AddressOfData 필드만 쓰이기도 하며, Ordinal 필드만 쓰이기도 하고, Function 필드만 쓰이기도 합니다. 

 

OriginalFirstThunk: Import Name Table(INT)의 RVA를 지닙니다. 혹은 Import Lookup Table(ILT)의 RVA를 지닌다고 합니다. 여기서 INT는 구조체 IMAGE_THUNK_DATA의 배열로 구성이 되며, 구조체 IMAGE_THUNK_DATA의 AddressOfData 필드는 실제 Import되는 함수의 이름이 포함된 구조체 IMAGE_IMPORT_BY_NAME에 대한 RVA를 지닙니다.

 

위 그림에서 구조체 배열 첫번째 요소의 OriginalFirstThunk 값을 읽으면 00051D20(RVA)이고, 이는 51D20 - 1000 + 400으로 RAW는 51120 입니다. 51120로 이동하여 한번 보도록 합시다.

 

2677BC39520A59E416C273

 

 

위 그림에서 영역 지정된 부분은 INT 영역, 즉 구조체 IMAGE_THUNK_DATA 배열의 영역이며 IMAGE_THUNK_DATA의 크기는 총 4바이트 입니다. (4개의 필드가 있으나 이는 모두 다 공용체의 안의 필드라서 같은 공간을 공유합니다) 그리고 INT의 끝은 NULL로 알 수 있으며, 이는 즉 읽어낸 4바이트가 모두 0일 경우에 그곳을 INT의 끝이라고 할 수 있습니다. 우선 첫번째 값을 읽으면 00052350(RVA)인데, RVA를 RAW로 바꾸면 52350 - 1000 + 400이니까 51750이 됩니다. 51750으로 이동해보도록 합시다.

 

2713723B520A5BD6332923

 

 

위 그림에서 드래그 된 영역은 구조체 IMAGE_IMPORT_BY_NAME의 영역으로 여기서 라이브러리 안 함수의 이름이 나왔는데, 앞의 2바이트(WORD)인 00E1은 Ordinal로 라이브러리 내의 함수 고유 번호라고 할 수 있습니다. 문자열의 끝은 \0(NULL)이므로 함수명을 읽으면 SHGetSpecialFolderPathW가 되겠습니다.

 

Name: 임포트(Import)된 DLL의 이름을 담은 문자열의 주소를 지닙니다. 

 

위 그림에서 구조체 배열 첫번째 요소의 Name의 값을 읽으면 00051D14(RVA)이고, 이는 51D14 - 1000 + 400으로 RAW는 51114가 됩니다. 51114로 이동하여 어떤 DLL이 임포트 되었는지 한번 보도록 합시다.

 

2474C041520A5F2215583C

 

저기에 SHELL32.dll이 보이시죠? 그리고 Name의 끝은 당연히 문자열이므로 \0(NULL)이 되겠습니다.

 

FirstThunk: OriginalFirstThunk와 마찬가지로 FirstThunk 필드도 구조체 IMAGE_THUNK_DATA의 RVA 값을 지닙니다. PE 파일이 메모리에 로딩되고 나서는 구조체 IMAGE_THUNK_DATA는 Import한 DLL 내의 함수의 실제 주소값을 지니며, 이렇게 함수의 주소값을 담고있는 구조체 IMAGE_THUNK_DATA 배열을 Import Address Table(IAT)라고 합니다.

 

위 그림에서 구조체 배열 첫번째 요소의 FirstThunk 값을 읽으면 00001000(RVA)이고, 이는 1000 - 1000 + 400으로 RAW는 400이 됩니다. 우선 400으로 이동해보도록 합시다.

 

217C2444520A61C505C4AE

 

위 영역은 SHELL32.dll의 IAT 배열 영역으로 INT와 같이 구조체 IMAGE_THUNK_DATA 배열이며, 여기서는 Function 필드에 함수의 주소가 들어갑니다. IMAGE_THUNK_DATA는 총 4바이트니, 앞의 4바이트를 읽어보면 73820468 입니다. 위에서 PE 파일이 메모리에 로딩되기 전에는 AddressOfData로 쓰이거나 하지만, 로딩된 후에는 IMAGE_THUNK_DATA는 임포트한 DLL 내의 함수의 실제 주소값을 지닙니다.

 

IAT에 대한 설명은 여기서 마치고, EAT도 같이 설명하려고 하였으나 내용이 비슷하여 생략하도록 하겠습니다. IAT를 충분히 이해하시고 계시다면 EAT도 별 어려움없이 볼 수 있으실거라 생각합니다.

블로그 이미지

Diano.

http://www.diano.kr / 이사하는중 /

,