환경설정 yum 설치
yum -y install vim ctags gdb wget man man-pages \
gcc make gdb glibc.i686 glibc-devel.i686 \
libgcc.i686 libstdc++.i686 glibc glibc-devel libgcc libstdc++
dnf -y install --enablerepo=powertools glibc-static glibc-static.686
보안 관련 공격코드나 분석툴이 파이썬으로 되어있어 보안에 중요
파이썬.py
for i in range(1, 6):
print(f"{i}. Hello !")
간단한 for 문 출력
C 프로그램의 컴파일 과정
전처리기(gcc -E) 컴파일러(cc1) 어셈블러(as) 링커(ld)
gcc -E gcc -S gcc -c gcc -o
test.c → test.i → test.s → test.o → test
소스파일 전처리파일 어셈블리파일 목적파일 실행파일
define : 전처리 지시어. 매크로 정의에 사용됨.
전처리기(gcc -E)에 의해 define 처리된 문자열을 값으로 변경하여 컴파일에 사용
#define TEST "TEST_STR"
전처리기(gcc -E)
gcc -E test.c -o test.i
=> test.i 를 열어보면
컴파일러(cc1)
어셈블러가 어셈블리 파일로 생성한다
gcc -S test.i -o test.s
gcc -masm=intel -S test.i -o test.s
어셈블리어 문법
1) at&t 문법 (기본값, 생략)
2) intel 문법 : -masm=intel 옵션 사용
아키텍처
- 64bit가 기본값 (옵션 생략)
- 32bit는 -m32옵션을 사용
+ ebp, esp, eax, ebx, ecx 등이 들어가면 32비트이다
+ rbp, rsp, rax, rbx, rcx 등이 들어가면 64비트 문법
intel 문법에는
.intel_syntax noprefix 라인이 들어가있다
64비트는 .LC0: .align 8
32비트는 .LC0: .align 4
어셈블러(as)
gcc -c test.s
=> 실행하면 오브젝트(목적) 파일이 생성된다. test.o
32비트의 경우 -m32 추가
gcc -c test.s
test.s: Assembler messages:
test.s:18: Error: invalid instruction suffix for `push'
test.s:19: Error: invalid instruction suffix for `push'
test.s:22: Error: invalid instruction suffix for `push'
test.s:26: Error: invalid instruction suffix for `push'
test.s:27: Error: invalid instruction suffix for `push'
=> 32비트 test.s파일을 -m32 옵션없이 64비트 오브젝트 파일을 만들려고 하자 에러 발생함
즉, 32비트는 어셈블리 파일도 32비트여야 하고, 64비트 또한 어셈블리 파일이 64비트여야 한다
링커(ld)
gcc -o test test.o
=> test 실행파일 생성됨.
생략되었지만, 링커는 printf.o 파일과 test.o 파일을 결합시켜 최종 실행파일을 생성한다.
또 C라이브러리인 -lc 옵션이 원래 들어가 있는데 기본값이므로 생략되었음.
정적 컴파일 -static
gcc -static -o test_st test.o
=> ldd로 정보 확인불가능(not a dynamic executable). file로 확인하면
test_st: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=23845f3cf143f1177c08889e82ed17d65b9ad1cb, not stripped, too many notes (256)
이렇게 출력되어 확인할 수 있다
정적 컴파일은 파일 내부에 라이브러리를 포함하여, 파일 크기가 커진다. 장점은 외부 공유 라이브러리에 의존할 필요가 없다는 점.
동적 컴파일은 기본값으로 옵션이 생략된다.
( 라이브러리 주소만 가지고 있는 방식 )
크기가 비교적 작다. 다만 실행 시스템에 버전에 맞는 라이브러리가 없으면 실행 불가능
-rwxr-xr-x. 1 root root 18048 5월 11 15:42 test
-rwxr-xr-x. 1 root root 1708088 5월 11 16:02 test_st
=> 동적 컴파일과 정적 컴파일의 파일 크기 차이.
실습) gdb를 이용한 바이너리 분석
gdb란
GNU Debugger로 리눅스에서 사용되는 실행파일, 프로세스 등 분석하는 디버깅 툴.
이 툴을 사용하여 실행파일의 내부구조를 확인할 수 있고
실행파일의 소스코드를 복원할 수 있다
( 향후 사용할 파이썬을 이용한 gdb 확장 프로그램도 gdb를 잘 알아야 도움이 된다 )
gdb의 목적은 프로그램 실행동안 프로그램 내부에서 진행되고 있는 것이 무엇인지 (또는 프로그램이 죽는 순간 무엇을 했는지) 알기 위함이다
지정된 조건에서 멈추도록 하거나
멈췄을 때 무엇이 일어났는지 테스트 하거나
프로그램 시작시 영향을 줄 수 있는 것을 지정하거나 할 수 있다.
참고 사이트 : http://korea.gnu.org/manual/release/gdb/gdb.html
환경설정 패키지
yum or dnf -y install gcc make gdb glibc.i686 glibc-devel.i686 \
libgcc.i686 libstdc++.i686 glibc glibc-devel libgcc libstdc++
gdb를 이용해 바이너리(실행파일)을 분석하려면 컴파일 과정에서 디버깅 정보 -g 옵션을 삽입하여야 한다
ex) gcc -g -o 실행파일 소스파일
(gdb) help
(gdb) help 명령어
(gdb) help all <- 모든 명령어
(gdb) help run
=> 도움말 출력
(gdb) ctrl + d
(gdb) q
(gdb) quit
=> gdb 종료/빠져나가기
r은 프로그램 실행을 의미한다.
화면의 경우, c_Chap01Ex04의 실행파일을 gdb로 실행한 상태.
disassemble 명령어를 사용해서 실행파일을 디스어셈블한다 (disas 로 축약 가능)
실행파일에 포함된 기계어 코드(0,1)를 해석해서 어셈블리 코드로 변환해준다
기본 문법은 at&t 문법을 사용한다.
set disassembly-flavor intel
=> intel 문법으로 변경하려면 이렇게 작성하고 (윈도우에서 주로 사용)
set disassembly-flavor att
=> 다시 기본값 at&t로 돌아오려면 이렇게 사용
명령어의 축약
run -> r
break -> b
continue -> c
disassemble -> disas
quit -> q
Dump of assembler code for function main: warning: Source file is more recent than executable. 10 { 0x080484ad <+0>: lea 0x4(%esp),%ecx 0x080484b1 <+4>: and $0xfffffff0,%esp 0x080484b4 <+7>: pushl -0x4(%ecx) 0x080484b7 <+10>: push %ebp 0x080484b8 <+11>: mov %esp,%ebp 0x080484ba <+13>: push %ecx 0x080484bb <+14>: sub $0x14,%esp 11 int number; // 변수 number 를 선언한다. 12 number = 1; // 변수 number에 값 1을 대입한다. 0x080484be <+17>: movl $0x1,-0xc(%ebp) 13 14 while (number <= 5) // number의 값이 5보다 작거나 같다면 0x080484c5 <+24>: jmp 0x80484de <main+49> 0x080484de <+49>: cmpl $0x5,-0xc(%ebp) 0x080484e2 <+53>: jle 0x80484c7 <main+26> 15 { 16 printf("number = %d\n", number); // number에 저장된 값을 출력한다. 0x080484c7 <+26>: sub $0x8,%esp 0x080484ca <+29>: pushl -0xc(%ebp) 0x080484cd <+32>: push $0x804858c --Type <RET> for more, q to quit, c to continue without paging-- 0x080484d2 <+37>: call 0x8048350 <printf@plt> 0x080484d7 <+42>: add $0x10,%esp 17 number++; 0x080484da <+45>: addl $0x1,-0xc(%ebp) 18 } 19 20 return 0; 0x080484e4 <+55>: mov $0x0,%eax 21 } 0x080484e9 <+60>: mov -0x4(%ebp),%ecx 0x080484ec <+63>: leave 0x080484ed <+64>: lea -0x4(%ecx),%esp 0x080484f0 <+67>: ret End of assembler dump. |
disas /m main
=> 소스코드에 대한 어셈블리 코드 출력 ( 원본 소스코드의 경우, for문을 통해 printf 출력하는 내용 )
아래는 인텔
Dump of assembler code for function main:
10 {
0x080484ad <+0>: lea ecx,[esp+0x4]
0x080484b1 <+4>: and esp,0xfffffff0
0x080484b4 <+7>: push DWORD PTR [ecx-0x4]
0x080484b7 <+10>: push ebp
0x080484b8 <+11>: mov ebp,esp
0x080484ba <+13>: push ecx
0x080484bb <+14>: sub esp,0x14
11 int number; // 변수 number 를 선언한다.
12 number = 1; // 변수 number에 값 1을 대입한다.
0x080484be <+17>: mov DWORD PTR [ebp-0xc],0x1
13
14 while (number <= 5) // number의 값이 5보다 작거나 같다면
0x080484c5 <+24>: jmp 0x80484de <main+49>
0x080484de <+49>: cmp DWORD PTR [ebp-0xc],0x5
0x080484e2 <+53>: jle 0x80484c7 <main+26>
15 {
16 printf("number = %d\n", number); // number에 저장된 값을 출력한다.
0x080484c7 <+26>: sub esp,0x8
0x080484ca <+29>: push DWORD PTR [ebp-0xc]
0x080484cd <+32>: push 0x804858c
--Type <RET> for more, q to quit, c to continue without paging--
0x080484d2 <+37>: call 0x8048350 <printf@plt>
0x080484d7 <+42>: add esp,0x10
17 number++;
0x080484da <+45>: add DWORD PTR [ebp-0xc],0x1
18 }
19
20 return 0;
0x080484e4 <+55>: mov eax,0x0
21 }
0x080484e9 <+60>: mov ecx,DWORD PTR [ebp-0x4]
0x080484ec <+63>: leave
0x080484ed <+64>: lea esp,[ecx-0x4]
0x080484f0 <+67>: ret
End of assembler dump.
Dump of assembler code for function main: 0x080484ad <+0>: 8d 4c 24 04 lea 0x4(%esp),%ecx 0x080484b1 <+4>: 83 e4 f0 and $0xfffffff0,%esp 0x080484b4 <+7>: ff 71 fc pushl -0x4(%ecx) 0x080484b7 <+10>: 55 push %ebp 0x080484b8 <+11>: 89 e5 mov %esp,%ebp 0x080484ba <+13>: 51 push %ecx 0x080484bb <+14>: 83 ec 14 sub $0x14,%esp 0x080484be <+17>: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%ebp) 0x080484c5 <+24>: eb 17 jmp 0x80484de <main+49> 0x080484c7 <+26>: 83 ec 08 sub $0x8,%esp 0x080484ca <+29>: ff 75 f4 pushl -0xc(%ebp) 0x080484cd <+32>: 68 8c 85 04 08 push $0x804858c 0x080484d2 <+37>: e8 79 fe ff ff call 0x8048350 <printf@plt> 0x080484d7 <+42>: 83 c4 10 add $0x10,%esp 0x080484da <+45>: 83 45 f4 01 addl $0x1,-0xc(%ebp) 0x080484de <+49>: 83 7d f4 05 cmpl $0x5,-0xc(%ebp) 0x080484e2 <+53>: 7e e3 jle 0x80484c7 <main+26> 0x080484e4 <+55>: b8 00 00 00 00 mov $0x0,%eax 0x080484e9 <+60>: 8b 4d fc mov -0x4(%ebp),%ecx 0x080484ec <+63>: c9 leave 0x080484ed <+64>: 8d 61 fc lea -0x4(%ecx),%esp 0x080484f0 <+67>: c3 ret End of assembler dump. |
disas /r main
=> 소스코드에 대한 기계어 코드를 출력
아래는 인텔
Dump of assembler code for function main:
0x080484ad <+0>: 8d 4c 24 04 lea ecx,[esp+0x4]
0x080484b1 <+4>: 83 e4 f0 and esp,0xfffffff0
0x080484b4 <+7>: ff 71 fc push DWORD PTR [ecx-0x4]
0x080484b7 <+10>: 55 push ebp
0x080484b8 <+11>: 89 e5 mov ebp,esp
0x080484ba <+13>: 51 push ecx
0x080484bb <+14>: 83 ec 14 sub esp,0x14
0x080484be <+17>: c7 45 f4 01 00 00 00 mov DWORD PTR [ebp-0xc],0x1
0x080484c5 <+24>: eb 17 jmp 0x80484de <main+49>
0x080484c7 <+26>: 83 ec 08 sub esp,0x8
0x080484ca <+29>: ff 75 f4 push DWORD PTR [ebp-0xc]
0x080484cd <+32>: 68 8c 85 04 08 push 0x804858c
0x080484d2 <+37>: e8 79 fe ff ff call 0x8048350 <printf@plt>
0x080484d7 <+42>: 83 c4 10 add esp,0x10
0x080484da <+45>: 83 45 f4 01 add DWORD PTR [ebp-0xc],0x1
0x080484de <+49>: 83 7d f4 05 cmp DWORD PTR [ebp-0xc],0x5
0x080484e2 <+53>: 7e e3 jle 0x80484c7 <main+26>
0x080484e4 <+55>: b8 00 00 00 00 mov eax,0x0
0x080484e9 <+60>: 8b 4d fc mov ecx,DWORD PTR [ebp-0x4]
0x080484ec <+63>: c9 leave
0x080484ed <+64>: 8d 61 fc lea esp,[ecx-0x4]
0x080484f0 <+67>: c3 ret
End of assembler dump.
breakpoint
=> 프로그램을 멈추는 포인트. help b 로 상세 내용을 볼 수 있음.
b main
Breakpoint 1 at 0x80484be: file c_Chap01Ex04.c, line 12.
b main
r
i b
n
n
p number
n
n
c
=> main 소스코드에 대한 브레이크 포인트 지정. r로 run 실행. i b로 브레이크 포인트 정보 확인.
n은 next로, 다음 코드로 진행을 뜻함.
p는 print로, number값을 출력하였음.
c는 continue로 나머지 프로그램을 마저 전부 진행함.
+)
n은 run. 프로그램 실행.
set : 매개변수나 환경설정을 셋팅할 때 사용
show : 수많은 매개변수와 환경설정의 현재 설정을 표시해준다
l은 list이며 현재 코드의 소스 코드를 출력해준다.
watch : 지정된 변수에 변화가 생기면 실행 중지
info : 다양한 유형의 정보 출력 ex) into locals, info b
bt : 현재 실행하고 있는 스택 출력(함수 호출 스택)
examine (x) : 메모리 주소 검사
자신의 홈 디렉터리에 .gdbinit 파일을 만들고, 그 안에 설정을 넣으면 gdb를 실행할 때마다 적용한다.
ex)
set disassembly-flavor intel
=> gdb를 실행할 때마다 intel 문법으로 셋팅한다.
(gdb) disas main
Dump of assembler code for function main:
=> 0x080484ad <+0>: lea ecx,[esp+0x4]
하략.
↑처음 실행했을 때 disas main 결과
(gdb) disas main
Dump of assembler code for function main:
0x080484ad <+0>: lea ecx,[esp+0x4]
0x080484b1 <+4>: and esp,0xfffffff0
0x080484b4 <+7>: push DWORD PTR [ecx-0x4]
0x080484b7 <+10>: push ebp
0x080484b8 <+11>: mov ebp,esp
0x080484ba <+13>: push ecx
0x080484bb <+14>: sub esp,0x14
0x080484be <+17>: mov DWORD PTR [ebp-0xc],0x1
0x080484c5 <+24>: jmp 0x80484de <main+49>
=> 0x080484c7 <+26>: sub esp,0x8
하략.
↑ n으로 몇줄 더 실행하고 나서 disas main 결과
=> 화살표로 현재 위치를 표시해주고 있다
(gdb) p number
$1 = 134513951
(gdb) n
14 while (number <= 5) // number의 값이 5보다 작거나 같다면
(gdb) p number
$2 = 1
=> number에 쓰레기값이 들어가 있다가 1로 초기화되는 것 확인
(gdb) p &number
$3 = (int *) 0xffffd42c
=> number의 주소값도 확인 가능
(gdb) x/xw &number
0xffffd42c: 0x00000001
(gdb) x/2xw &number
0xffffd42c: 0x00000001 0xf7fd14d0
(gdb) x/3xw &number
0xffffd42c: 0x00000001 0xf7fd14d0 0xffffd450
(gdb) x/4xw &number
0xffffd42c: 0x00000001 0xf7fd14d0 0xffffd450 0x00000000
=> x로 메모리 조사. x는 hex(16진수)형태이고, w는 word(4byte 단위)를 의미한다.
2wx, 3xw, 4xw는 반복숫자를 넣은 것이다. &number의 시작 주소에서 word 4byte만큼 2번, 3번, 4번 보여준다는 뜻.
형식 : x/[반복숫자][출력형태][크기] [메모리주소]
출력형태
o(octal), x(hex), d(decimal), u(unsigned decimal), t(binary), f(float), a(address), i(instruction), c(char) and s(string)
크기
b(byte), h(halfword, 2 bytes), w(word, 4 bytes), g(giant, 8 bytes)
결과값을 살펴보면
0xffffd42c: 0x00000001
└ 0xffffd42c
0xffffd42c: 0x00000001
└ 0xffffd42d
0xffffd42c: 0x00000001
└ 0xffffd42e
0xffffd42c: 0x00000001
└ 0xffffd42f
주소값이 이렇게 책정된다 16진수 0 ~ f 까지의 값을 생각하면 됨.
두번째 값을 살펴보면
0xffffd42c: 0x00000001 0xf7fd14d0
└ 0xffffd430
0xffffd42c: 0x00000001 0xf7fd14d0
└ 0xffffd431
0xffffd42c: 0x00000001 0xf7fd14d0
└ 0xffffd432
0xffffd42c: 0x00000001 0xf7fd14d0
└ 0xffffd433
이렇게 뒤에서부터 주소값이 책정된다.
(gdb) x/xb &number
0xffffd42c: 0x01
(gdb) x/2xb &number
0xffffd42c: 0x01 0x00
=> b 바이트단위로 보여준다
(gdb) x/16xw $ebp
0xffffd438: 0x00000000 0xf7e34f36 0x00000000 0x08048370
0xffffd448: 0x00000000 0xf7e34f36 0x00000001 0xffffd4d4
0xffffd458: 0xffffd4dc 0xffffd474 0x0804a010 0x00000008
0xffffd468: 0xffffd4dc 0xf7fbc000 0xf7e2475c 0xf7fbc000
(gdb) x/16xw $ebp-16
0xffffd428: 0xffffd4dc 0x00000001 0xf7fd14d0 0xffffd450
0xffffd438: 0x00000000 0xf7e34f36 0x00000000 0x08048370
0xffffd448: 0x00000000 0xf7e34f36 0x00000001 0xffffd4d4
0xffffd458: 0xffffd4dc 0xffffd474 0x0804a010 0x00000008
=> w word 단위로 보여주는데, 16개를 보여준다.
cpu에 대한 메모리 공간(레지스터), 그 중에 ebp는 스택의 바닥을 가리킨다.
스택의 바닥에서부터 16개만큼의 공간을 보여달라는 의미이다.
-16은 16개 이전이라는 뜻.
최상단 : esp 레지스터
레지스터 : cpu 안에 들어있는 고속의 메모리 공간.
(gdb) display number
1: number = 134513951
=> number 변수를 모니터링 display 한다.
(gdb) n
16 printf("number = %d\n", number); // number에 저장된 값을 출력한다.
1: number = 2
(gdb) n
number = 2
17 number++;
1: number = 2
=> n (next) 할 때마다 number가 같이 출력된다.
축약어는 disp