thesis/slides.typ
Jean-Marie 'Histausse' Mineau 0fd8b495c0
Some checks failed
/ test_checkout (push) Failing after 35s
update slides
2025-12-01 00:22:53 +01:00

2083 lines
49 KiB
Typst

#import "@preview/polylux:0.4.0": *
#import "slides/lib.typ": *
#import "slides/icons.typ" as ico
#import "@local/codly:1.3.1": *
// Require local install, fix needed for highlight-inset
// TMP="$(mktemp -d)" && curl -L https://github.com/Dherse/codly/archive/refs/tags/v1.3.1.zip -o "${TMP}/c.zip" && unzip -d "${TMP}" "${TMP}/c.zip" && mkdir -p ~/.local/share/typst/packages/local/codly && mv "${TMP}/codly-1.3.1" ~/.local/share/typst/packages/local/codly/1.3.1 && rm -rf "${TMP}"
#import "@preview/codly-languages:0.1.1": *
#show: codly-init.with()
#let default-codly = (
display-name: false,
display-icon: false,
zebra-fill: none,
fill: luma(240),
radius: 1em,
inset: (y: 0.15em),
highlighted-default-color: highlight-color,
highlight-fill: it => it.lighten(40%), //highlight-color,
highlight-inset: (x: 0pt, y: 0pt),
highlight-outset: (x: 0.15pt, y: 0.15em),
)
#codly-disable()
#let analyse-apk = move(dx: -50pt, image("slides/imgs/apk-analysis.svg", width: 300pt))
#set text(lang: "en")
#set list(marker: none)
#set par(leading: 0.2em)
#set list(spacing: 1em)
#show: sns-polylux-template.with(
txt-font: "New Computer Modern",
title-font: "TeX Gyre Heros",
aspect-ratio : "16-9",
title : [From Large Scale Analysis to Dynamic Deobfuscation],
subtitle : [The Woes of Android Reverse Engineering],
footer-text : [Jean-Marie Mineau -- PhD Defense],
//short-event : [Rennes, 2025/12/9],
title-size : 32pt,
section-size : 18pt,
size : 22pt,
//logo-1 : image("slides/imgs/logo_irisa.png"),
//logo-2 : image("slides/imgs/logo_pirat.png"),
//
colormap : sns-polylux-template_sns-pirat,
authors : (
{
set text(weight: "bold")
[MINEAU Jean-Marie]
v(1em)
},
text(size: 16pt)[LALANDE Jean-François, PhD supervisor],
text(size: 16pt)[VIET TRIEM TONG Valérie, PhD co-supervisor],
//text(size: 15pt)[NICOMETTE Vincent, Rapporteur],
//text(size: 15pt)[SIGNOLES Julien, Rapporteur],
//text(size: 15pt)[BARAIS Olivier, Jury],
//text(size: 15pt)[AONZO Simone, Jury],
),
date : datetime(year: 2025, month: 12, day: 9),
)
/*
* Intro:
* Dear jury, gentle people of the audience, here and online, thank you for your presence.
* I am Jean-Marie Mineau, and today I will be defending my thesis about Android Application reverse engineering and the many difficulties a reverse engineer might encounter.
* This thesis was suppervised by Jean-François Lalande and Valerie Viet Triem Tong, within the PIRAT research team at IRISA.
*/
#title-slide(
logo: grid(columns: 2,
image("slides/imgs/logo_pirat.png"),
image(width: 500pt, "slides/imgs/logo_cs.png"),
image("slides/imgs/logo_irisa.png"),
image("slides/imgs/platypus.png"),
)
)
#slide(
new-sec: true,
title: [Introduction],
hide-title: true,
/*foreground: {
ghost-5(x: 10%, y: 30pt)
ghost-4(x: 95%, y: 80%)
//ghost-4(x: 45%, y: 43%)
}*/
)[
#set align(center+horizon)
#grid(
columns: (1fr, 1fr), [
#image("slides/imgs/google.png", width: 200pt)
- Smartphones are computers
- Android = Linux + Android Runtime (ART)
- APK = computer program (Java-ish)
],
//image("slides/imgs/phone.png", height: 350pt)
ico.phone(
height: 350pt,
body: {
ico.android(height: 150pt, stroke: none)
}
)
)
#v(2em)
]
#counter("logical-slide").update( n => n - 1 )
#slide(
foreground: ghost-4(x: 60%, y: 25%, rot: 45deg)
)[
#set align(center+horizon)
#grid(
columns: (3fr, 2fr), stack(dir: ltr,
item-by-item[
- Personal Data and PII
- Computing Power
- Phone
- Mic, Camera, \ Geolocalisation
],
[ $ => $ ],
item-by-item()[
- Ransomware/Spyware
- Cryptojacker
- Expander (phone billing)
- Stalkerware
]
),
{
move(
dx: 20pt,
ico.phone(
height: 350pt,
body: {
ico.android(height: 150pt, stroke: none)
}
)
)
}
)
]
#slide(
title: [Obfuscation],
//foreground: eye-1(x: 95%, y: 85%, mirror: true)
)[
#set list(marker: [-])
Applications might use *obfuscation* to either:
- protect their IP
- hide malicious behaviour
#v(1em)#uncover(2)[
We will focus on two techniques:
- *Dynamic Code Loading*
- *Reflection*
/*
* Low effort yet efficiant, commonly found
*/
]
]
#for i in range(4) {
if i != 0 {
counter("logical-slide").update( n => n - 1 )
}
show: yes-codly
slide(
title: [Obfuscation],
subtitle: if i == 0 [Example] else if i == 1 [Dynamic Code Loading] else if i in (2, 3) [Reflection] else { none },
foreground: eye-1(x: 95%, y: 85%, mirror: true)
)[
#if i == 0 {
codly(..default-codly)
} else if i == 1 {
codly(
highlighted-lines: (1, 5, 6, 7, 8),
..default-codly
)
} else if i == 2 {
codly(
highlighted-lines: (2, 3),
highlights: (
(line: 10, start: 42, end: 59, fill: pirat-color.blue),
(line: 13, start: 3, end: 21, fill: pirat-color.blue),
),
..default-codly
)
} else if i == 3 {
codly(
highlighted-lines: (10,),
highlights: (
(line: 12, start: 14, end: 34, fill: pirat-color.blue),
(line: 15, start: 2, end: 19, fill: pirat-color.blue),
),
..default-codly
)
}
#scale(70%, reflow: true)[
```java
String DEX = "ZGV4CjA [...] EAAABEAwAA";
String className = "W5f3 [...] 3sls=";
String methodName = "n6WGYJzjDrUvR9cYljlNlw==";
ClassLoader cl = new InMemoryDexClassLoader(
ByteBuffer.wrap(Base64.decode(DEX, 2)),
Main.class.getClassLoader()
);
Class<?> loadedClass = this.cl.loadClass(decrypt(className));
Object obj = "FooBar";
Object ret = loadedClass.getMethod(
decrypt(methodName),
String.class
).invoke(null, obj);
```]
]
}
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Obfuscation],
subtitle: [Deobfuscated],
foreground: {
place-fg(x: 44%, y: 55%, $ lr(}, size: #130pt) $ )
place-fg(x: 44%, y: 26%, $ lr(}, size: #110pt) $ )
arrow((385pt, -260pt), (450pt, -230pt))
arrow((385pt, -125pt), (450pt, -110pt))
},
)[
#show: yes-codly
#grid(
columns: (1fr, 4em, 1fr),
[
#scale(60%, reflow: true)[
```java
String DEX = "ZGV4CjA [...] EAAABEAwAA";
String className = "W5f3 [...] 3sls=";
String methodName = "n6WGYJzjDrUvR9cYljlNlw==";
ClassLoader cl = new InMemoryDexClassLoader(
ByteBuffer.wrap(Base64.decode(DEX, 2)),
Main.class.getClassLoader()
);
Class<?> loadedClass = this.cl.loadClass(decrypt(className));
Object obj = "FooBar";
Object ret = loadedClass.getMethod(
decrypt(methodName),
String.class
).invoke(null, obj);
```]
],
[],
[
#codly(
skips: ((3, 10),),
..default-codly
)
#scale(100%)[
```java
public class Foo {
public static String bar(String arg) {
}
}
```
```java
String ret = Foo.bar("FooBar");
```]
]
)
]
#slide(
title: [Analysis Methods],
foreground: eye-3(x: 3%, y: 5%)
)[
#set align(center+horizon)
#ico.analyse()
]
#counter("logical-slide").update( n => n - 1 )
#slide(
foreground: ghost-5(x: 10%, y: 7%)
)[
#set align(center+horizon)
#grid(
columns: (1fr, 1fr),
gutter: 2em,
[
== Dynamic Analysis
#item-by-item[
- Run the application
- *See* dynamically loaded bytecode
- *See* reflection calls
- Limited by code coverage
]
],
[
== Static Analysis
#item-by-item(start: 5)[
- Do *not* run the application
- *Not* limited by code coverage
- But only for the *code available*
//- Some values cannot be computed
]
],
//grid.cell(colspan: 2, uncover(8)[
// #text(size: 30pt)[Can we combine both?]
//]),
)
]
#slide(
title: [Which Tools are _really_ Working?],
foreground: eye-3(x: 3%, y: 5%)
)[
#set align(center+horizon)
#ico.analyse()
#h(10em)
#ico.machinery()
#place(
center+horizon,
dy: 120pt,
text(size: 400pt, fill: pirat-color.red)[?]
)
]
#slide(
title: [Problem Statement 1],
)[
#item-by-item[
- Which tool to use?
- Are they easy to install?
- Are they working?
]
#highlight-block(pb1-text)
]
#slide(
title: [Class Loading],
)[
#set align(center)
#show: yes-codly
#grid(
columns: (2fr, 1em, 1fr),
scale(70%, reflow: true)[
#codly(
highlights: (
(line: 1, start: 0, end: 11, fill: pirat-color.blue),
(line: 1, start: 22, end: 43, fill: pirat-color.blue),
(line: 3, start: 14, end: 27, fill: pirat-color.blue),
(line: 6, start: 32, end: 40, fill: pirat-color.blue),
),
..default-codly
)
```java
ClassLoader cl = new InMemoryDexClassLoader(
ByteBuffer.wrap(Base64.decode(DEX, 2)),
Main.class.getClassLoader()
);
Class<?> loadedClass = this.cl.loadClass(decrypt(className));
```
], [], uncover(2, scale(70%, reflow: true)[
#codly(
..default-codly
)
```java
class A {
public static void foo() {
B b = new B();
b.bar();
}
}
```
Where is the class loader?
])
)
]
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Class Loading],
)[
#item-by-item[
- Used to select classes implementation
- More complex than it looks
- Doubious documentation
- Not studied in the context of Android Static Analysis
]
#highlight-block(pb2-text)
]
#slide(
title: [Can we Deobfuscate?],
foreground: eye-3(x: 3%, y: 5%)
)[
#set align(center+horizon)
#analyse-apk
]
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Deobuscation],
)[
#set align(center+horizon)
#grid(
columns: (1fr, 1fr),
gutter: 2em,
[
== Dynamic Analysis
Easier to solve Dynamic Code Loading and Reflection Calls
],
[
== Static Analysis
Better code coverage
],
grid.cell(colspan: 2, uncover(2)[
#text(size: 30pt)[Can we combine both?]
]),
)
]
//#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Problem Statement 3],
)[
#item-by-item[
- Dynamic analysis is good against DCL and reflection
- Dynamic analysis is limited by code coverage
- Static analysis is not
- How to use existing tools without modifying them?
]
#highlight-block(pb3-text)
]
#for i in range(3) {
let stage = (
"static-only",
"static-vs-dyn",
"theseus",
).at(i)
if i != 0 { counter("logical-slide").update( n => n - 1 ) }
slide(
//foreground: rotate(30deg, smallcaps(text(fill: pirat-color.red, size: 50pt)[#stage]))
)[
#if i == 0 {
place(
top+left,
dx: 70%,
dy: 25%,
align(center, text(fill: pirat-color.red.darken(15%))[
*PB 1*: Static Analysis \ are the tools working?
]))
}
#if i == 1 {
place(
top+left,
dx: 35%,
dy: 25%,
align(center, text(fill: pirat-color.red.darken(15%))[
*PB 2*: Do Static Tools \ match Android Runtime?
]))
}
#if i == 2 {
place(
top+left,
dx: 5%,
dy: 5%,
align(center, text(fill: pirat-color.red.darken(15%))[
*PB 3*: Improve any Static Tools \ with dynamic analysis?
]))
}
#set align(horizon+center)
#theseus-outline(stage: stage)
]
}
#new-section-slide([Tool Reusability])
#slide(
title: [State of the Art],
)[
#set list(spacing: 0.5em)
Li #etal (2017):
#v(0pt)
#item-by-item[
- Systematic literature review for Android static analysis (38 tools)
- Lists 31 open-sourced tools
- Does not test the tools
]
#uncover("4-")[Reaves #etal (2016):]
#v(0pt)
#item-by-item(start: 4)[
- Tests 7 Android analysis tools
- Tests analysing 16 real-world applications
- Raises concerns about reusability and analysis of real-world applications
]
]
#slide(
title: [Methodology]
)[
#set align(center+horizon)
#show figure.caption: none
#scale(100%, get_figure(<fig:rasta-methodo-collection>))
#v(1em)
#text(size: 25pt)[22 tools selected, 2 we could not package]
/*
#stack(dir: ltr,
scale(40%, reflow: true, get_figure(<tab:rasta-tools>)),
scale(55%, reflow: true, get_figure(<tab:rasta-sources>)),
)*/
]
#slide(
title: [Methodology: RASTA],
foreground: place(
bottom + left,
dx: 88%,
dy: -63%,
)[
#set align(center+horizon)
#set text(size: 15pt)
62 525 APKs #v(-1.5em) from #v(-1.5em) 2010 to 2023
]
)[
#set align(center+horizon)
#show figure.caption: none
#scale(90%, get_figure(<fig:rasta-overview>))
#text(size: 25pt)[We check if the results *exist* after running a tool]
]
#slide(
title: [Results],
foreground: ghost-2(x: 97%, y: 10%)
)[
#set align(center+horizon)
#show figure.caption: none
#scale(100%, get_figure(<fig:rasta-exit>))
]
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Results],
foreground: {
ghost-2(x: 97%, y: 10%)
let x_0 = 112pt
let y_0 = -116pt
let w = 21pt
let h = 236pt
let dx = 33.3
let h_legend = 60pt
for i in range(20) {
let color = if i in (2, 4, 6, 7, 8, 9, 14, 16, 18, 19) {
white.transparentize(100%)
} else {
white.transparentize(10%)
}
if i == 1 {
place(
bottom + left,
dx: x_0 + i*dx*1pt + w/2,
dy: y_0,
rect(
width: w/2,
height: h_legend,
//stroke: red,
fill: color,
)
)
}
let (y_0, h) = if i in (0, 1) {
(y_0 - h_legend, h - h_legend)
} else {
(y_0, h)
}
place(
bottom + left,
dx: x_0 + i*dx*1pt,
dy: y_0,
rect(
width: w,
height: h,
//stroke: red,
fill: color,
)
)
}
place(bottom + left, line(
start: (x_0 - 20pt, y_0 - h/2),
end: (x_0 + dx * 20 * 1pt, y_0 - h/2),
stroke: pirat-color.red + 3pt
))
}
)[
#set align(center+horizon)
#show figure.caption: none
#scale(100%, get_figure(<fig:rasta-exit>))
]
/*
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Results],
foreground: {
ghost-2(x: 97%, y: 10%)
let x_0 = 112pt
let y_0 = -117pt
let w = 21pt
let h = 235pt
let dx = 33.3
for i in range(20) {
let color = if i in (3, 10) {
white.transparentize(100%)
} else {
white.transparentize(10%)
}
place(
bottom + left,
dx: x_0 + i*dx*1pt,
dy: y_0,
rect(
width: w,
height: h,
//stroke: red,
fill: color,
)
)
}
place(bottom + left, line(
start: (x_0 - 20pt, y_0 - h/2),
end: (x_0 + dx * 20 * 1pt, y_0 - h/2),
stroke: pirat-color.red + 3pt
))
}
)[
#set align(center+horizon)
#show figure.caption: none
#scale(100%, get_figure(<fig:rasta-exit>))
]
#slide(
title: [Results over Time],
)[
#set align(center+horizon)
#show figure.caption: none
#scale(150%, get_figure(<fig:rasta-exit-evolution-java>))
]
#slide(
title: [Bytecode Size],
)[
#set align(center+horizon)
#show figure.caption: none
#scale(120%, get_figure(<fig:rasta-rate-evolution-java-2022>))
#text(size: 22pt)[Finishing rate as a function of the bytecode size, for APKs discovered in 2022]
]
*/
#slide(
title: [PB1: Conclusion],
)[
#v(1fr)
//#set align(center)
#item-by-item[
- Over *22* tools, *10 are usable*
- Newer applications are harder to analyse
- Applications with *more bytecode* are *harder* to analyse
- Applications targetting more recent versions of Android are harder to analyse
- Confirms and *extends Reaves #etal*
- Docker containers for tool *released*
]
#v(1fr)
#align(center, text(fill: pirat-color.blue.darken(30%))[International Conference on Software and Systems Reuse (ICSR 2024)])
#v(1em)
]
#new-section-slide([Class Shadowing])
#slide(
title: [Class Loading],
)[
#set align(center)
#show: yes-codly
#grid(
columns: (2fr, 1em, 1fr),
scale(70%, reflow: true)[
#codly(
highlights: (/*
(line: 1, start: 0, end: 11, fill: pirat-color.blue),
(line: 1, start: 22, end: 43, fill: pirat-color.blue),
(line: 3, start: 14, end: 27, fill: pirat-color.blue),
(line: 6, start: 32, end: 40, fill: pirat-color.blue),
*/),
..default-codly
)
```java
ClassLoader cl = new InMemoryDexClassLoader(
ByteBuffer.wrap(Base64.decode(DEX, 2)),
Main.class.getClassLoader()
);
Class<?> loadedClass = this.cl.loadClass(decrypt(className));
```
], [], scale(70%, reflow: true)[
#codly(
..default-codly
)
```java
class A {
public static void foo() {
B b = new B();
b.bar();
}
}
```
]
)
]
#slide(
title: [State of the Art],
//foreground: rotate(30deg, text(fill: pirat-color.red, size: 50pt)[State of the Art])
)[
#set list(spacing: 3em)
#item-by-item[
- Previous contributions focus on Java runtime (#eg Gong 1998)
- Android related contributions focus on Dynamic Code Loading (#eg Zhang #etal 2015)
]
]
#for i in range(5) {
let strong-at(i, str-idxs, body) = {
if i in str-idxs {
strong(body)
} else {
body
}
}
slide(
title: [Android Ecosystem],
foreground: {
let c = white.transparentize(10%)
let place-rect(x, y, w, h) = place-fg(
x: x, y: y,
rect(
width: w,
height: h,
stroke: c,
fill: c,
)
)
// phone
if i in () {
place-rect(15%, 15%, 16.5%, 55%)
}
// Platform Classes
if i in (1,) {
place-rect(18%, 38%+4pt, 12%, 15%)
place-rect(21%, 54%, 8%, 5%)
}
// API access
if i in (1,4) {
place-rect(26%-4pt, 31%+2.3pt, 5%, 7%+2pt)
}
// APK file
if i in (2,3, 4) {
place-rect(19%+3pt, 22%, 9%, 9%+1pt)
place-rect(21%+5pt, 32%, 4%, 3%)
}
// doc
if i in (1,2,4) {
place-rect(31.5%, 45%, 30%, 25%)
}
// dev
if i in (2,4) {
place-rect(39.5%, 15%, 22%, 30%)
}
// Dev SDK
if i in (1,) {
place-rect(49.6%, 18%, 10%, 15%)
}
// Dev classes
if i in (3,) {
place-rect(41.5%-1pt, 18%, 8%+2pt, 10%)
}
// compil
if i in (2,3, 4) {
place-rect(31.5%, 22%, 8%, 5%)
}
}
)[
#set align(center+horizon)
#show figure.caption: none
#grid(
columns: (3fr, 1fr),
scale(reflow: true, get_figure(<fig:cl-archisdk>)),
[
#set align(left)
#set text(size: 20pt)
#set list(marker: [-])
=== Types of classes:
- #strong-at(i, (1,))[APK Classes]
- #strong-at(i, (2,))[Platform Classes]
- #strong-at(i, (3,))[SDK Classes]
- #strong-at(i, (4,))[Hidden APIs]
]
)
]
}
#slide(
title: [Android ClassLoaders],
foreground: {
let stroke = black + 5pt
let y0 = -177pt
let y1 = -270pt
let x0 = 250pt
let x1 = 272pt
let x2 = 570pt
let x3 = 600pt
arrow(
(x0, y0),
(x0, y1),
(x1, y1),
stroke: stroke
)
arrow(
(x3, y0),
(x3, y1),
(x2, y1),
stroke: stroke
)
place-fg(x: x0 - 2.5em, y: (y0+y1)/2)[Delegate]
place-fg(x: x3 + 2.5em, y: (y0+y1)/2)[Delegate]
}, {
let stroke = black + 3pt
let width = 13em
let height = 4.5em
set align(center+horizon)
set rect(width: 250pt, height: 75pt, radius: 20pt, inset: 20pt)
v(1fr)
rect(
stroke: stroke,
height: height,
width: width,
{
[*Boot Class Loader*]
v(-0.5em)
line(length: 80%)
v(-0.5em)
[_Platform Classes_]
}
)
v(1fr)
stack(
dir: ltr,
1fr,
rect(
stroke: stroke,
height: height,
width: width,
{
[*System Class Loader*]
v(-0.5em)
line(length: 80%)
v(-0.5em)
sym.emptyset
}
),
1fr,
rect(
stroke: stroke,
height: height,
width: width,
{
[*APK Class Loader*]
v(-0.5em)
line(length: 80%)
v(-0.5em)
[_APK Classes_]
}
),
1fr,
)
v(1fr)
})
#slide(
title: [MultiDex]
)[
#set align(center + horizon)
#only(1)[
#apk-block[
#set align(left+top)
=== `app.apk`
#line(length: 30%)
```
AndroidManifest.xml
resources.arsc
META-INF/
res/
classes.dex
```
]
]
#only(2)[
#apk-block[
#set align(left+top)
=== `app.apk`
#line(length: 50%)
#stack(dir: ltr,
```
AndroidManifest.xml
resources.arsc
META-INF/
res/
classes.dex
```,
h(2em),[
```
classes2.dex
classes3.dex
```
]
)
]
]
#only(3)[
#apk-block[
#set align(left+top)
=== `app.apk`
#line(length: 75%)
#stack(dir:ltr,
```
AndroidManifest.xml
resources.arsc
META-INF/
res/
classes.dex
classes2.dex
classes3.dex
```, h(2em),
```
classes4.dex
classes5.dex
classes6.dex
classes7.dex
classes8.dex
classes9.dex
classes10.dex
```, h(2em),
```
classes11.dex
classes12.dex
classes13.dex
classes14.dex
classes15.dex
classes16.dex
...
```
)
]
#ghost-4(x: 2%, y: 2%, mirror: true)
]
#only(4)[
#grid(
columns: (1fr, 1fr),
apk-block[
#set align(left+top)
=== `app.apk`
#line(length: 30%)
#show "classes.dex": set text(fill: pirat-color.red.darken(10%))
#show "classes2.dex": set text(fill: pirat-color.red.darken(10%))
#show "classes3.dex": set text(fill: pirat-color.red.darken(10%))
```
AndroidManifest.xml
resources.arsc
META-INF/
res/
classes.dex
classes2.dex
classes3.dex
```
],
yes-codly[
#scale(100%, reflow: true,
```python
def get_dex_name(index: int):
if index == 0:
return "classes.dex"
else:
return f"classes{index+1}.dex"
```)
]
)
]
]
#for i in range(3) {
counter("logical-slide").update( n => n - 1 )
show table.cell: c => {
if c.y >= 1 and ((i < 2 and c.x == 2) or (i < 1 and c.x == 1)) {
[]
} else {
c
}
}
slide(
title: [Self-Shadowing],
)[
#grid(
columns: (2fr, 0.5em, 3fr),
yes-codly[
#set list(spacing: 0.5em)
#scale(100%, reflow: true,
```python
def get_dex_name(index: int):
if index == 0:
return "classes.dex"
else:
return f"classes{index+1}.dex"
```)
#align(bottom, text(size: 0.75em)[#sym.star estimation, non-deterministic])
], [], {
set text(size: 24pt)
show table.cell: set align(center+horizon)
let r = text.with(fill: pirat-color.red.darken(10%))
table(
columns: (1fr, 1fr, 1fr),
//inset: (x,y) => if y == 0 { 10pt } else { 3pt },
stroke: none,
table.header(
[Android],
table.vline(),
[Soot],
table.vline(),
[Androguard#super[#sym.star]]
),
table.hline(),
[`classes.dex`], [`classes.dex`], [`classes10.dex`],
..if (i != 0) {(
[], r[`classes1.dex`], [],
[], r[`classes10.dex`], [],
)},
[`classes2.dex`], [`classes2.dex`], [`classes9.dex`],
[`classes3.dex`], [`classes3.dex`], [`classes8.dex`],
table.cell(colspan: 3, inset: -3pt)[...],
[`classes9.dex`], [`classes9.dex`], [`classes2.dex`],
[`classes10.dex`], [], [`classes1.dex`],
strike[`classes1.dex`], [], [`classes.dex`],
)}
)
]
}
#for i in range(4) {
if i != 0 { counter("logical-slide").update( n => n - 1 ) }
slide(
title: [Shadow Attacks]
)[
#if i in (0, 4) {
codly(
..default-codly
)
} else if i == 2 {
codly(
highlighted-lines: (7, 8),
..default-codly
)
} else if i == 1 {
codly(
highlighted-lines: (1, 2, 3, 4, 5, 10, 11, 12, 13, 14, 15),
..default-codly
)
}
#let partial-hide(
i,
hidden: (),
partial: (),
body
) = {
if i in hidden {
hide(body)
} else if i in partial {
set text(fill: luma(200))
body
} else {
body
}
}
#set align(center+horizon)
#grid(
columns: (1fr, 1fr),
column-gutter: 2em,
yes-codly[
#scale(55%, reflow: true,
```python
def get_dex_name(index: int):
if index == 0:
return "classes.dex"
else:
return f"classes{index+1}.dex"
def load_class(class_name: str):
if is_platform_class(class_name):
return boot_cl.load(class_name)
else:
index = 0
dex_file = get_dex_name(index)
while exists_in_apk(dex_file) and \
class_name not in classes_of(dex_file):
index += 1
dex_file = get_dex_name(index)
if file_exists_in_apk(dex_file):
return load_from_file(dex_file, class_name)
else:
raise ClassNotFoundError()
```)
#v(-1em)
#smallcaps(text(size: 15pt)[Android Class Loading Algorithm])
], [
#set align(left)
#set text(size: 18pt)
#partial-hide(i, hidden: (0,), partial: (2,))[
=== Self Shadowing
Trick the tool into using an APK class instead of another APK class
]
#partial-hide(i, hidden: (0, 1))[
=== SDK shadowing
Trick the tool into using an APK class instead of an SDK class
]
#partial-hide(i, hidden: (0, 1))[
=== Hidden API shadowing
Trick the tool into using an APK class instead of an hidden API class
]
]
)
]
}
#slide(
title: [Impact on Tools],
)[
#import "4_class_loader/X_var.typ": warn, ok, ko
#set align(center+horizon)
#show figure: it => {
set align(left)
show table: set align(center+horizon)
it
}
#figure({
table(
columns: 5,
stroke: none,
align:(left+horizon, center+horizon, center+horizon, center+horizon, center+horizon),
table.hline(),
table.header(
table.cell(colspan: 5, inset: 3pt)[],
table.cell(rowspan: 2)[Tool],
table.cell(rowspan: 2)[Version],
table.vline(end: 3),
table.vline(start: 4),
table.cell(colspan: 3)[Shadow Attack],
[Self], [SDK], [Hidden],
),
table.cell(colspan: 5, inset: 3pt)[],
table.hline(),
table.cell(colspan: 5, inset: 3pt)[],
[Jadx], [1.5.0], [#warn], [#ok], [#ok],
[Apktool], [2.9.3], [#warn], [#ok], [#ok],
[Androguard], [4.1.2], [#warn], [#ok], [#ok],
[Flowdroid], [2.13.0], [#ok], [#ko], [#ok],
table.cell(colspan: 5, inset: 3pt)[],
table.hline(),
)
[#ok: attack sucessful \ #warn: sucessful but producing warning or can be seen by the reverser \ #ko: attack failed]
},
)
]
#counter("logical-slide").update( n => n - 1 )
#slide(
title: [Impact on Tools],
)[
#import "4_class_loader/X_var.typ": warn, ok, ko
#set align(center+horizon)
#show figure: it => {
set align(left)
show table: set align(center+horizon)
it
}
#figure({
table(
columns: 5,
stroke: none,
align:(left+horizon, center+horizon, center+horizon, center+horizon, center+horizon),
table.hline(),
table.header(
table.cell(colspan: 5, inset: 3pt)[],
table.cell(rowspan: 2)[Tool],
table.cell(rowspan: 2, hide[Version]),
table.vline(end: 3),
table.vline(start: 4),
table.cell(colspan: 3)[Shadow Attack],
[Self], [SDK], [Hidden],
),
table.cell(colspan: 5, inset: 3pt)[],
table.hline(),
table.cell(colspan: 5, inset: 3pt)[],
[Jadx], hide[1.5.0], text(fill: orange)[#warn], [#ok], [#ok],
[Apktool], hide[2.9.3], [#warn], [#ok], [#ok],
[Androguard], hide[4.1.2], [#ko], [#ok], [#ok],
[Flowdroid], hide[2.13.0], [#ko], [#ko], [#ok],
table.cell(colspan: 5, inset: 3pt)[],
table.hline(),
)
[#ok: attack sucessful \ #warn: sucessful but producing warning or can be seen by the reverser \ #ko: attack failed]
},
)
#place(right+top)[
#set align(left)
#align(center)[Pull Requests:]
#v(-1em)
#link("https://github.com/androguard/androguard/pull/1149")[androguard/pull/1149] \
#link("https://github.com/soot-oss/soot/pull/2211")[soot/pull/2211] (#text(fill: green)[merged])\
#link("https://github.com/skylot/jadx/pull/2702")[jadx/pull/2702] (#text(fill: orange)[\~merged])
]
]
#slide(
title: [Androguard on Toy Malware],
foreground: eye-4(x: 97%, y: 85%, mirror: true)
)[
#set align(center+horizon)
//#show image: scale.with(250%)
//#set figure(gap: 4em)
//#v(3em)
#grid(
columns: (1fr, 1fr),
//get_figure(<fig:cl-andro_non_obf_cg>),
figure(
image("slides/imgs/call_graph_expected.svg", width: 100%),
caption: [Expected],
supplement: none,
),
//get_figure(<fig:cl-andro_obf_cg>)
figure(
image("slides/imgs/call_graph_obf.svg", width: 100%),
caption: [Computed],
supplement: none,
)
)
]
#for i in range(3) {
if i != 0 { counter("logical-slide").update( n => n - 1 ) }
slide(
title: [In the Wild: 49 975 APKs],
)[
#set align(center+horizon)
// TODO: Simplifier table, mettre nb apk dans titre
// enlever SDK et 1ere partie 100%
//#scale(90%, reflow: true, get_figure(<tab:cl-shadow>))
#import "4_class_loader/X_var.typ": scan_50k, scan_only_shadow
#import "lib.typ": num
#let nb_col = 7
#scale(90%, reflow: true, table(
columns: nb_col,
stroke: none,
align: center+horizon,
fill: (x, y) => {
if (
i == 1 and (x, y) in ((1, 4), (2, 4), (4, 4), (6, 4))
) or (
i == 2 and (x, y) in ((1, 5), (2, 5))
) {
highlight-color
} else {
none
}
},
inset: (x: 0% + 7pt, y: 0% + 5pt),
table.hline(),
table.header(
table.cell(colspan: nb_col, inset: 3pt)[],
table.cell(rowspan: 2, eye-2(x: 35pt, y: 30pt, height: 70pt)),
table.vline(end: 3),
table.vline(start: 4),
table.cell(colspan: 3)[*Number of apps*],
table.vline(end: 3),
table.vline(start: 4),
table.cell(colspan: 2)[*Nb Shadow Classes*],
table.vline(end: 3),
table.vline(start: 4),
table.cell(rowspan: 2)[*Identical Code*],
[], [%], [% malware],
[Average], [Median],
),
table.hline(),
table.cell(colspan: nb_col, inset: 3pt)[],
..scan_only_shadow.map(e => (
[*#e.method*],
num(e.nbapp), [#e.ratioapp%], [#e.ratiomal%],
num(e.avgshadow), num(e.median),
[#e.id%]
)).flatten(),
table.cell(colspan: nb_col, inset: 3pt)[],
table.hline(),
))
]
}
#slide(
title: [PB2: Conclusion]
)[
#v(1fr)
#item-by-item[
- We modeled the class loading algorithm
- #h(2em) Static Analysis Tools did not
- We introduced obfuscation techniques based on this model
- We did not find deliberate shadow attacks
- #h(2em) Ambiguous cases exists in the wild
]
#v(1fr)
#align(center, text(fill: pirat-color.blue.darken(30%))[Digital Threats: Research and Practice, vol. 6 (3), 2025])
#v(0.5em)
]
#new-section-slide([The Application of Theseus])
// TODO put everywhere Theseus Transformeur
#slide(
title: [Dexhunter: Zang #etal (2015)],
)[
#align(center+horizon, dexhunter-outline(
small_icon_size: 75pt,
big_icon_size: 200pt,
))
]
#slide(
title: [DroidRA: Li #etal (2016)],
)[
#align(center+horizon, droidra-outline(
small_icon_size: 75pt,
big_icon_size: 150pt,
))
]
#slide(
title: [Theseus: Overview],
)[
#set align(center+horizon)
#only(1, theseus-outline(stage: "theseus-no-static", labels: true))
#only(2, theseus-outline(stage: "theseus", labels: true))
]
#slide(
title: [Dynamic Analysis],
)[
- Frida: intercepts method calls
#v(2em)
#uncover("2-")[
- Android Emulator: runs on computer/server
- Grodd Runner: clicks buttons (Abraham #etal, 2015)
]
#v(2em)
#uncover(3)[
- Phone with adb enable: actual hardware
- Human: intelligent button clicker
]
]
#slide(
title: [Transformation: Dynamic Code Loading],
foreground: {
ghost-6(x: 80%, y: 15%, mirror: true)
align(horizon+center, line(length: 80%, stroke: (thickness: 3pt, dash: (10pt, 5pt))))
place(horizon+right, dx: -1em)[
Collected at Runtime
Patched Application
]
}
)[
// Split schema: observed dyn code loaded / new apk
#set align(center+horizon)
#show figure.caption: none
#show image: box.with(width: 58%)
#get_figure(<fig:th-inserting-dex>)
]
#for i in range(7) {
if i != 0 { counter("logical-slide").update( n => n - 1 ) }
slide(
title: [Reflection Transformation]
)[
#show: yes-codly
#set align(center+horizon)
#scale(90%, reflow: true, grid(
rows: (3em, 2em, 12em),
{
if i == 1 {
codly(
offset: 4,
highlighted-lines: (5,),
..default-codly
)
} else if i == 3 {
codly(
offset: 4,
highlights: (
(line: 6, start: 27, end: 48, fill: pirat-color.blue),
),
..default-codly
)
} else {
codly(
offset: 4,
..default-codly
)
}
```java
Method mth = getMethod();
String retData = (String) mth.invoke(obj, args);
```
},
{
v(1fr)
align(center+horizon, sym.arrow.b.stroked)
v(1fr)
},
{
if i == 3 {
codly(
offset: 4,
highlights: (
(line: 10, start: 12, end: 33, fill: pirat-color.blue),
),
..default-codly
)
} else if i == 4 {
codly(
offset: 4,
highlights: (
(line: 7, start: 5, end: 43, fill: pirat-color.blue),
(line: 8, start: 21, end: 31, fill: pirat-color.blue),
(line: 8, start: 38, end: 45, fill: pirat-color.blue),
(line: 8, start: 47, end: 54, fill: pirat-color.blue),
),
..default-codly
)
} else if i == 5 {
codly(
offset: 4,
highlights: (
(line: 6, start: 0, end: 13, fill: pirat-color.blue),
(line: 8, start: 3, end: 8, fill: pirat-color.blue),
(line: 10, start: 3, end: 8, fill: pirat-color.blue),
(line: 12, start: 18+9, end: 32, fill: pirat-color.blue),
),
..default-codly
)
} else if i == 6 {
codly(
offset: 4,
highlights: (
(line: 8, start: 12, end: 19, fill: pirat-color.blue),
(line: 12, start: 0, end: 25, fill: pirat-color.blue),
),
..default-codly
)
} else if i < 2 {
codly(
offset: 5,
..default-codly
)
} else {
codly(
offset: 4,
..default-codly
)
}
v(1fr)
if i in (0, 1) {
```java
String retData = obj.myMethod((String)args[0]);
```
} else {
```java
Method mth = getMethod();
Object objRet;
if (T.check_is_reflectee_mymethod_XXXX(mth)) {
objRet = (Object)((Reflectee) obj).myMethod((String)args[0]);
} else {
objRet = mth.invoke(obj, args);
}
String retData = (String) objRet;
```
}
v(1fr)
}
))
]
}
#slide(
foreground: {
}
)[
#set align(center+horizon)
#theseus-outline(labels: true)
#place(
bottom+left,
dx: -25pt,
dy: -335pt,
box({
for i in range(3) {
place(
dx: i*10pt,
dy: i*10pt,
ico.apk(height: 60pt, fill: red)
)
}
set text(weight: "semibold", fill: luma(30%))
place(dy: -1.5em)[*RASTA*]
})
)
#arrow(
stroke: 6pt + black,
(
4pt,
-245pt
),
(
4pt,
-195pt,
),
)
]
#counter("logical-slide").update( n => n - 1 )
#slide(
foreground: {
place(
bottom+left,
dx: 200pt,
dy: -50pt,
ellipse(
width: 100pt,
height: 310pt,
stroke: 10pt + pirat-color.red,
)
)
place(
bottom+left,
dx: 140pt,
dy: -370pt,
text(weight: "bold", fill: pirat-color.red, size: 30pt)[Dynamic Results]
)
}
)[
#set align(center+horizon)
#theseus-outline(labels: true)
#place(
bottom+left,
dx: -25pt,
dy: -335pt,
box({
for i in range(3) {
place(
dx: i*10pt,
dy: i*10pt,
ico.apk(height: 60pt, fill: red)
)
}
set text(weight: "semibold", fill: luma(30%))
place(dy: -1.5em)[*RASTA*]
})
)
#arrow(
stroke: 6pt + black,
(
4pt,
-245pt
),
(
4pt,
-195pt,
),
)
]
#for i in range(3) {
let hide-if-not-2(i, body) = if i == 2 { body } else { hide(body) }
slide(
title: [Dynamic Analysis],
foreground: ghost-1(x: 97%, y: 10%, height: 70pt)
)[
#import "5_theseus/X_var.typ": *
#import "lib.typ": num, mypercent
#set align(center+horizon)
#let nb_col = 4
#stack(dir: ltr, table(
columns: nb_col,
stroke: none,
fill: (x, y) => {
if (
(x == 2 and y > 2 and i == 1) or
((x, y) in ((1, 6), (1, 7)) and i == 2)
) {
highlight-color
} else {
none
}
},
inset: 7pt,
align: center+horizon,
table.header(
table.hline(),
table.cell(colspan: nb_col, inset: 2pt)[],
table.cell(rowspan: 2)[],
table.cell(rowspan: 2)[nb apk],
table.vline(end: 3),
table.vline(start: 4),
table.cell(colspan: 2, inset: (bottom: 0pt))[activities visited],
[0], [$>= 1$],
),
table.cell(colspan: nb_col, inset: 2pt)[],
table.hline(),
table.cell(colspan: nb_col, inset: 2pt)[],
[All], num(dyn_res.all.nb), num(dyn_res.all.z_act_visited), num(dyn_res.all.nz_act_visited),
[With Reflection], num(dyn_res.reflection.nb), num(dyn_res.reflection.z_act_visited), num(dyn_res.reflection.nz_act_visited),
[With Code Loading], num(dyn_res.code_loading.nb), num(dyn_res.code_loading.z_act_visited), num(dyn_res.code_loading.nz_act_visited),
table.cell(colspan: nb_col, inset: 2pt)[],
table.hline(),
), h(3em), hide-if-not-2(i)[
#set list(spacing: 2em)
- *Reflection* detected on \ #strong(mypercent(dyn_res.reflection.nb, dyn_res.all.nb)) of APKs tested
- *DLC* detected on \ #strong(mypercent(dyn_res.code_loading.nb, dyn_res.all.nb)) of APKs tested
])
]
}
#slide(
title: [Collected Bytecode],
foreground: eye-1(x: 5%, y: 70%, height: 70pt)
)[
/*
* Explications, give example of Google Services:
* whithout service: does not work
* with service: service recognize the app as malware and block it
*/
#set align(center+horizon)
#show link.where(dest: <acr-apk>): it => it.body
#show link.where(dest: <acr-dex>): it => it.body
#show figure.caption: none
#set table(
fill: (x, y) => {
if (
x == 4 and y > 2
) {
highlight-color
} else {
none
}
}
)
#scale(90%, reflow: true, get_figure(<tab:th-bytecode-hashes>))
]
#slide(
foreground: {
arrow(
stroke: 10pt + pirat-color.red,
(
770pt,
-250pt,
),
(
620pt,
-350pt,
)
)
arrow(
stroke: 10pt + pirat-color.red,
(
620pt,
-350pt,
),
(
770pt,
-250pt,
)
)
place(
bottom+left,
dx: 680pt,
dy: -350pt,
text(weight: "bold", fill: pirat-color.red, size: 30pt)[#set align(center); Improved \ Results?]
)
}
)[
#set align(center+horizon)
#theseus-outline(stage: "theseus-vs-static", labels: true)
]
#for i in range(3) {
if i != 0 {counter("logical-slide").update( n => n - 1 )}
slide(
title: [Added Method Calls],
)[
#set align(center+horizon)
//#show link.where(dest: <acr-apk>): it => it.body
//#show link.where(dest: <acr-dex>): it => it.body
//#show figure.caption: none
//#scale(90%, reflow: true, get_figure(<tab:th-compare-cg>))
#import "5_theseus/X_var.typ": compared_callgraph
#import "lib.typ": num
#let nb_col = 3
#table(
columns: (1fr, 1.1fr, 1fr),
align: center+horizon,
stroke: none,
fill: (x, y) => if (
i == 1 and (x == 1 and y > 1 and y != 9)
) or (
i == 2 and (x == 2 and y > 1)
){
highlight-color
} else {
none
},
table.hline(),
table.header(
//[SHA 256], [Original CG edges], [New CG edges], [Edges added], [Reflection edges added],
table.cell(rowspan: 2)[APK SHA 256],
table.cell(colspan: nb_col - 1)[Number of Call Graph edges], [Diff (Total After - Before)], [Replaced Reflection],
),
table.hline(),
..compared_callgraph.map(
(e) => (
[#lower(e.sha256).slice(0, 10)...],
num(e.added),
num(e.added_ref_only)
)).flatten(),
[#lower("5D2CD1D10ABE9B1E8D93C4C339A6B4E3D75895DE1FC49E248248B5F0B05EF1CE").slice(0, 10)...], table.cell(colspan: nb_col - 1)[_Transformation Crashed_],
table.hline(),
)
]
}
/*
#slide(
title: [Toy Example: New Call Graph],
foreground: ghost-3(x: 93%, y: 10%)
)[
// TODO: Légende des couleurs
#import "@preview/diagraph:0.3.5": render
#set align(center+horizon)
#scale(47%, box(render(
read("5_theseus/figs/patched_main_main.dot"),
//width: 100%,
labels: (name) => { move(dy: -7pt, scale(140%, text(size: 10pt, weight: "bold", name))) }
)))
]
*/
#slide(
title: [Impact on Finishing Rate],
foreground: {
let strk = 3pt + pirat-color.blue
import "slides/icons.typ": arrow
arrow((290pt, -335pt), (320pt, -310pt), strk: strk)
arrow((400pt, -335pt), (340pt, -310pt), strk: strk)
}
)[
#set align(center+horizon)
#show figure.caption: none
#move(dx: -70pt)[Original #h(2em) Transformed]
/*
* JFL bet on a question about SAAF
*/
#box(width: 80%, get_figure(<fig:th-status-npatched-vs-patched>))
]
#slide(
title: [PB3: Conclusion],
)[
#set list(indent: 1em)
#item-by-item[
- We can statically *analyse APKs with reflection and dynamic code loading* with our method
- Our dynamic analysis is questionable
- The dynamically loaded bytecode we intercepted is *mainly telemetry and advertisement* related
]
#uncover("4-", underline[Software Contributions:])
#item-by-item(start: 5)[
- *Androscalpel*: rust crate to *parse, modify and generate bytecode*
- *Theseus*: tool implementing the method presented here
]
]
#new-section-slide([Conclusion])
#slide(title: [Experimentations])[
#import "lib.typ": num
#set align(center+horizon)
#table(
columns: 4,
inset: 0.5em,
stroke: (x, y) => (
y: if y != 0 and y != 5 and (x, y) != (0, 3) { 1pt } else { none },
left: if x != 0 { 1pt } else { none },
),
table.header[][Experiment][Number of APKs][Time],
[RASTA], [20 static analyses], num(62525), [2 months],
[Class Loading], [1 static analysis], num(49975), [1 week],
table.cell(rowspan: 3)[Theseus],
[dynamic analysis], num(4957), [1 week],
[patching], num(4748), [2 days],
[18 static analyses], num(8955), [2 months],
)
]
#slide[
We showed that:
#item-by-item[
- Most Static analysis tools are no longer usable after a few years.
The size of the application seems to be the most significant factor.
- Android behaviour is complex and not well known.
In the specific case of class loading, we showed that state-of-the-art tools do not match Android, leading to invalid analyses.
- APKs can be augmented with instrumentation to improve further analyses with any other tools.
- Also, dynamic analysis is still very much not trivial.
]
]
#slide(
title: [Future Works],
foreground: {
ghost-3(x: 80%, y: 70%)
ghost-6(x: 20%, y: 30%)
ghost-7(x: 70%, y: 20%)
}
)[
#set align(center+horizon)
A lot of engineering, preferably spearheaded by Google.
]
//- Benchmark of APKs to evaluate finishing rate
//- Make tools reusing sources from Android Open Source Project
//- Require developpers to provide high coverage tests inputs with the APKs
#for i in range(4) {
let delta_x = 50pt
let myell = ellipse.with(
stroke: green + 5pt,
outset: 5pt,
fill: white,
)
let myell_red = ellipse.with(
stroke: red + 5pt,
outset: 0pt,
fill: white,
)
let myline(start, end, color: green) = {
place(left+top, dx: delta_x, line(
start: start, end: end,
stroke: color + 5pt,
))
}
let myplace(pos, body) = context {
let size = measure(body);
place(
left+top,
dx: pos.at(0) - size.width / 2 + delta_x,
dy: pos.at(1) - size.height / 2,
body
)
}
let pos_apk = (300pt, -100pt)
let pos_android = (400pt, 0pt)
let pos_aosp = (300pt, 100pt)
let pos_dataset = (100pt, -50pt)
let pos_tools = (100pt, 50pt)
let pos_devs = (500pt, -100pt)
let pos_google = (500pt, 100pt)
let pos_test = (600pt, 0pt)
let circle_apk = myell[APK]
let circle_android = myell[Android]
let circle_aosp = myell[AOSP]
let circle_dataset = myell[Data Set]
let circle_tools = myell[Analysis tools]
let circle_devs = myell[Developers]
let circle_google = myell[Google]
let circle_test = myell_red(align(center)[Public \ Dynamic Tests])
counter("logical-slide").update( n => n - 1 )
slide(
title: [Future Works], {
if i >= 1 {
let dx = 25pt
let cdx = 30pt
let cdy = 30pt
let st = (
pos_dataset.at(0) - dx,
pos_dataset.at(1)
)
let en = (
pos_tools.at(0) - dx,
pos_tools.at(1)
)
let c1 = (
st.at(0) - cdx,
(st.at(1) + en.at(1)) / 2 - cdy
)
let c2 = (
st.at(0) - cdx,
(st.at(1) + en.at(1)) / 2 + cdy
)
place(left+top, dx: delta_x, curve(
stroke: red + 5pt,
curve.move(st),
curve.cubic(c1, c2, en)
))
place(
left+top,
dx: delta_x + st.at(0) - 150pt,
dy: (st.at(1) + en.at(1)) / 2 - 30pt,
text(fill: red)[
RASTA-like \
benchmark
]
)
}
if i >= 2 {
let cdx = 50pt
let cdy = 0pt
let st = (
pos_tools.at(0),
pos_tools.at(1)
)
let en = (
pos_aosp.at(0),
pos_aosp.at(1)
)
let c = (
(st.at(0) + en.at(0)) / 2 - 40pt,
(st.at(1) + en.at(1)) / 2 + 50pt
)
let c1 = (
c.at(0) - cdx,
c.at(1) - cdy
)
let c2 = (
c.at(0) + cdx,
c.at(1) + cdy
)
place(left+top, dx: delta_x, curve(
stroke: red + 5pt,
curve.move(st),
curve.cubic(c1, c2, en)
))
place(
left+top,
dx: delta_x + c.at(0) - 100pt,
dy: c.at(1),
text(fill: red)[
Use Android Code
]
)
}
if i >= 3 {
myline(pos_apk, pos_test, color: red)
myline(pos_android, pos_test, color: red)
myline(pos_devs, pos_test, color: red)
myline(pos_tools, pos_test, color: red)
myline(pos_google, pos_test, color: red)
myplace(pos_test, circle_test)
place(
left+top,
dx: delta_x + pos_test.at(0) - 10pt,
dy: pos_test.at(1) + 45pt,
text(fill: red, align(center)[
Require \
high-coverage \
tests inputs \
for some \
permissions
])
)
}
myline(pos_apk, pos_android)
myline(pos_aosp, pos_android)
myline(pos_apk, pos_dataset)
myline(pos_dataset, pos_tools)
myline(pos_devs, pos_apk)
myline(pos_google, pos_android)
myline(pos_aosp, pos_google)
myline(pos_tools, pos_apk)
myplace(pos_apk, circle_apk)
myplace(pos_android, circle_android)
myplace(pos_aosp, circle_aosp)
myplace(pos_dataset, circle_dataset)
myplace(pos_devs, circle_devs)
myplace(pos_tools, circle_tools)
myplace(pos_google, circle_google)
})
}
#empty-slide[
Questions
]
#pagebreak()
#set page(height: auto, margin: 25mm)
#bibliography("bibliography.bib")