










This is the second lab of course CMU 15-213. I have to say that, unlike datalab, this lab is fairly ‘logical’ and fun to play with. The idea of looking into assembly code to get a fundamental understanding of how programs actually got compiled into machine level language is absolutely effective and genius in teaching.
Make sure you’ve solved the puzzles on yourself before checking my solutions!
If you find any mistakes in this blog, you’re most welcomed to leave a comment on it.
1 | objdump -D bomb > bomb.s |
Obviously we gotta look into the assembly code of phase_1:
1 | 0000000000400ee0 <phase_1>: |
Even more obvious we should take a look into function <strings_not_equal>:
1 | 0000000000401338 <strings_not_equal>: |
From the code we can see that the funcion first compare the length of the two strings and then compare each character.
If we type in hello as a ‘test’ input for this phase, we can find out that it is the first call for string_length that calculates the length of our input string, for %eax is set to 5(as the length of hello) after it returns. (In the help of gdb)
Then this 5 is moved into %r12 at:
1 | 401347:441 89 c4 mov %eax,%r12d |
Then compared with the second length calculated at:
1 | 401357:441 39 c4 cmp %eax,%r12d |
So the idea is simple: just step into the second string_length and see what is at the memory address stored in the register corresponding to the first argument of a function call, which is %rdi:
Step into the second <string_length>
1 | (gdb) print /x $rdi |
And we’re done for phase 1.
As usual, assembly:
1 | 0000000000400efc <phase_2>: |
Immediately we see this <read_six_numbers>, so 1 2 3 4 5 6 is an ideal test string we should put in here.
Assembly of read_six_numbers:
1 | 000000000040145c <read_six_numbers>: |
%rdi while manipulating %rsi a lot, which is pretty strange at first glance.%rdi is implicitly passed to sscanf as the address to hold the user input.main to see where %rdi is set after read_line: 1 | # ...Phase 1 |
read_line.There seems to be a lot going on before invoking sscanf, but it’s mainly preparing space for user input on the stack.
In short:
%rdi passed all along to sscanf as input string.%rsi holds the pointer pointing to the address of the buffer which is to store the six numbers parsed by sscanf.%rdx, %rcx, %r8, %r9 holding the first four addresses of each of the target buffer block to hold the parsed numbers, while the rest 2 addresses are pushed on the stack.sscanf then set the values stored in those addresses to the six input numbers.sscanf returns, compare the return value with 0x5. If the return value, which represents the number of the values parsed, is less or equal to 5, then explode the bomb.Note:
1 | 401467:448 8d 46 14 lea 0x14(%rsi),%rax |
After reading six numbers from user input, first check whether the first number is 0x1:
1 | 400f0a:483 3c 24 01 cmpl $0x1,(%rsp) |
Then there is a loop determining whether the input numbers are of an expected pattern:
1 | 400f17:48b 43 fc mov -0x4(%rbx),%eax |
Can be written as pseudocode like:
1 | int rax; |
To this stage, the answer can not be more obvious.
Assembly:
1 | 0000000000400f43 <phase_3>: |
From the judgment towards the return value of sscanf:
1 | 400f5b:4e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt> |
We can see that the number of the expected user input should be more than 1, also:
1 | 400f6a:483 7c 24 08 07 cmpl $0x7,0x8(%rsp) |
For function sscanf, %rdi holds the address of the raw input, %esi holds the format string, thus rdx and rcx holds the two addresses where the program put the two parsed numbers to.
Note:
0x7(also non-negative for ja reads unsigned values comparison result). As a result 1 2 should be an ideal test input.Note:
%rdi is passed from main function, pointing to the address of user input string.Then the ‘bomb’ put the second input into %eax and jump to somewhere in a jump table:
1 | 400f71:48b 44 24 08 mov 0x8(%rsp),%eax |
The syntax here in the second line of code means:
0x402470 + 0 + (%rax) * 0x8So according to our test input, the value at address stored in %rax should be 0x1, Thus the target address of this jmp should be located at 0x402470 + 0 + 1 * 0x8 = 0x402478, which is:
1 | (gdb) x/wx 0x402478 |
Which is:
1 | 400fb9:4b8 37 01 00 00 mov $0x137,%eax |
Which means the correct second input corresponding to 0x1 as the first input is 0x137 = 311.
We can therefore reverse the whole jump table:
1 | switch (first_input): |
Assembly:
1 | 000000000040100c <phase_4>: |
Easy part:
sscanf user input, write two numbers at address %rsp + 0x8 and %rsp + 0xc.0xe.%rdx to 0xe. Set %rsi to 0x0. Set %rdi to the first user input.<func4>.Let’s take a look into func4:
1 | 0000000000400fce <func4>: |
We can see that the calculation for %ecx is actually fixed, which means whatever the input numbers are, %ecx will be 0x7 during the two comparisons:
1 | # ... |
Also we notice that each time the function returns from a recursion, it doubles the value in %rax and increments it by 1.
As the assembly code we can see after func4 returns:
1 | # ... |
We definitely don’t want %rax to be non-zero. As a result, we should pass a value to func4 that both jle and jge fires, which means %edi should equal to %ecx at the moment of comparison.
Since we’ve already know that %ecx will be a fixed 0x7, the value of %edi is now revealed.
The second input is an easy zero:
1 | # ... |
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。