profile image

L o a d i n g . . .

ELF 동적 분석

바이너리를 분석할 때, 바이너리가 실행되며 변화하는 상태를 관찰하기 위해 동적 디버깅이 필요한 경우가 있다.

때문에 리눅스의 실행 파일인 ELF 파일을 동적으로 디버깅하는 방법에 대해 알아보도록 하겠다.

 

여기선 가장 유명한 ELF 디버거인 gdb(GNU Debugger)를 사용하여 ELF 바이너리를 동적 디버깅하는 방법에 대해 알아보겠다.

 

gdb의 디스어셈블리 문법에는 AT&T intel 두 종류가 있다. 이 중 널리 쓰이는 디스어셈블리 문법은 intel 이다. gdb 기본 설정에서의 디스어셈블리 문법은 AT&T이기 때문에, 디버깅 실습을 하기 전에 gdb의 디스어셈블리 문법을 intel로 바꾸어 주어야 한다.

 

.gdbinit gdb를 시작할 때 자동적으로 실행할 gdb 명령어들을 저장하고 있는 파일이다.

다음 명령어를 통해 .gdbinit에 gdb의 디스어셈블리 문법을 intel로 바꾸어 주는 명령어인 set disassembly-flavor intel을 추가할 수 있다.

 

.gdbinit 파일 수정

$ echo "set disassembly-flavor intel" >> ~/.gdbinit
$

 

disassembly-flavor 설정 확인

$ gdb -q
(gdb) show disassembly-flavor 
The disassembly flavor is "intel".
(gdb) 

 

 

ELF 동적 분석 실습

// gcc -o example0 example0.c -m32
#include <stdio.h>
int main(void){
  int sum = 0;
  int val1 = 1;
  int val2 = 2;
  sum = val1 + val2;
  printf("1 + 2 = %d\n", sum);
  return 0;
}

example0.c는 1과 2의 덧셈 결과를 출력하는 예제이다.

다음은 example0을 gdb의 인자로 전달한 것이다.

$ gdb ./example0

 

gdb에서 함수의 디스어셈블리 결과를 출력해주는 명령어는 disassemble, 혹은 disas이다.

(gdb) disas main
Dump of assembler code for function main:
   0x0804840b <+00>:	push   ebp
   0x0804840c <+01>:	mov    ebp,esp
   0x0804840e <+03>:	sub    esp,0xc
   0x08048411 <+06>:	mov    DWORD PTR [ebp-0x4],0x0
   0x08048418 <+13>:	mov    DWORD PTR [ebp-0x8],0x1
   0x0804841f <+20>:	mov    DWORD PTR [ebp-0xc],0x2
   0x08048426 <+27>:	mov    edx,DWORD PTR [ebp-0x8]
   0x08048429 <+30>:	mov    eax,DWORD PTR [ebp-0xc]
   0x0804842c <+33>:	add    eax,edx
   0x0804842e <+35>:	mov    DWORD PTR [ebp-0x4],eax
   0x08048431 <+38>:	push   DWORD PTR [ebp-0x4]
   0x08048434 <+41>:	push   0x80484d0
   0x08048439 <+46>:	call   0x80482e0 <printf@plt>
   0x0804843e <+51>:	add    esp,0x8
   0x08048441 <+54>:	mov    eax,0x0
   0x08048446 <+59>:	leave  
   0x08048447 <+60>:	ret    
End of assembler dump.
(gdb) 

 


 

0x804842e 주소에 브레이크포인트를 설정해 val1 + val2의 결과값이 저장된 eax 레지스터의 값을 살펴보겠다.

gdb에서 브레이크포인트를 설정하는 명령어는 break 혹은 b이다.

(gdb) b *0x804842e
Breakpoint 1 at 0x804842e
(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x0804842e <main+35>
(gdb) 

info를 사용해 브레이크포인트가 정상적으로 설정된 것을 확인할 수 있다.

 

프로세스를 실행시켜 주는 명령어인 run (r)을 이용해 브레이크포인트가 설정된 지점까지 example0을 실행시켜 보겠다.

(gdb) r
Starting program: ~/example0 
Breakpoint 1, 0x0804842e in main ()
(gdb) info reg
eax            0x3	3
ecx            0x97b9e553	-1749424813
edx            0x1	1
ebx            0x0	0
esp            0xffffd5dc	0xffffd5dc
ebp            0xffffd5e8	0xffffd5e8
esi            0xf7fb2000	-134537216
edi            0xf7fb2000	-134537216
eip            0x804842e	0x804842e <main+35>
eflags         0x206	[ PF IF ]
cs             0x23	35
ss             0x2b	43
ds             0x2b	43
es             0x2b	43
fs             0x0	0
gs             0x63	99
(gdb) 

eip 레지스터가 0x804842e인 것으로 보아 브레이크포인트를 설정했던 위치에서 실행이 멈춘 것을 확인할 수 있다.

 

print (p) 명령어를 이용해 val1 + val2의 결과가 저장되어 있는 eax 레지스터의 값을 출력해보겠다.

(gdb) p $eax
$1 = 3
(gdb) 

eax 레지스터에 3이 저장되어 있는 것을 볼 수 있다.

 


 

0x8048439 주소에 브레이크포인트를 설정해 printf 함수의 인자들을 살펴보도록 하겠다.

프로세스가 멈추어있는 상태에서 프로세스를 이어서 실행시켜 주는 명령어는 continue (c)이다. 만약 브레이크포인트가 설정되어 있다면 다음 브레이크포인트 지점까지 프로세스를 실행시킨다.

(gdb) b*0x8048439
Breakpoint 2 at 0x8048439
(gdb) c
Continuing.
Breakpoint 2, 0x08048439 in main ()
(gdb) 

 

x 명령어를 사용하면 인자로 주어진 주소의 메모리를 볼 수 있다.

x 명령어는 또한 출력 타입을 지정해줄 수 있는데, 우선 esp 레지스터의 메모리를 word 타입으로 2개만큼(x/2wx) 출력해보도록 하겠다.

(gdb) x/2wx $esp
0xffffd544:	0x080484d0	0x00000003

x86 아키텍처의 호출 규약에 의해, printf 함수가 호출되는 시점의 스택 메모리에 함수의 인자들이 순서대로 저장되어 있는 것을 볼 수 있다.

 

첫 번째 인자인 1 + 2 = %d\n 문자열의 주소 0x80484d0을 문자열 형태로(x/s) 출력해보겠다.

(gdb) x/s 0x080484d0
0x80484d0:	"1 + 2 = %d\n"
(gdb) 

 

마지막으로 함수의 다음 인스트럭션까지 실행해주는 nexti (ni) 명령어를 이용해 printf 함수를 실행한 것이다.

(gdb) x/i $eip
=> 0x8048439 <main+46>:	call   0x80482e0 <printf@plt>
(gdb) ni
1 + 2 = 3
0x0804843e in main ()
(gdb) x/i $eip
=> 0x804843e <main+51>:	add    esp,0x8
(gdb) 

printf 함수가 실행되어 "1 + 2 = 3" 문자열이 출력되었다.

 

 

Process Attach

gdb를 이용하면 실행중인 프로세스를 디버깅할 수 있다. 

//gcc -o read_write read_write.c -m32
//read_write.c
#include <stdio.h>
int main(void){
  char buf[64] = {};
  printf("Input : ");
  scanf("%63s", buf);
  printf("Your input : %s", buf);
  
}

read_write는 사용자의 입력을 받아 문자열을 출력해주는 프로그램이다.

 

gdb에 -p PID 혹은 --pid=PID를 인자로 전달하면 PID에 해당하는 프로세스에 gdb를 attach할 수 있다.

 

우선, read_write를 실행하여 실행 중인 프로세스의 PID를 알아보겠다.

실행 중인 프로세스의 PID를 확인하는 첫 번째 방법은 ps 프로그램을 이용하는 방법이다.

$ ./read_write
Input : 

read_write 바이너리를 실행한 후, 아래와 같이 read_write 프로세스의 PID(50353)를 구할 수 있다.

$ ps -aux | grep read_write
theori    50353  0.0  0.0   2204   520 pts/28   S+   21:50   0:00 ./read_write

 

두 번째 방법은 pidof나 pgrep 프로그램을 사용하여 프로세스의 PID를 구하는 것이다.

pidof나 pgrep의 인자로 바이너리 이름을 전달하면 아래와 같이 해당 바이너리의 PID를 구할 수 있다.

$ pidof read_write
50353
$ pgrep read_write
50353

 


 

구한 PID를 이용해 gdb 프로세스 디버깅을 해보도록 하겠다.

$ ./read_write
Input : 

프로세스가 사용자의 입력을 받기 위해 실행 대기중일 때, 아래와 같이 gdb를 프로세스에 attach시킨다.

# gdb -q -p 50353
Attaching to process 50353
Reading symbols from /Linux_Exploitation_Mitigation/read_write...(no debugging symbols found)...done.
Reading symbols from /lib/i386-linux-gnu/libc.so.6...(no debugging symbols found)...done.
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
0xf7f8ffd9 in __kernel_vsyscall ()
...
(gdb)

 

 

scanf 함수가 호출된 이후인 0x804850a 주소에 브레이크포인트를 설정해 입력한 데이터를 메모리에서 확인해보도록 하겠다.

(gdb) b *0x0804850a
Breakpoint 1 at 0x804850a
(gdb) c
Continuing.
$ ./read_write
Input : abcd1234
Breakpoint 1, 0x0804850a in main ()
(gdb) x/4wx $ebp-0x4c
0xffaf0acc:	0x64636261	0x34333231	0x00000000	0x00000000
(gdb) 

입력한 문자열 "abcd1234"가 buf의 주소인 0xffaf0acc 주소에 저장되어 있는 것을 확인할 수 있다.

 

 

참고
Dreamhack - Linux Exploitation & Mitigation Part 1
복사했습니다!