[리버싱] BombLab phase_6 분석하기
이 포스팅을 작성한 달숏달숏은 어셈블리언어를 처음 배운 사람이며, 리버싱이란 것도 이번 BombLab을 통해서 처음 접한 사람입니다.
저도 배우는 학생의 입장인지라 틀린 정보가 섞여있을 수 있으니 너그러이 양해해주시고, 틀린 점이 있는 경우 편하게 피드백해주시면 감사하겠습니다 :)
BombLab은 phase1부터 phase6까지 6개의 문제가 있으며, 총 6개의 암호를 입력받아 다음 단계로 진행된다. 중간에 틀린 암호를 입력하면 "BOOM!!!"메시지와 함께 종료된다.
- 바이너리의 역어셈블을 통해 기계수준 프로그램의 구조 이해
- 일반적인 디버거 사용법과 리버싱 체험
- 6단계의 암호 입력 과정을 분석하여 바이너리 폭탄 해제
가 BombLab과제의 목표라고 할 수 있겠다.
- Phase_6 : linked list, 포인터, struct 자료구조
phase_6를 disas한다
개인적으로... phase_5에서 에너지 소모를 많이 했는데 갑자기 코드가 확 길어진 것이 달갑지는 않다 ㅋㅋ; (살려줘..)
이번 포스팅은 좀 길다. 이거 쓰는데에 3일 걸렸다.(대충 각오하라는 의미)
코드를 잘 읽어보면 <+32>에 read_six_numbers가 보인다. 입력값이 6개의 정수라는 의미인 것 같다.
코드를 쭉 읽으면서 내려와보자. <+32>에서 read_six_numbers가 호출되고 나면, <+43>에서 jump문을 만나게 되는데, unconditional jump이므로 <+45>의 explode_bomb은 생각할 필요가 없이 <+82>로 이동된다.
<+82>에서 mov와 sub를 시행한 후, <+92>에서 cmp로 값을 비교한 다음 <+95>에서 ja문을 만나는데, 잠깐 ja문에 대해서 짚고 넘어가겠다.
- ja, jb, je 명령어
cmp b,a 가 있을 때
- ja : a > b 인 경우 jump (unsigned계열, 부호가 없을 때)
- jb : a < b 인 경우 jump (unsigned계열, 부호가 없을 때)
- je : a = b 인 경우 jump
즉, $eax > $0x5가 될 경우에 jump가 실행되어 <+45>의 explode_bomb이 실행된다는 의미이므로, $eax가 5보다 작거나 같아야 한다.
그럼 ja문이 존재하는 <+95>에서의 $eax를 알아야 하므로 <+95>에 breapoint를 걸고 프로그램을 실행해본다.
phase_6에 넣을 암호를 임의로 1 2 3 4 5 6 으로 정하고 입력해보았다.
breakpoint에 걸린 것을 확인할 수 있다. (breakpoint 1은 phase_6, breakpoint 2는 explode_bomb, breakpoint 3이 phase_6+95 이다.)
이 상태에서 $eax의 값을 확인해보면
0이라는 결과가 나온다.<+95>시점에서 $eax가 0이기 때문에 $eax < 0x5 이므로 ja문이 실행되지 않을 것이다.
그러면 왜 $eax는 0인 것일까?
<+92>의 cmp가 등장하기 전에, <+89>의 sub를 보면, $eax-1 된 결과가 0이라는 것을 알 수 있다. 그럼 혹시 맨 처음에 입력했던 1 2 3 4 5 6 중 1에서 -1이 되었기 때문에 0이 된 것은 아닐까 하는 생각을 해볼 수 있다.
그래서 6 2 3 4 5 6 을 입력해보았다. 만약 내 추측이 맞다면 <+92>에서 $eax는 5가 될 것이고, $eax <= 5이기 때문에 ja가 실행되지 않을 것이기 때문이다.
<+105>부분을 보면 je문이 있고, <+101>에서 $r13d와 6을 비교하고 있는 것을 볼 수 있다. 그런데 $r13d는 <+37>에서 mov를 통해 0이 들어간 뒤부터 <+97>에 도달하기 전까지 값을 변화시킬만한 instruction이 존재하지 않는다. 즉, <+97>시점까지의 $r13d는 0일 것이며 레지스터를 확인한 결과도 이와 동일했다.
그렇다는 것은, <+97>에서 $r13d는 1이 되고, <+101>에서 0x6과 비교를 하게 되겠지만 무조건 0x6과 다를 수 밖에 없으므로 <+105>의 je문은 실행되지 않는다.
<+107>이 실행되면 %ebx에는 %r13d에 담겨있던 1이 담기게 되며 그 상태로 <+110>에서 <+60>으로 jump하게 된다.
<+60>에는 낯선 movsql 명령어가 존재한다.
- movsql 명령어
32bit value를 64bit destination으로 sign-extend 하며 move하는 명령어이다.
sign-extension이라 함은 간단하게 말하자면, 부호 확장을 할 때 새로 생겨난 빈 bit를 무엇으로 채울것이냐 라고 했을 때 기존 수의 부호에 따라 음수였을 경우 1로 채우고 양수였을 경우 0으로 채우는 것을 의미한다.
<+60>의 movesql %ebx,%rax 에서, %rax는 8byte지만 %ebx는 4byte이다. 4byte를 8byte로 옮기는 것이기 때문에 상위 4byte의 공백이 생긴다. 이 공백을 어떤 bit로 채울까의 문제인데, 우리는 어차피 양수를 넣었기 때문에 공백을 전부 0으로 채우게 되며 결론적으로 %rax의 값과 %ebx의 값이 같아지게 된다.
따라서 <+60>에서 %rax에는 0x1이 담기게 된다.
<+63>에서 (%rsp,%rax,4)을 %eax에 담는다.
<+66>에서 $eax와 $rbp를 비교해서 그 값이 같지 않으면 <+69>에서 jump를 하게 된다.
값이 같으면, <+71>의 explode_bomb을 호출하게 되므로 값이 같으면 안된다.
%rbp에 대한 정보가 나와있는 부분이 어딘지 확인해보았을 때, read_six_number을 실행한 이후 만나는 <+43>의 jmp를 실행하고 나면 <+82>에 %r12의 값을 담는 부분이 있다.
그 이후로는 $rbp의 값을 변화시키는 instruction이 없기 때문에 <+82>에서 담긴 값이 유지가 된다.
그래서 $rbp가 무엇인지 확인해보았다.
<+63>시점에서 연산을 수행하기 전 $rsp가 무엇인지 확인해 보았다.
$rsp도 1이다. 그런데, %rbp와 %rsp에 담긴 메모리 주소가 같은 값을 가리키고 있는 것을 확인할 수 있다. 이 메모리 주소는 도대체 어디일까?
바로, 첫번 째 입력한 정수가 담겨있는 위치 값이다. 6 2 3 4 5 6 을 입력하면, 동일한 위치에 6이 담기기 때문이다.
정리해보면
<+63>에서 (%rsp,%rax,4)의 값을 $eax에 담게 되는데,
$rsp는 맨 처음에 입력한 정수 값이고, $rax에는 항상 0x1이 담긴다(위에서 설명)
그러면 %rsp + %rax*4는 결국 %rsp에 담긴 주소값에서 0x4만큼 이동한 값인데, int 1개의 크기가 0x4에 해당하므로 두 번째로 입력한 정수를 가리키게 된다.
즉, <+63>에서는 (두번째로 입력한 정수의 값)을 $eax에 담는 것이다.
이 상태로 <+66>에 도달하면 $eax값과 $rbp를 비교하게 되는데 $rbp에는 맨 처음에 입력한 정수의 값이 담겨있다.
즉, <+66>에서는 (두 번째로 입력한 정수의 값)과 (맨 처음에 입력한 정수의 값) 을 비교하고 있는 셈이다.
따라서 <+69>의 jne문에서 jump가 일어나게 하려면 첫 번째로 입력한 정수의 값과 두 번째로 입력한 정수의 값이 달라야 한다. 나는 1 2 3 4 5 6 을 입력했으므로 1과 2는 서로 다르기 때문에 jne문에서 jump가 일어난다.
jne에서 jump하고 나면 <+52>로 이동되는데, 이 부분을 잘 살펴보면 <+52>부터 <+69>까지는 loop인 것을 알 수 있고 loop가 돌게 하는 값은 $ebx의 값이다.
또한 <+60>부터 <+69>까지는 바로 위에서 보았던 부분이다. 바로 위에서는 암호로 첫번째로 입력한 정수와 두 번째로 입력한 정수가 같은지 체크하는 용도로 사용되었다.
$ebx는 원래 0x1인 상태였고, <+52>에서 $ebx에 1을 더했으니 0x2가 된다. <+60>이 실행되면 $rax는 0x2가 된다. 이렇게 되면 (%rsp,%rax,4)는 첫번째 로 입력한 값에서 0x8만큼 이동하게 되므로 세 번째로 입력한 정수를 가리키게 된다. 그 정수 값이 <+66>에서 $eax에 담긴다. 그리고 <+69>에서 $eax와 $rbp를 비교하는 것이다. 즉, (세 번째로 입력한 정수의 값)과 (첫 번째로 입력한 정수의 값)을 비교하게 된다.
그렇다면 이 loop는 이렇게 해석할 수 있다:
$ebx가 2일 때부터 5일 때까지 4번의 loop를 돌면서, 첫 번째로 입력한 정수의 값과 세 번째~여섯번째 입력한 정수의 값이 같은지 비교하는 것이다. 이 때 두 값이 같을 경우 explode_bomb을 호출한다.
잠시 <+58>에 나온 jg명령어에 대해서 짚고 넘어가겠다.
- jg, jl 명령어
- jg : a > b 인 경우 jump (signed계열, 부호가 있을 때)
- jl : a < b 인 경우 jump (signed계열, 부호가 있을 때)
$ebx의 값이 6이 되고, 0x5보다 커지게 되면 <+58>의 jg명령어에 의해 loop를 빠져나오며 <+78>로 jump하게 된다.
이 뒤부터가 좀 대환장파티이다.
정신을 똑바로 차려야한다 .. ..
여기서 잠깐 헷갈릴 수 있는 개념을 짚고 넘어가려고 한다.
때로는 너무 당연해서 제대로 설명해주지 않는 개념들이 정말 많다. 이런 근소한 개념을 정확히 알아야 공부에 어려움이 없다고 생각한다.
- (%레지스터)와 %(레지스터)
'()'는 de-reference를 의미한다. %rbx일 때는 %rbx에 %rax에 있는 값을 넣는 것이지만, (%rbx)인 경우에는 %rbx에 %rax의 값을 넣는 것이 아니라 %rbx가 가리키고 있는 메모리 주소에 %rax의 값을 넣는 것이다. 다음의 예시를 보자.
0x120에 담긴 123을 %rax에 옮기고 싶을 때, 둘 중 어느 것을 사용해야 할까?
- mov %rdi,%rax
- mov (%rdi),%rax
답은 2이다. 123은 %rdi 자체에 담긴 값이 아니라, %rdi에 담겨있는 메모리 주소인 0x120에 담긴 값이기 때문이다.
다시 원래 하던 얘기로 돌아와보자.
음.. 지금 상황은 <+58>의 jg명령어에 의해 loop를 빠져나와 <+78>에 도달한 상태이다.
<+78>의 add를 주목해보자.
일단 %r12에 무엇이 들어있는지 먼저 보여주겠다.
그럼 add $0x4,%r12 의 결과는 무엇일까?
이 부분의 개념을 잡기 위해서 위의 설명을 굳이 했다.
답은 5가 아니기 때문이다😂 왜 설명을 했냐면, 나는 초짜라서 5라고 생각했다ㅋㅋ 근데 5가 아니다. $r12가 1이 아니다. 엄밀히 따지면 %r12안에 있는 주소가 가리키는 메모리에 들어있는 값이 1인 것이다.
위의 instruction을 실행하면, 마치 %r12가 가지고 있는 위치 값에 0x4의 offset이 작용한듯한 결과가 나온다. add $0x4,%r12 의 결과는 다음과 같다.
%r12에 들어있던 주소값에서 4byte만큼 이동을 했다. 이것은 무엇을 의미할까? 바로 '두 번째로 입력한 정수의 값'을 의미하게 된다. 정수 1개는 4byte니까, 첫 번째로 입력한 정수의 값에서 4byte 옆으로 가면 두 번째로 입력한 정수의 값이 나오는 것이다.
따라서 만약에 맨 처음에 1 2 3 4 5 6 을 입력했다면 add $0x4,%r12 의 결과는 2이다.
<+82>에서 %r12(주소)을 %rbp에 넣는다.
<+85>에서는 %r12에 들어있는 값(두 번째로 입력한 정수의 값, 여기서는 2)을 %eax에 넣기 위해 ()를 사용한 것이다.
<+89>에서 %eax값에서 1을 뺀다.
<+92>에서 cmp를 통해 %eax값을 0x5와 비교하고 있음을 확인할 수 있다.
$eax가 0x5보다 크다면 <+95>의 ja문에 의해 jump를 하게 되는데, <+45>에는 explode_bomb이 있기 때문에 $eax <= 0x5 여야 한다.
%eax에는 두 번째로 입력한 정수의 값이 들어가므로, 정리하자면 두 번째로 입력되는 정수의 값은 6보다 작거나 같아야 한다. 2도 가능하다. 5보다 작으니까.
<+97>에서의 %r13d에는 0이 들어있다.
%r13d의 값이 0x6이 되기 전까지 <+105>의 je문은 실행될 일이 없고, <+110>의 jmp에 의해 <+60>으로 jump하게 된다.
그런데 <+60>은 위에서 다룬 loop가 있는 구간이다. 굳이 자세히 뜯어볼 필요는 없지만 주의해야할 점은, <+82>에서 %rbp의 값을 다음 정수로 update하고 있다는 점이다. 위에서 살펴보았을 때는 %rbp의 값을 update하는 instruction이 없었기 때문에 %rbp는 첫 번째 입력한 정수로 고정이었지만, 지금은 계속 다음 정수로 update되며 loop를 돌기 때문에 매 loop를 돌 때 첫 번째 정수와 나머지 정수의 값을 비교하고, 두 번째 정수와 나머지 정수의 값을 비교하고, 세 번째 정수와 나머지 정수의 값을 비교하고 ... 이런식으로 6개의 정수 중 같은 것이 없는지 겁사하게 된다. (같은 것이 있을 경우 explode_bomb을 호출한다)
위에서와 같이 loop를 다 돌고 나면 <+58>에서 <+78>로 이동하게 될 것이다.
<+78>에서 %r12에 담긴 값은 '두 번째로 입력한 정수의 값을 가리키는 주소값'에서 0x4만큼 이동했으므로 '세 번째로 입력한 정수의 값을 가리키는 주소값'이 된다.
그리고 <+82>와 <+85>에서 위와 동일하게 %rbp에 %r12의 값을, %eax에 세 번째로 입력한 정수의 값을 넣는다.
<+89>에서 %eax에서 1을 뺀 다음, 그 값을 0x5와 비교한다. %eax가 5보다 크면 explode_bomb을 호출하게 된다.
이 과정을 한 번 끝낼 때마다 아까의 %r13d의 값이 1씩 증가하게 되는 것이다. 이중 loop인 것이다.
즉 이 loop를 통해서 검사하는 것은 다음과 같다
- 입력받은 모든 정수 중 서로 같은 값이 없어야 한다.
- 세 번째~여섯 번째 정수들의 값도 (두 번째 정수와 동일하게) 6보다 작거나 같아야 한다
따라서 1 2 3 4 5 6 이라는 입력값은 조건에 부합한다.
1 2 3 4 5 5 이런 암호도 안된다. 5가 두 번 나오기 때문이다.
0 1 2 3 4 5 은 사용해도 될까?
안된다. 위에서 %eax에서 1을 빼는 과정이 있는데, 0에서 1을 빼면 음수가 되는 것이 아니라 매우 큰 양수 값이 되기 때문이다. 그렇게 되면 ja문에 바로 걸리게 된다.
%r13d의 값이 0x6에 도달하는 순간, <+105>의 je문을 통해 <+160>으로 이동하게 된다.
<+160>에서 %esi에 0을 넣는다.
<+165>에서 <+138>로 이동한다.
이 상태에서 %rsp에는 첫 번째로 입력한 정수가 담겨있는 위치가 저장되어 있고, %rsi는 0이다. 따라서 위에서 보았던 경우와 같게 <+138>에서는 %rsi값에 따라 n번째 정수의 값을 %ecx에 담게 되리라는 예측을 할 수 있다.
<+156>의 jg문에서는 %ecx에 담긴 값이 0x1보다 클 경우 jump한다. 조건에 맞지 않을 경우 <+158>에서 jmp를 통해 <+123>으로 가게 되는데, 나의 경우에는 첫 번째 정수인 1인 경우밖에 이에 해당하지 않는다. 0은 사용할 수 없고, 나머지 정수는 1보다 크기 때문이다.
첫 번째 정수가 1이어서 <+123>으로 이동을 했다.
<+123>의 mov가 실행되면 %rdx값이 0x20(%rsp,%rsi,8)에 담긴다.
%rdx에는 6이 담겨있다.
%rdx에 들어있는 메모리 주소는 입력한 첫 번째 정수가 들어있는 %rsp에 들어있는 메모리 주소에서 0x4*5 만큼 더한 값으로, 여섯 번째 정수가 들어있는 메모리 위치를 가리키고 있음을 알 수 있다. (1 2 3 4 5 6 중 6이 들어있는 위치라는 의미)
%rsi에는 0이 들어있으니 0x20+%rsp에 6이 담긴다는 의미인데, %rsp에는 첫 번째 정수의 위치가 들어있다. 0x20은 10진수로 32니까, 0x4*8에 해당한다.
<+128>에서 %rsi의 값에 1을 더해서 1로 만들고, <+132>와 <+136>은 건너뛰게 된다 (%rsi의 값이 0x6과 다르므로).
다시 <+138>에 도달했고, %ecx에 두 번째로 입력한 정수의 위치를 담게 된다(나의 경우는 2).
<+141>에서 %eax에 1을 담고
<+146>을 실행하면 심기를 거스르는 복잡해보이는 값이 담겨있다. 일단은 지금 당장 중요한 값은 아니기 때문에 <node1>이 뭐지? 정도의 생각만 하면서 슬쩍 넘어가자.
<+156>에서 %ecx에 담긴 값(2)은 0x1보다 크기 때문에 <+112>로 이동하게 된다.
<+112>에서 또 다시 %rdx값의 연산이 있는데 위와 비슷하게 크게 신경쓰진 말고 엥 이번에는 <node2>잖아? 하고 넘어가보자.
<+116>에서 %eax에 들어있는 값(1)에 1을 더하고(결과는 2), <+119>에서 %ecx(2)와 비교한다.
두 결과가 동일하기 때문에 <+121>에서 jump가 일어나지 않는다.
<+123>에서 %rdx에 담긴 값을 0x20(%rsp,%rsi,8)에 담는다.
<+128>에서 %rsi+1을 해서 %rsi는 2가 됐고, <+132>와 <+136>은 건너뛰게 된다.
그리고 또 다시 <+138>에 세 번째 정수의 값을 담고, %eax를 0x1로 update하고... 이런 과정이 반복이 된다.
그러다가 %rsi의 값이 0x6이 되었을 때, 그러니까 총 6번 loop를 돌고 난 뒤에 loop를 빠져나와 <+167>로 이동한다.
%rax와 %rbx는 6이 담겨있다.
이 6은 내가 입력했던 6이 아니다. 내가 입력했던 6을 표현하기 위해서는 주소값을 담아야 할 것이기 때문이다. 아마 loop를 돌며 사용했던 조건을 위해 생겨난 0x6이 아닐까 추측하고 있다.
그리고 나서 %ebp에 0x5를 넣고 나면 <+241>로 이동한다.
%rax와 %rbx에 담긴 값들은 의미를 모르겠는 값들이지만 <+247>에서 비교를 해야한다. <+249>의 jge에 걸리지 않으면 explode_bomb이 호출되기 때문에 (%rbx)가 %eax보다 크거나 같아야한다.
<+247>의 시점에서 $eax와 $rbx는 다음과 같다.
<+225>에 의해서 %ebp에는 0x5가 담겨있다.
<+232>에서 mov를 실행하고, %ebp에서 -1을 한 다음 je문으로 가는 구조이다.
만약 이 <+239>의 je문이 실행된다면, <+258>로 jump하여 stack canary의 값을 비교하고 프로그램이 정상종료되게 된다. 즉, je문이 실행되게 해야한다.
je문이 실행되려면 %ebp의 값이 0x1이 되어야 하는데 그럼 %ebp가 끝나기 전까지 loop를 돌게 될 것임을 예측할 수 있다.
<+241>부터 <+247>을 수행하기 위해서는 %rbx에 담긴 것이 도대체 무엇인지 정체를 알아야 할 필요성이 있다.
위에서는 '일단 그냥 넘어가자' 라는 말로 일축하고 넘어갔지만, 기억을 되돌려보면 <node1>, <node2>와 같은 표현이 있던 것을 떠올릴 수 있다.
그럼 이 node는 도대체 무엇을 의미할까?
이것의 정체는 바로 linked list이다.
x/20x로 %rbx를 뜯어보자.
일단 각 node의 두 번째 값들은 차례대로 나열되어 있다. node의 번호처럼 보인다.
node의 첫번째 값들은 사실 계속해서 %rbx나 %rax에 담기는 값이다. 즉, <+247>에서 비교를 할 때 쓰이는 값이다.
세번째 값과 네번째 값은 떨어져있어서 헷갈릴 수 있지만, 두 값을 합치면 완전한 하나의 주소가 된다. <node1>기준으로 세번째 값과 네번째 값을 뒤에서부터 합치면 0x0000555555758210이다. 이 위치는 어디일까? 바로 <node2>의 위치이다. 즉, 각 node를 잇고있는 포인터가 저장되는 곳이다.
입력한 정수는 6개였는데, node가 5개만 있을리 없다. 역시, <node5>의 세번째, 네번째 값이 존재했고 이 값을 합쳐서 x/4x로 값을 추적해보았더니 <node6>까지 찾아낼 수 있었다.
보기 좋게 표로 정리하면 이런 느낌이 된다
<node1>
0x2da
1
<node2>
0x1ad
2
<node3>
0x093
3
<node4>
0x0d1
4
<node5>
0x076
5
<node6>
0x169
6
여기서 다시 아까의 instruction을 상기해보자.
%rbx가 어떤 node를 가리키게 될 것이다. 이 node를 <node1>이라고 한다면, %rax에는 거기서 0x8만큼 이동하면 있는 <node1>에 있는 <node2>를 가리키는 포인터가 담기게 된다. 그리고 이 포인터를 참조해서 얻은 <node2>가 %eax가 담기게 되는 것이다.
즉, %rax에는 %rbx의 다음 node가 담긴다.
그런데 %rbx가 항상 %eax보다 크거나 같아야 한다. 그렇게 되면 node의 값이 큰 순으로 정렬을 해야한다. 왜냐하면 loop를 돌면서 모든 node들을 비교하게 될 것이고, 그 모든 node들 사이에서 어떤 node는 이전 node보다 크거나 같은 값을 가져야 한다는 규칙이 성립해야하기 때문이다.
위의 linked list를 값이 작은 것부터 정렬하면 이렇게 된다.
<node5>
0x076
5
<node3>
0x093
3
<node4>
0x0d1
4
<node6>
0x169
6
<node2>
0x1ad
2
<node1>
0x2da
1
우리는 값이 큰 순서대로 숫자를 배치해야하므로 아래에서부터 값을 읽으면, 1 2 6 4 3 5 라는 결과가 나온다.
암호는 1 2 6 4 3 5 이다.
BombLab을 하고있다면 이 BombLab에는 secret phase가 존재한다는 것을 알 것이다. secret phase에 대한 힌트를 하나 주자면, main함수를 disas해보길 바란다.
































댓글
댓글 쓰기