CTF Assembly Challenge: Learning ARM and X86 Assembly

The problem is relatively simple but tests the basics of binary assembly.I personally think that the fundamentals are veryimportant, so I spent some time carefully studying this problem to solidify my foundation.If it wereoffline, without GPT’s help, contestants would need a solid understanding of assembly to conduct a detailed analysis of the problem.

If you learn assembly with problems to guide you, analyze patiently, I believe the gains will be significant, which helps to understand the logic and methods of assembly operations.

Let’s start today’s problem explanation; the question will be uploaded in a package. If there are any errors, please point them out.

一


x86汇编


I won’t paste the original problem here; it’s in the attachment. Here I’ll paste my annotated analysis. The problem provides AT&T style x86 assembly, so how does it differ from the Intel syntax we are familiar with?

1. Operand order differs: In AT&T syntax, the order of operands is “source, destination”, whereas in Intel syntax, it is “destination, source”. For example, in AT&T syntax, <span>movl %eax, %ebx</span> means moving the value from the <span>eax</span> register to the <span>ebx</span> register, while in Intel syntax, the corresponding instruction is <span>mov ebx, eax</span>.

2. Register names differ: In AT&T syntax, register names start with <span>%</span>, while in Intel syntax, there is no prefix. For example, in AT&T syntax, <span>%eax</span> represents the EAX register, while in Intel syntax, the corresponding register name is <span>EAX</span>.

3. Immediate values and memory address representations differ: In AT&T syntax, immediate values use a <span>$</span> prefix, while memory addresses are enclosed in square brackets <span>[]</span>. For example, <span>movl $0x123, %eax</span> means moving the immediate value <span>0x123</span> to the <span>eax</span> register, while <span>movl (%ebx), %eax</span> means moving data from the memory address pointed to by the <span>ebx</span> register into the <span>eax</span> register.

4. Sign extension differs: In AT&T syntax, sign extension is performed by default, while in Intel syntax, it requires using <span>movsx</span> or <span>movzx</span> instructions. For example, in AT&T syntax, <span>movb -1(%eax), %bl</span> means moving the byte at the address <span>eax-1</span> after sign extension into the <span>bl</span> register, while in Intel syntax, the corresponding instruction would be <span>movsx bl, byte ptr [eax-1]</span>.(It’s recommended to first look at the original problem, analyze it manually, and then try to decrypt to see if you can get the correct flag.)

With the above foundation, let’s start looking at the problem:

```@ Define some auxiliary information, such as: file name, section name
.file "main.c"
    .text
    .section .rodata
    .align 32
    .type encode, @object
    .size encode, 39
encode:
    .string "**************************************"
    .text
    .globl main               @ Mark main as globally visible.
    .type main, @function    @ Declare main as a function.
main:
.LFB0:
    .cfi_startproc     @ CFI (Call Frame Information) directive, used to generate debugging information.
    endbr64         @ Corresponds to specific processor instruction, providing overflow detection for branch target addresses.
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp     @ rsp=rsp-16
    @ Stack operation instructions, used to save and restore register values. The above, excluding CFI instructions, is classic stack space allocation instructions
    movl    $0, -4(%rbp)  @ [rbp-4]=0
    jmp    .L2              @ Unconditional jump to L2 to start execution

@ L5 From the analysis below, it can be known that this is a loop
.L5:
    addl    $1, -4(%rbp)  @[rbp-4]++
.L2:
    movl    -4(%rbp), %eax        @ eax=[rbp-4]
    cltq                        @ Sign extend the low 32 bits of eax to 64 bits, storing the result in the rax register (in x86-64, eax is the low 32 bits of rax).
    leaq    encode(%rip), %rdx  @ rdx=encode[rip]  This passes the flag to rdx
    movzbl    (%rax,%rdx), %eax   @ Read a byte from the memory address based on rax+rdx, zero-extend it to 32 bits. Then store the result in the eax register
    movsbl    %al, %ecx           @ ECX = EAX
    movl    -4(%rbp), %eax      @ eax = [rbp-4]
    andl    $1, %eax            @ Check odd/even
    testl    %eax, %eax
    je    .L3                    @ If even, jump to L3
    movl    -4(%rbp), %eax      @ eax=[rbp-4]
    subl    $1, %eax            @ eax=eax-1
    cltq                       
    leaq    encode(%rip), %rdx  @ rdx = encode[rip]
    movzbl    (%rax,%rdx), %eax   @ eax = [rax+rdx] rax acts like index i
    movsbl    %al, %eax    
    jmp    .L4
.L3:
    movl    -4(%rbp), %eax   @ eax=[rbp-4]
.L4:
    xorl    %ecx, %eax    @ eax = eax ^ ecx
    movl    %eax, %edi    @ edi = eax
    call    putchar@PLT   @ putchar
    movl    -4(%rbp), %eax   @ eax=[rbp-4]
    cltq                    
    cmpq    $37, %rax        @ cmp(37,rax) 
    jbe    .L5             @ If rax < 37, jump to L5, continue loop
    movl    $0, %eax    @ eax =0 
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc

    @ Some debugging information
.LFE0:
    .size main, .-main
    .ident "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section .note.GNU-stack,"",@progbits
    .section .note.gnu.property,"a"
    .align 8
    .long 1f - 0f
    .long 4f - 1f
    .long 5
0:
    .string "GNU"
1:
    .align 8
    .long 0xc0000002
    .long 3f - 2f
2:
    .long 0x3
3:
    .align 8
4:


This problem has a bit of a twist regarding the relationship between [rbp-4], eax, and ecx.

Here is the complete solution wp; the crypto ciphertext is provided in the problem attachment.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


int main() {
    char crypto[] = { 0x66,0x0a,0x63,0x06,0x7f,0x1e,0x37,0x00,0x38,0x03,0x6f,0x04,0x6e,0x56,0x3d,0x55,0x22,0x06,0x26,0x51,0x72,0x04,0x21,0x03,0x21,0x01,0x7c,0x05,0x2b,0x0e,0x7c,0x50,0x17,0x56,0x10,0x0b,0x16,0x4f,0x26 };
    for (size_t i = 0; i < 39; i++)
    {
        if (i % 2 == 0)// Even
        {
            printf("%c", crypto[i] ^i);
        }
        else {
            char tmp = i;
            crypto[i - 1] = crypto[i - 1] ^ (i - 1);
            printf("%c", crypto[i-1] ^ crypto[i]);
        }
    }

    return 0;
}

</string.h></stdlib.h></stdio.h>
二


ARM汇编


Here is a brief introduction to ARM assembly, taken from my personal notes.

ARM assembly is a low-level programming language aimed at the ARM architecture, used for writing programs closely related to hardware. Here are some characteristics of ARM assembly:

1. Reduced Instruction Set: The ARM architecture is known for its Reduced Instruction Set Computing (RISC). Its instruction set is designed to be simple and efficient, providing basic arithmetic, logical, data transfer, and flow control operations.

2. Registers: The ARM architecture has a large number of general-purpose registers. The 32-bit ARM architecture (ARMv7 and ARMv8) provides 16 general-purpose registers (R0-R15) for storing commonly used data and temporary calculation results.

3. Three-Address Instruction Format: ARM instructions typically use a three-address instruction format, meaning the instruction includes source operands, destination operands, and the operation to perform. This makes ARM assembly relatively concise and reduces memory access times.

4. Thumb Instruction Set: On some ARM processors, there is also a Thumb instruction set that uses 16-bit instructions (compared to 32-bit ARM instructions) to reduce code size. The Thumb instruction set can provide higher code density, suitable for memory-constrained embedded systems.

5. Cross-Platform: The ARM architecture is widely used in mobile devices, embedded systems, and low-power applications, including smartphones, tablets, and IoT devices. Due to the diversity of the ARM processor family, ARM assembly can be shared among different ARM processors, but there will also be some differences and specific instructions.

Here is the annotated assembly of the problem:

 @ ARM gcc 11.1(linux)
.Ltext0:
flag:
        .ascii  "***************************\000"
.LC0:
        .ascii  "%02x\000"
main:
.LFB0:
        @ This is a comment indicating that the function takes no parameters, uses an 8-byte stack frame, and needs to save the context data.
        @ args = 0, pretend = 0, frame = 8
        @ frame_needed = 1, uses_anonymous_args = 0

        push    {r7, lr}   @ Save the values of r7 and lr registers to the stack for later restoration.
        sub     sp, sp, #8 @ Allocate 8 bytes of space on the stack.
        add     r7, sp, #0 @ Save the stack pointer to r7 register, forming a frame pointer.
.LBB2:
        movs    r3, #0
        str     r3, [r7, #4] @ Assign 0 to r3 register and store it at the frame pointer offset of 4.
        b       .L2          @ Unconditional jump to label .L2.
.L5:                         @ This is a label marking the start of a loop (the reason it's a loop will be analyzed later)
        ldr     r3, [r7, #4] 
        and     r3, r3, #1 
        cmp     r3, #0
        bne     .L3          
        @ Load a value from the frame pointer offset of 4 into r3 register and perform a bitwise AND with 1. Then compare the result with 0, and if not equal, jump to label .L3.
        movw    r3, #:lower16:flag @ Load the lower 16 bits address of the global variable flag into r3 register.
        movt    r3, #:upper16:flag @ Load the upper 16 bits address of the global variable flag into the high 16 bits of r3 register.
        ldr     r2, [r7, #4]       @ Load a value from the frame pointer offset of 4 into r2 register. This value may be an offset used to correct the address in r3 register.
        add     r3, r3, r2         @ Add the value in r2 register to r3 register to correct the address of flag.
        ldrb    r3, [r3]        @ zero_extendqisi2 Load a value from the address in r3 register and zero-extend it. This value is the value of flag.
        eor     r3, r3, #57   @ Perform a bitwise XOR operation with the value in r3 register and 57.
        uxtb    r3, r3        @ Zero-extend the value in r3 register to a byte.
        mov     r1, r3        @ Copy the value in r3 register to r1 register as the second parameter for the printf function.
        movw    r0, #:lower16:.LC0   @ Load the string similarly
        movt    r0, #:upper16:.LC0
        bl      printf          @ Call the function
        b       .L4            @ Unconditional jump

.L3:    @ flag[i] is odd, do not operate
        movw    r3, #:lower16:flag
        movt    r3, #:upper16:flag
        ldr     r2, [r7, #4]
        add     r3, r3, r2      @ The purpose here is to take the index flag[i]
        ldrb    r3, [r3]        @ zero_extendqisi2
        mov     r1, r3          @
        movw    r0, #:lower16:.LC0
        movt    r0, #:upper16:.LC0
        bl      printf          @ Call the function


.L4:    @ Load a value from the frame pointer offset of 4 into r3 register and add 1. Then save the result back to the frame pointer offset of 4
        ldr     r3, [r7, #4]
        adds    r3, r3, #1
        str     r3, [r7, #4]


.L2:    @ Load a value from the frame pointer offset of 4 into r3 register and compare it with 26. If r3 is less than or equal to 26, jump to label .L5, otherwise continue executing.
        ldr     r3, [r7, #4]
        cmp     r3, #26
        ble     .L5

.LBE2:  @ Assign 0 to r3 register and store it as the return value in r0 register, increase the value of frame pointer and set the stack pointer to the frame pointer
        movs    r3, #0
        mov     r0, r3
        adds    r7, r7, #8
        mov     sp, r7
        @ sp needed
        pop     {r7, pc}
.LFE0:
.Letext0:
.Ldebug_info0:
.Ldebug_abbrev0:
.Ldebug_line0:
.LASF6:
.LASF10:
.LASF0:
.LASF11:
.LASF3:
.LASF7:
.LASF9:
.LASF1:
.LASF15:
.LASF8:
.LASF12:
.LASF2:
.LASF14:
.LASF5:
.LASF13:
.LASF4:
@ 7d416a436d4642315c740c64095f7872745f6d720d315769574744

Write the corresponding decryption script:

#include <stdio.h>

char flag[] = { 0x7d,0x41,0x6a,0x43,0x6d,0x46,0x42,0x31,0x5c,0x74,0x0c,0x64,0x09,0x5f,0x78,0x72,0x74,0x5f,0x6d,0x72,0x0d,0x31,0x57,0x69,0x57,0x47,0x44 }; // The string content here should match the actual flag in the code

int main() {
    int i = 0;
    while (i <= 26) {
        if ((i & 1) != 0) { // If i is odd, output directly
            printf("%c", flag[i]);
        } else {
            printf("%c", flag[i] ^ 57);
        }
        i++;
    }
    return 0;
}

</stdio.h>

As you can see, both problems check for odd/even, unlike previous ones which were simply XOR or addition/subtraction. This way of checking odd/even further tests the contestants’ understanding of the assembly process.

CTF Assembly Challenge: Learning ARM and X86 Assembly

Kanxue ID: NYSECbao

https://bbs.kanxue.com/user-home-971547.htm

*This article is an excellent piece from the Kanxue Forum, authored by NYSECbao. Please indicate that it comes from the Kanxue community when reprinting.
CTF Assembly Challenge: Learning ARM and X86 Assembly

# Previous Recommendations

1、IOFILE exploit introduction

2、Experience of the front end of compiler principles

3、How to reverse the encryption of Himalaya xm files using pure guessing (wasm part)

4、How AMSI helps you defend against malware

5、sRDI — Shellcode reflective DLL injection technology

6、Detection and parameter calculation analysis of APP

CTF Assembly Challenge: Learning ARM and X86 Assembly

Leave a Comment