Study

레지스터의 종류

General Purpose Registers

  1. 데이터 레지스터

    • RAX : 누산기로서, 연산 결과를 저장하는 데 주로 사용
    • RBX : 기본 레지스터로서, 데이터를 보관하거나 주소 값을 가리키는 데 사용
    • RCX : 주로 반복문의 카운터로 사용
    • RDX : 확장된 산술 연산에서 단일 레지스터의 크기를 초과할 때 추가로 사용하는 레지스터
  2. 포인터 레지스터

    • RBP : 스택 프레임의 기본 포인터로, 스택프레임의 시작 지점 주소(스택 복귀 주소)로 사용
    • RSP : 스택의 최상단(현재 스택 주소)로 사용
  3. 인덱스 레지스터

    • RSI : 데이터를 복사할 때 src 데이터(복사할 데이터의 주소)로 사용
    • RDI : 데이터를 복사할 때 복사된, dest 데이터의 주소로 사용

Segment Registers

  • CS : 현재 실행되는 코드 세그먼트 레지스터
  • DS : 일반 데이터 세그먼트 레지스터
  • SS : 스택 세그먼트 레지스터
  • ES, FS, GS: 추가 세그먼트 레지스터, 특정 용도나 운영 체제의 특수한 목적으로 사용

Instruction Pointer

  • RIP : 현재 실행 중인 명령어의 주소를 가리킴.

Flags Register

  • RFLAGS : 연산 결과나 CPU의 상태에 따라 여러 flag bit를 저장함.

문제

ASMLevel7에 오신 것을 환영합니다
==================================================

어떤 레벨과 상호 작용하기 위해서는 이 프로그램에게 stdin을 통해 raw 바이트를 보내야 합니다.
이 문제들을 효과적으로 해결하기 위해서는, 먼저 한 번 실행하여 무엇을 해야 하는지 확인한 다음,
어셈블리 코드를 작성하고, 조립한 다음, 이 프로그램에게 바이트를 파이프합니다.

예를 들어, 어셈블리 코드를 asm.S 파일에 작성한다면, 다음과 같이 객체 파일로 조립할 수 있습니다:
as -o asm.o asm.S

그리고 나서 .text 섹션 (코드)을 asm.bin 파일로 복사할 수 있습니다:
objcopy -O binary --only-section=.text asm.o asm.bin

마지막으로, 그것을 챌린지에게 보낼 수 있습니다:
cat ./asm.bin | /challenge/run

모든 것을 한 명령어로 실행할 수도 있습니다:
as -o asm.o asm.S && objcopy -O binary --only-section=.text ./asm.o ./asm.bin && cat ./asm.bin | /challenge/run

이 레벨에서는 레지스터를 사용하여 작업하게 됩니다. 레지스터에서 수정하거나 읽어야 할 것을 요청받게 됩니다.

이제 메모리에 동적으로 몇 가지 값을 설정할 것입니다. 각 실행마다 값이 바뀝니다. 이는 레지스터를 공식적으로 조작해야 한다는 것을 의미합니다. 사용할 레지스터는 미리 알려드리고 결과를 어디에 둬야 하는지도 알려드릴 것입니다. 대부분의 경우, rax입니다.

이 레벨에서는 비트 로직과 연산을 많이 다루게 될 것입니다. 이는 레지스터나 메모리 위치에 저장된 비트와 직접 상호작용하는 것을 많이 포함합니다. 또한 x86의 로직 명령어인 and, or, not, xor를 사용해야 할 가능성이 높습니다.

어셈블리에서의 쉬프팅은 또 다른 흥미로운 개념입니다! x86은 레지스터에서 비트를 '쉬프트'할 수 있게 해줍니다. 예를 들어, rax를 살펴봅시다. 이 예제를 위해 rax는 8 비트만 저장할 수 있다고 가정합니다(보통은 64 비트를 저장합니다). rax의 값은 다음과 같습니다:
rax = 10001010
만약 값을 왼쪽으로 한 번 쉬프트한다면:
shl rax, 1
새 값은 다음과 같습니다:
rax = 00010100
여러분은 볼 수 있듯이, 모든 것이 왼쪽으로 쉬프트되고 가장 높은 비트는 떨어져 나가며 오른쪽에 새로운 0이 추가됩니다. 이를 통해 여러분이 관심 있는 비트에 특별한 것을 할 수 있습니다. 또한 빠른 곱셈, 나눗셈, 그리고 가능한 나머지 연산을 하는데 있어 좋은 부작용도 있습니다.
다음은 중요한 명령어입니다:
shl reg1, reg2       <=>     reg1을 reg2만큼 왼쪽으로 쉬프트
shr reg1, reg2       <=>     reg1을 reg2만큼 오른쪽으로 쉬프트
참고: 모든 'regX'는 상수나 메모리 위치로 대체될 수 있습니다

다음 명령어만 사용하여 다음을 수행해 주세요:
mov, shr, shl
다음을 수행해 주세요:
rdi의 5번째 최하위 바이트로 rax를 설정하세요.
즉,
rdi = | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
B4의 값을 rax로 설정하세요.

이제 여러분의 코드를 위해 다음을 설정할 것입니다:
rdi = 0x67285a2b46dc27a8

어셈블리 코드를 바이트로 제공해주세요 (최대 0x1000 바이트까지):

문제 풀이

양쪽으로 Shift하여 RAX에 삽입

[bits 64]
shl rdi, 24
shr rdi, 56
mov rax, rdi

8 7 6 5 4 3 2 1
7e da 18 9c 3b 0f a8 53

0x7eda189c3b0fa853이 rdi 레지스터에 할당되었음.
rdi 레지스터의 5번째 하위 byte는 0x9c인 것을 알 수 있음.

8 7 6 5 4 3 2 1
9c 3b 0f a8 53 00 00 00

1 byte 당 8 bit인 것을 생각한다면, 5번째 byte가 8번째 byte로 되려면 24 bit만큼 이동을 해야 함.
shl rdi, 0x18을 통해 24bit를 Left Shift하였음.

8 7 6 5 4 3 2 1
00 00 00 00 00 00 00 9c

이후, 8번째 byte가 1번째 byte로 될려면 7번 Right Shift를 해야하니 56 bit를 이동해야 함.
shr rdi, 0x38를 통해 56 bit를 Right Shift하였음.

Right Shift 후 RAX의 하위 레지스터에 삽입

[bits 64]
shr rdi, 32
mov al, dil   

8 7 6 5 4 3 2 1
59 a0 c1 6d b5 88 8d 7d

0x59a0c16db5888d7d이 rdi 레지스터에 할당되었음.
rdi 레지스터의 5번째 하위 byte는 0x6d인 것을 알 수 있음.

8 7 6 5 4 3 2 1
00 00 00 00 59 a0 c1 6d

5번째 byte가 1번째 byte로 되려면, 32 bit 만큼 Right Shift 되어야 함.
shr rdi, 0x20를 통해 32 bit Right Shift하였음.

이후, mov al, dil를 통해 rdi 레지스터의 하위 8 byte를 의미하는 dil 레지스터를 al 레지스터에 삽입함.

코드 분석

class ASMLevel7(ASMBase):
    """
    Shift
    """

    init_rdi = random.randint(0x55AA55AA55AA55AA, 0x99BB99BB99BB99BB)

    dynamic_values = True
    registers_use = True
    bit_logic = True
    whitelist = ["mov", "shr", "shl"]

    @property
    def description(self):
        return f"""
        Shifting in assembly is another interesting concept! x86 allows you to 'shift'
        bits around in a register. Take for instance, rax. For the sake of this example
        say rax only can store 8 bits (it normally stores 64). The value in rax is:
        rax = 10001010
        If we shift the value once to the left:
        shl rax, 1
        The new value is:
        rax = 00010100
        As you can see, everything shifted to the left and the highest bit fell off and
        a new 0 was added to the right side. You can use this to do special things to
        the bits you care about. It also has the nice side affect of doing quick multiplication,
        division, and possibly modulo.
        Here are the important instructions:
        shl reg1, reg2       <=>     Shift reg1 left by the amount in reg2
        shr reg1, reg2       <=>     Shift reg1 right by the amount in reg2
        Note: all 'regX' can be replaced by a constant or memory location

        Using only the following instructions:
        mov, shr, shl
        Please perform the following:
        Set rax to the 5th least significant byte of rdi
        i.e.
        rdi = | B7 | B6 | B5 | B4 | B3 | B2 | B1 | B0 |
        Set rax to the value of B4

        We will now set the following in preparation for your code:
        rdi = {hex(self.init_rdi)}
        """

    def trace(self):
        self.start()
        expected = (self.init_rdi >> 32) & 0xFF
        yield self.rax == expected, f"rax was expected to be {hex(expected)}, but instead was {hex(self.rax)}"

해당 코드들은 밑 레벨의 코드와 비슷하기 때문에 trace 메소드만 설명하도록 하겠음.

힌트 출력

bit_logic = True
# ASMBase Class의 코드

if self.bit_logic:
    hints += """
    In this level you will be working with bit logic and operations. This will involve heavy use of
    directly interacting with bits stored in a register or memory location. You will also likely
    need to make use of the logic instructions in x86: and, or, not, xor.
    """

사용자에게 비트 논리와 연산에 관해 중점을 준다는 힌트를 제공함.
또한 and, or, not, xor 논리 명령어에 관해 알려주고 있음.

Whitelist

whitelist = ["mov", "shr", "shl"]

mov, shr, shl 명령으로만 문제 해결하라는 코드임.
아마 출제자는 양쪽으로 Shift하여 RAX에 삽입하는 방법도 염두하여 shl 명령어도 whitelist에 추가한건지 아닌가 추측됨.

정답 검증 로직

def trace(self):
    self.start()
    expected = (self.init_rdi >> 32) & 0xFF
    yield self.rax == expected, f"rax was expected to be {hex(expected)}, but instead was {hex(self.rax)}"

self.start 메소드를 호출하여 제출한 어셈블리 바이너리를 실행함.

self.init_rdi를 32 Bit 오른쪽으로 이동 후 0xFF을 AND 연산하여 하위 8 Bit를 추출하여 expected 변수에 저장함.

제출한 어셈블리 바이너리 실행 후의 rax 레지스터 값과 expected 변수의 값을 비교하여 같으면, (True, rax was expected to be [정답 결과], but instead was [제출한 결과])을 반환하게 됨.

복사했습니다!