The Way
어셈블리 파일과 바이너리 파일 수정 본문
컴파일을 하기 위해서는 보통 gcc를 사용한다.
gcc의 컴파일 과정은 다음과 같은 순서로 이루어지는데,
a.c (작성한 코드)
→ a.i (#define이나 #include 같은 것을 처리함)
→ a.s (어셈블리어로 번역)
→ a.o (어셈블리 파일을 오브젝트 파일로 번역)
→ a.out (오브젝트 파일을 linking까지 완료)
중간 단계까지만 진행하는 것도 가능한데,
-E 옵션을 주면 전처리만, -S 옵션을 주면 어셈블리어로, -c 옵션을 주면 오브젝트 파일까지만 완료할 수 있다.
1) 어셈블리어
먼저 어셈블리어로 번역되는 과정에 주목해보자. 어셈블리어는 파고들자만 한도끝도 없을 정도로 복잡하지만, 대표적으로 다음과 같은 명령들은 알아둘 만 하다.
mov src des
: src를 des로 복사
add, sub, imul, sal, sar, shr, xor, and, or
: add src des의 형식으로 구성되어있는데, des += src를 뜻한다. 나머지도 모두 형식은 동일.
각각 더하기, 빼기, 곱하기, <<(shift arithmatic left인 듯), >>, >>(logical shift), ^, &, |를 뜻한다.
cmp src2 src1
: src1 - src2를 한 뒤 그 결과대로 condition code를 설정함.
jmp je, jne 등
: 분기 명령어. jne는 jump not equal의 약자.
명령어는 add처럼 안 써있고 addl처럼 뒤에 글자가 더 붙어있는데 이는 비트 수를 나타낸다. addl은 32비트, addq는 64비트 이런 식이다.
이게 두 강의에 걸친 내용이라 내용이 너무 많다. 관심있는 사람은 어셈블리어를 따로 공부해보자.
중요한 점은 어셈블리어는 C 코드와 1대 1로 대응되지 않는다는 점이다. 같은 동작을 하는 코드라도 얼마든지 다르게 쓸 수 있기 때문.
이 때문에 나중에 해킹이라도 해보려는 생각이 있는 사람이면 어셈블리어만 보고 코드의 동작을 유추할 수 있는 훈련이 필요하겠다.
컴파일시 -g 옵션을 주면 코드 같은 정보도 그대로 남아있기 때문에 코드도 다시 복구해낼 수 있다.
2) 바이너리 파일을 어셈블리 파일로
a.c에 다음과 같이 코드를 작성한 뒤 컴파일을 해보자.
코드의 의미는 n을 2배한 뒤 t를 더해 return하는 아주 간단한 코드이다.
당연히 해당 코드의 실행 결과는 8이 될 것이다.
> gcc -g -o a a.c
이제 a라는 이름의 실행 파일이 생겼을 것이다.
기계어는 어셈블리어랑 1대1로 대응되므로 어셈블리어로 복구할 수 있다.
objdump라는 툴을 이용하면 간편하다.
> objdump -S a
이제 어셈블리 코드가 보일 것이다. 처음 컴파일 할 때 -g 옵션을 주었기 때문에 중간중간 해당 부분의 코드도 나오는 것을 확인할 수 있다.
간단히 어셈블리어를 따라가보면, n을 eax로 복사하고, 2배하여 edx에 저장하고, t를 eax로 복사하고, eax += edx를 하며 끝나는 것을 확인할 수 있다.
3) 바이너리 파일에 조작 가하기
이번에는 2번에서 추출한 어셈블리 파일을 바탕으로, 바이너리 파일에 조작을 가해볼 것이다.
목표는 원래 2 * n + t를 return했던 doubleAndadd 함수를 3 * n - t를 return하도록 바꾸는 것이다.
바이너리 파일을 수정하기 위해서는 hexeditor를 사용해야 하는데, hexedit라는 좋은 프로그램이 있다. 설치하자.
> sudo apt install hexedit
그 다음 hexedit으로 우리가 만든 프로그램 a를 켜보자.
> hexedit a
뭔가 숫자들이 많이 보일텐데, 함수 부분으로 빠르게 넘어가자.
어셈블리 코드를 보면 doubleAndadd 함수의 주소는 64a부터 시작이므로, 갯수를 잘 센 뒤 숫자들을 잘 비교해보면 밑줄 친 부분이 doubleAndadd 함수의 코드임을 알 수 있다.
함수의 코드 바이트 수가 달라진다면 다음 함수의 메모리 주소가 밀리기 때문에, call 등 전체적으로 수정을 많이 해야 한다.
따라서 명령어를 최대한 유지하는 선에서 바꾸는 것이 편할 것이다.
다시 한 번 어셈블리를 차근차근 살펴보자.
마지막에 t를 메모리에서 eax로 옮긴 뒤, 계산해서 edx에 놔두었던 2 * n을 더해준다.
단순히 마지막 줄을 sub로 바꾼다면 t - 2 * n이 된다. 우리는 3 * n - t가 되기를 원하므로 조금 많은 수정이 필요할 것 같다.
mov -0x4(%rbp),%eax
lea (%rax,%rax,1),%edx
mov -0x8(%rbp),%eax
add %edx,%eax
를 레지스터와 명령어를 요리조리 바꾸어보자.
mov -0x4(%rbp),%edx
lea (%rdx,%rdx,1),%eax
mov -0x8(%rbp),%edx
sub %edx,%eax
여기에 3 * n이 되어야 하므로 lea까지 수정하자.
mov -0x4(%rbp),%edx
lea (%rdx,%rdx,2),%eax
mov -0x8(%rbp),%edx
sub %edx,%eax
이제 완성이다. 이제 수정을 해보자. 수정을 하려면 바뀐 어셈블리어를 기계어로 알아야 한다. assembler를 이용하자.
https://defuse.ca/online-x86-assembler.htm#disassembly
각각에 해당하는 기계어는 다음과 같음을 알 수 있다.
mov -0x4(%rbp),%edx // 8b 55 fc
lea (%rdx,%rdx,2),%eax // 8d 04 52
mov -0x8(%rbp),%edx // 8b 55 f8
sub %edx,%eax // 29 d0
열심히 수정을 하자. 수정된 부분은 굵은 글씨로 표시된다.
수정하고 ctrl-x를 누르면 저장된다.
프로그램을 실행해보면 수정 전의 프로그램은 8을 출력했지만, 수정 후에 프로그램을 실행해보면 7이 나온다. 성공적으로 해킹을 마쳤음을 확인할 수 있다.
'공부 > 운영체제 실습' 카테고리의 다른 글
비트 연산 trick (0) | 2019.04.03 |
---|